[HN Gopher] C and C++ prioritize performance over correctness
       ___________________________________________________________________
        
       C and C++ prioritize performance over correctness
        
       Author : bumbledraven
       Score  : 206 points
       Date   : 2023-08-18 16:27 UTC (6 hours ago)
        
 (HTM) web link (research.swtch.com)
 (TXT) w3m dump (research.swtch.com)
        
       | tester756 wrote:
       | >The problem is that C++ defines that every side-effect-free loop
       | may be assumed by the compiler to terminate. That is, a loop that
       | does not terminate is therefore undefined behavior. This is
       | purely for compiler optimizations, once again treated as more
       | important than correctness.
       | 
       | It is crazy that people decided to do it. It not only weird, but
       | also wrong.
       | 
       | There's no such a thing like "side-effect-free" code.
       | 
       | There's always side-effect like CPU usage, temperature, mem
       | usage, etc.
       | 
       | There is whole class of attacks based on those.
        
         | gpderetta wrote:
         | Side-effect is a Word of Power in the context of the standard
         | with very specific meaning that doesn't include, for example,
         | temperature increase. Even if you use it as a Control key
         | replacement.
        
         | dragonwriter wrote:
         | > There's always side-effect like CPU usage, temperature, mem
         | usage, etc.
         | 
         | If you are writing a loop in your code with no classic side
         | effects hoping to regulate temperature, that's probably a bad
         | and unreliable design to start with, whether or not the
         | compiler optimizes it away.
         | 
         | If you read a temperature sensor to validate that the loop is
         | doing the right thing and adjust frequency based on the result,
         | its still a little crazy but its less completely insane and no
         | longer free of classix side effects.
        
           | tester756 wrote:
           | All I'm saying is that code doesn't have to perform stuff
           | like prints, file write, http call in order to generate side
           | effect
           | 
           | You can run heavy computation and do not do anything with the
           | result just in order to increase CPU usage, right? (which
           | then should result in temp increase ;))
        
             | tylerhou wrote:
             | The C abstract machine isn't a physical machine, so
             | temperature is not a side effect :)
        
         | layer8 wrote:
         | What is meant are side effects as defined by the C abstract
         | machine. It has no concept of CPU speed, temperature, etc.
        
         | klodolph wrote:
         | > There's no such a thing like "side-effect-free" code.
         | 
         | This kind of like "Ceci n'est pas une pipe." In that it's
         | useful to remind people that there is no such thing as side-
         | effect-free code, strictly speaking, but as soon as you remind
         | people, you go back to talking about whether a particular piece
         | of code has side effects. Someone points at object depicted in
         | the painting and asks what it is, and you say "It's a pipe."
         | 
         | Basically, to reason about code, it's useful to ignore certain
         | classes of side effects, most of the time.
        
       | zzo38computer wrote:
       | I have some of my opinions about how it should do.
       | 
       | > Uninitialized variables
       | 
       | The compiler should be allowed to do one of:
       | 
       | 1. Assume any arbitrary value, as long as it remains consistent
       | until a new value is assigned (which might or might not depend on
       | the previous value). (However, the exact value need not be known
       | at compile-time, and might not even be known at run-time if the
       | program doesn't care about the exact value (in the example if the
       | loop is removed then the exact value doesn't matter at run-time).
       | It might be different every time the program runs.) Removing the
       | loop in the example is valid; it would also be valid to start at
       | a negative number (since you did not specify an unsigned type),
       | but if for example you declare i before the loop instead of
       | inside and then you add printf("%d\n",i); after the loop, then
       | the program is required to print a number which is at least 10
       | and can fit in the int type; if standard library optimizations
       | are enabled then it would be allowed to e.g. replace it with
       | puts("42"); instead (but if the loop is not removed, then the
       | number printed by this must be 10).
       | 
       | 2. Read the value from a register or memory which is then used as
       | the storage for that variable (for whatever time it needs to,
       | just as though it was given a value which was returned from a
       | function with no side-effects), but without initializing it.
       | 
       | 3. Do whatever the target instruction set does (which seems very
       | unlikely to me to do anything other than the above).
       | 
       | > Arithmetic overflow
       | 
       | The C specification should specify two's complement (it is the
       | only good representation of integers anyways). Fortunately GCC
       | has -fwrapv and -ftrapv, and I often use -fwrapv to avoid problem
       | with signed overflow.
       | 
       | > Infinite loops
       | 
       | There is a rationale with some sense, but maybe better would be
       | nevertheless not optimizing out the loop entirely like that.
       | However, if standard library optimizations are enabled then it
       | should be allowed to replace the loop with:
       | for(;;) pause();
       | 
       | > Null pointer usage
       | 
       | In this case, the program should just actually attempt to read,
       | write, or call the null address, which should result in a
       | segfault or other run-time error if possible (instead of being
       | optimized out). However, the compiler is allowed to assume that
       | such a call can never return, if it wishes (whether or not that
       | is actually true on the target computer).
       | 
       | In the case of a read or write that is not marked as volatile,
       | the compiler may substitute any value for a read and may optimize
       | out a write, although it does not have to; it may also do what is
       | specified above.
       | 
       | However, the optimization shown in the article should be allowed
       | if Do is a local variable which is uninitialized, rather than
       | being initialized as null. (You can disable optimizations (or
       | possibly change some options) if you don't like that.)
        
       | grandinj wrote:
       | rsc should really know better, he has been around long enough.
       | 
       | C and C++ have as a basic design rationale                   "as
       | close to machine performance as possible"              "don't pay
       | for what you don't use"              "don't break backwards
       | compatibility"
       | 
       | The last one is what is what is responsible for most of the
       | issues. It would be nice to fix stuff like UB, but the reason
       | that C/C++ is so popular and still alive is precisely because it
       | does care more about compatibility than fixing stuff likethat.
       | 
       | Breaking existing code is extremely uncool for long-lived
       | codebases.
        
         | agwa wrote:
         | Undefined behavior _permits_ backwards compatibility to be
         | broken, because when a compiler adds a new optimization that
         | exploits UB, existing programs can change behavior, breaking
         | code that previously worked. The blog post cites infinite loops
         | as an example. Thus, eliminating UB would improve backwards
         | compatibility.
        
           | layer8 wrote:
           | Compatibility is defined in terms of a contract between
           | program and language implementation. If the program violates
           | the contract by containing UB, then compatibility does not
           | apply.
           | 
           | Taking your argument, compilers couldn't even add any benign
           | optimization without breaking compatibility, because that
           | would change the runtime of the affected code.
        
             | agwa wrote:
             | > _Compatibility is defined in terms of a contract between
             | program and language implementation. If the program
             | violates the contract by containing UB, then compatibility
             | does not apply._
             | 
             | Indeed, furthering my point that having UB in a language
             | undermines backwards compatibility.
             | 
             | > _Taking your argument, compilers couldn't even add any
             | benign optimization without breaking compatibility, because
             | that would change the runtime of the affected code._
             | 
             | That does not follow from what I said.
        
               | layer8 wrote:
               | The point is, you have to define compatibility with
               | regards to _what_. Otherwise nothing can ever change. The
               | purpose of the C standard is to provide that reference
               | point, via its definition of what constitutes a
               | conforming program and a conforming language
               | implementation. You only get compatibility problems when
               | you step out of that scope.
        
       | haberman wrote:
       | > There are undeniably power users for whom every last bit of
       | performance translates to very large sums of money, and I don't
       | claim to know how to satisfy them otherwise.
       | 
       | That is the key, right there.
       | 
       | In the 1970s, C may have been considered a general-purpose
       | programming langauge. Today, given the landscape of languages
       | currently available, C and C++ have a much more niche role. They
       | are appropriate for the "power users" described above, who need
       | every last bit of performance, at the cost of more development
       | effort.
       | 
       | When I'm working in C, I'm frequently watching the assembly
       | language output closely, making sure that I'm getting the
       | optimizations I expect. I frequently find missed optimization
       | bugs in compilers. In these scenarios, undefined behavior is a
       | tool that can actually help achieve my goal. The question I'm
       | always asking myself is: what do I have to write in C to get the
       | assembly language output I expect? Here is an example of such a
       | journey: https://blog.reverberate.org/2021/04/21/musttail-
       | efficient-i...
       | 
       | I created the https://github.com/protocolbuffers/upb project a
       | long time ago. It's written in C, and over the years it has
       | gotten to a state where the speed and code size are pretty
       | compelling. Both speed and code size are very important to the
       | use cases where it is being used. It's a relatively small code
       | base also. I think focused, performance-oriented kernels are the
       | area where C makes the most sense.
        
         | overgard wrote:
         | > In the 1970s, C may have been considered a general-purpose
         | programming langauge. Today, given the landscape of languages
         | currently available, C and C++ have a much more niche role.
         | They are appropriate for the "power users" described above, who
         | need every last bit of performance, at the cost of more
         | development effort.
         | 
         | I really don't think this is true. I've worked in CAD and video
         | games and embedded software, and in all those you're likely
         | using C++ (not to mention a lot of desktop software that can't
         | afford to embed a chromium instance for electron.) For some
         | reason people here just assume that anything that isn't web
         | development or backend server CRUD stuff is a niche.
         | 
         | As much attention as something like Rust or whatnot gets on
         | hacker news, the reality is that if you can't afford garbage
         | collector pauses, and you need performance, you're using C/C++
         | most of the time.
        
           | kiratp wrote:
           | Rust is not a GC language. It can and does achieve the same
           | perf as any modem C++ with the latest flavor of the C++
           | safety guardrails.
           | 
           | The set of reasons to start a new project in C/C++ are few
           | and the list is shrinking by the day.
        
             | overgard wrote:
             | I phrased it poorly, I didn't mean to imply Rust is GC'd. I
             | mean that it's still niche. C++ very much is not niche,
             | which is my point. The other non-niche languages generally
             | are GC'd (java, C#, javascript, etc.)
             | 
             | > The set of reasons to start a new project in C/C++ are
             | few and the list is shrinking by the day.
             | 
             | That'd be true if every project started from scratch with
             | only the language standard library. And yet..... almost any
             | project you start is going to be dependent on a large chunk
             | of code you didn't write, even on greenfield projects.
        
               | haberman wrote:
               | > That'd be true if every project started from scratch
               | with only the language standard library. And yet.....
               | almost any project you start is going to be dependent on
               | a large chunk of code you didn't write, even on
               | greenfield projects.
               | 
               | I think this is true, and I'd refine my original
               | statement accordingly. My original comment was thinking
               | more from first principles, not as much about pragmatic
               | considerations of ecosystem support.
               | 
               | If we were to disregard the momentum of existing
               | ecosystems, I think C/C++ would be niche choices today:
               | very important for certain, focused use cases, but not
               | meant for the masses. Taking into account the momentum of
               | existing ecosystems however, they still play a large role
               | in many domains.
        
             | MarkMarine wrote:
             | I love Rust, but as a daily C++ user (embedded
             | microcontrollers, not supported by Rust yet, and then
             | native Linux tooling to interface with them) what I find
             | most frustrating about Rust is the number of crates that
             | need to be knit together to make working software. Often
             | these crates are from a single dev who last committed in
             | 2020 or something. I really wish there was something like
             | go's standard library in Rust. That has been my barrier to
             | adoption, and believe me I WANT to use it at work and I
             | don't want to juggle chainsaws anymore.
        
               | Ar-Curunir wrote:
               | Er how is that different from C++? It doesn't have a go-
               | like std library either, and you can totally use Rust
               | without crates.io in the same manner.
        
               | MarkMarine wrote:
               | I suppose that wasn't clear. My mistake. I use C/C++ for
               | micro controller code every day, because Rust doesn't
               | support my microcontroller, doesn't have a real RTOS, and
               | I'm making real hardware. Something that needs to run in
               | the field for 10+ years and I can't get out there and
               | ensure it's updated, and my company doesn't want to
               | invest millions of dollars on beta testing embedded rust
               | on. So embedded is out for now but I'm looking forward to
               | when it's supported fully and out of beta. Most embedded
               | controllers come with some RTOS and libraries they
               | support, written in C.
               | 
               | For tooling, I can context switch out of C++ which does
               | have boost, into [rust, go, python... etc] and deal with
               | that switch, or just be lazy and write it in C++. I've
               | tried to write three tools in Rust so far, and the pain
               | of not having a good stdlib, of essentially searching the
               | internet for a blog post that solves my issue then
               | finding the blog post was written by one dev 4 years ago
               | to pump up their library that was written 4 years ago and
               | then never supported after that... it's a bit exhausting.
               | 
               | Again, before ya'll attack. This is from the perspective
               | of a willing, excited customer. I want to use Rust at
               | work and advocate for it. Just saying, it's not easy in
               | the state it's in.
        
               | seabass-labrax wrote:
               | Which microcontroller are you using? Rust support for
               | embedded targets is slowly improving, so there might be a
               | beta build for your chip.
        
               | xedrac wrote:
               | In C++, you'll often see projects just use boost, which
               | is a big monolith of useful libraries.
        
               | tumdum_ wrote:
               | Embedded microcontrollers are not the place where boost
               | is used.
        
               | dgacmu wrote:
               | I'm sympathetic. I'm partway into an embedded project on
               | an stm32 / m0 for which there _is_ good rust support
               | through Embassy and it's utterly magical. And at the same
               | time, trying to do something for which there isn't a good
               | no-std crate is, er, anti-magical to the point of forcing
               | me to change the design. The ecosystem isn't as
               | comprehensive yet.
               | 
               | But when it works, wow. This has been the most fun
               | embedded project I've ever done with the least debugging.
        
               | oll3 wrote:
               | I have replaced my use of C (and C++) in embedded with
               | Rust for the last couple of years. Sure, some parts are
               | still missing or immature, but overall also very
               | promising. And I do enjoy it so much more than the MCU
               | manufacturer's please-insert-your-c-code-between-these-
               | comments-and-pray kind of development.
        
             | bayindirh wrote:
             | However, Rust doesn't have a "I know what I'm doing, let me
             | be" switch. No, "unsafe" is not that.
             | 
             | I have a couple of languages in my belt C, C++, Go, Python,
             | Java are the primary ones. C and C++ are reserved for
             | "power use". For power use, I mean scientific, HPC stuff or
             | any code I need the full capacity of the processor.
             | 
             | To get that, sometimes you need -ffmast-math, sometimes
             | said undefined behavior, or code patterns which translate
             | to better code for the processor you target.
             | 
             | Rust doesn't allow any of that, moreover limits me the way
             | I can write my programs, no thanks.
             | 
             | For security-sensitive compiled code, I can use Rust, but I
             | can't use Rust anywhere, and everywhere. It also has its
             | niche, and it can't and won't replace C or C++ at the end
             | of the day. It'll get its slice (which might be sizeable),
             | a couple of compiler implementations, and will evolve to
             | become another mainstream , boring language, which is good.
             | 
             | Also, if you give half an effort, C++ is pretty secure and
             | robust to begin with, esp. on the pointers department. Just
             | be mindful, and run a couple of valgrind tests on your
             | code, and you're set. Been there, done that.
        
               | kouteiheika wrote:
               | > To get that, sometimes you need -ffmast-math, sometimes
               | said undefined behavior, or code patterns which translate
               | to better code for the processor you target.
               | 
               | > Rust doesn't allow any of that, moreover limits me the
               | way I can write my programs, no thanks.
               | 
               | I'm willing to prove you wrong here. Can you give some
               | concrete examples?
               | 
               | For -ffast-math you can most certainly enable it, but
               | AFAIK for now only in a localized fashion through
               | intrinsics, e.g.:
               | 
               | https://doc.rust-
               | lang.org/std/intrinsics/fn.fadd_fast.html
               | 
               | https://doc.rust-
               | lang.org/std/intrinsics/fn.fmul_fast.html
               | 
               | So instead of doing this the dumb way and enabling
               | -ffast-math for the whole program (which most certainly
               | won't need it) you can profile where you're spending the
               | majority of your time, and only do this where it'll
               | matter, and without the possibility of randomly breaking
               | the rest of your numeric code.
               | 
               | Personally I find this to be a vastly better approach.
        
               | bayindirh wrote:
               | Thanks for the information, I'll look into that.
               | 
               | > So instead of doing this the dumb way and enabling
               | -ffast-math for the whole program (which most certainly
               | won't need it)...
               | 
               | When you write HPC enabled simulation code, you certainly
               | need it, and this is where I use it.
        
               | kiratp wrote:
               | > Rust doesn't have a "I know what I'm doing, let me be"
               | switch.
               | 
               | Every piece of code you write is security-sensitive. IMO
               | It is our professional responsibility to our users that
               | we treat it that way.
               | 
               | For a vast majority of situations, even if you know what
               | you are doing, you are human and therefore will make
               | mistakes.
               | 
               | If the (probably) most heavily invested C++ codebase,
               | Chrome, has memory use-after-free/overflow bugs, there is
               | no way that truly safe C++ can be written at any product-
               | scale.
        
               | bayindirh wrote:
               | > Every piece of code you write is security-sensitive.
               | 
               | No, not every application is CRUD, not every application
               | is interactive and no, not every application serves users
               | and communicate with outside world in a way allowing to
               | be abused during its lifetime.
               | 
               | This doesn't mean it gives me the freedom to write
               | reckless code all over, but the security model of an
               | authentication module and a mathematical simulation is
               | vastly different.
        
               | andrepd wrote:
               | In what sense does rust not expose the full power of the
               | processor to the user? Can you give a concrete example?
               | 
               | > Just be mindful, and run a couple of valgrind tests on
               | your code, and you're set.
               | 
               | Thousands of severe CVEs every year attest to the
               | effectiveness of that mindset. "Just git gud" is not a
               | meaningful thing to say, as even experienced devs
               | routinely make exploitable (and other) mistakes.
        
               | bayindirh wrote:
               | Rust probably won't allow me to implement a couple of
               | lockless parallel algorithms I have implemented in my
               | Ph.D., which are working on small matrices (3K by 3K).
               | These algorithms allow any number of CPUs do the required
               | processing without waiting for each other, and with no
               | possibility of stepping on each other, by design.
               | 
               | In the tests, it became evident that the speed of the
               | code is limited by the memory bandwidth of the system,
               | yet for the processors I work with that limit is also
               | very near to practical performance limit of the FPUs, as
               | well. I could have squeezed a bit more performance by
               | reordering the matrices to abuse prefetcher better, but
               | it was good enough and I had no time.
               | 
               | Well, the method I verify the said codebase is here [0].
               | Also, if BSD guys can write secure code with C, everybody
               | can. I think their buffer overflow error count is still
               | <10 after all these years.
               | 
               | [0]: https://news.ycombinator.com/item?id=31218757
        
           | jacquesm wrote:
           | For me the key is that I can lay things out in memory
           | _exactly_ the way I want, if necessary to the point where I
           | can fit things in cache entirely when I need the performance
           | and only to break out of the cache when I 'm done with the
           | data. This is obviously not always possible but the longer
           | you can keep it up the faster your code runs and the gains
           | you can get like that are often quite unexpected. I spend a
           | lot more time with gprof than with a debugger.
        
           | Cthulhu_ wrote:
           | > For some reason people here just assume that anything that
           | isn't web development or backend server CRUD stuff is a
           | niche.
           | 
           | I think that's because anything more "low level" (using the
           | phrase freely) quickly becomes highly specialized, whereas
           | web / CRUD development is a dime a dozen.
           | 
           | Source: Am web / CRUD developer. It's a weird one, on the one
           | side I've been building yet another numeric form field with a
           | range validation in the past week, but on the other I can
           | claim I've worked in diverse industries; public transit,
           | energy, consumer investment banking, etc. But in the end it
           | just boils down to querying, showing and updating data, what
           | data? Doesn't matter, it's all just strings and ints with
           | some validation rules in the end.
           | 
           | But there's my problem, I don't know enough about specialized
           | software like CAD or video games or embedded software to even
           | have an interest in heading in that direction, let alone
           | actually being able to find a job opening for it, let alone
           | being able to get the job.
        
         | mcguire wrote:
         | In the 1970s, the alternative to C was assembly. In fact, I can
         | remember hearing stories of the fights needed to pry every-day,
         | normal developers---not systems programmers, not power users---
         | off of assembly to a "higher level language".
         | 
         | It wasn't until the 90s that it became clear that C was not
         | appropriate for applications work (and this was in the era of
         | 4-16MB machines), although that took a long time to sink in.
        
           | overgard wrote:
           | The sheer amount of software that you're likely using right
           | now that's written in C would seem to contradict your claim.
        
             | SubjectToChange wrote:
             | The market share of C in desktop, server, and other
             | "enterprise" applications has _drastically_ dropped since
             | the 90s. Nowadays it 's quite rare to see C chosen for new
             | projects where it isn't required. In fact, despite of how
             | pervasive it is, a huge amount of C code cannot be built on
             | a pure C toolchain, i.e. C is essentially like Fortran.
        
           | daymanstep wrote:
           | Why is C inappropriate for applications work?
        
             | grumpyprole wrote:
             | The technical arguments should be obvious, e.g. spending
             | ones complexity budget on manual memory management and
             | avoiding footguns. But one amusing anecdote is that the
             | open source GNOME project founders were so traumatized by
             | their experience building complex GUI apps in C (already a
             | dubious idea 20 years ago), that they started building a C#
             | compiler and the Mono project was born.
        
               | seabass-labrax wrote:
               | In 2023, you'll be hard pushed to find a GNOME
               | application actually using C# and Mono. The vast majority
               | of GNOME components are written in C, with a large number
               | of them written in Vala and some in Rust and JavaScript.
        
               | grumpyprole wrote:
               | It was indeed a big yak to shave. Cloning a Microsoft
               | technology probably wasn't a good idea for OSS adoption
               | either.
        
             | TillE wrote:
             | Try writing a generic, reusable data structure in C. It's
             | agony.
        
               | bluetomcat wrote:
               | You don't need to write generic and reusable data
               | structures in C. You write a data structure suited for
               | the problem at hand, which often means that it's going to
               | be simpler and more performant because of the known
               | constraints around it.
        
               | grumpyprole wrote:
               | It _could_ be more performant because of the known
               | constraints around it, or it could be an ad-hoc,
               | informally-specified, bug-ridden, slow implementation of
               | half of some data structure. At least with a generic and
               | resuable data structure you have a known _reliable_
               | building block. Again, performance over safety.
        
               | commonlisp94 wrote:
               | > It could be more performant
               | 
               | No, it almost always is. The designers of a generic
               | library can't anticipate the use case, so can't make
               | appropriate tradeoffs.
               | 
               | For example, compare `std::unordered_map` to any well
               | written C hash table. The vast majority of hash tables
               | will never have individual items removed from them, but a
               | significant amount of complexity and performance is lost
               | to this feature.
        
               | SubjectToChange wrote:
               | _No, it almost always is._
               | 
               | A library author can spend ridiculous amounts of time
               | refining and optimizing their implementations, far more
               | than any application programmer could afford or justify.
               | 
               |  _The designers of a generic library can 't anticipate
               | the use case, so can't make appropriate tradeoffs._
               | 
               | This is definitely not true. Take C++ for instance, not
               | only is it possible to specialize generic code for
               | particular types, but it's absolutely routine to do so.
               | Furthermore, with all sorts of C++ template features
               | (type traits, SFINAE, CRTP, Concepts, etc) even user-
               | defined types can be specialized, in fact it's possible
               | to provide users with all sorts of dials and knobs to
               | customize the behavior of generic code for their own use
               | case. This functionality is not just a quality-of-life
               | improvement for library users, it has profound
               | implications for performance portability.
               | 
               |  _For example, compare `std::unordered_map` to any well
               | written C hash table._
               | 
               | std::unordered_map is a strawman. There are a plethora of
               | generic C++ hash tables which would match, if not soundly
               | outperform, their C counterpart. Also, even if we blindly
               | accepted your claim, then how do you explain qsort often
               | being beaten by std::sort or printf and its variants
               | being crushed by libfmt? What about the fact that Eigen
               | is a better linear algebra library than any alternative
               | written in C?
        
               | NavinF wrote:
               | That must be why every C project has its own string,
               | dynamic array, hashtable, etc. It's definitely more
               | performant to have several different implementations of
               | the same thing fighting for icache
        
               | commonlisp94 wrote:
               | It's a nice thought, but in practice C binaries are
               | orders of magnitude smaller than any other language. Also
               | compilers make that tradeoff for inlining all the time.
        
               | overgard wrote:
               | To be fair, most C++ projects (outside of embedded and
               | games) use STL for string/dynamic array/hashtable, and
               | while that standardization is certainly convenient I'm
               | not sure STL is generally faster than most hand written C
               | data structures, even with the duplicated code.
        
               | commonlisp94 wrote:
               | That's writing some other language in C syntax. You use
               | arrays or you write a specialized version for the use
               | case.
        
               | StillBored wrote:
               | Agony might be a bit much, and i'm not trying to defend C
               | because this is one of the strong reasons for using
               | C++/STL...
               | 
               | But, generally one should just reach for a library first
               | before doing a complex data structure regardless of
               | language. And for example, the linux kernel does a fine
               | job of doing some fairly complex data structures in a
               | reusable way with little more than the macro processor
               | and sometimes a support module. In a way the ease of
               | writing linked lists/etc are why there are so many
               | differing implementations. So, if your application is
               | GPL, just steal the kernel functions, they are reasonably
               | well optimized, and tend to be largely debugged, are
               | standalone, etc.
        
               | sn_master wrote:
               | Try doing the same in Go and it's even worse.
        
           | Thiez wrote:
           | In the 1960s they already had languages such as Lisp,
           | Fortran, Algol, Basic... Even Pascal is two years older than
           | C, and ML also came out around that time.
           | 
           | The statement "the alternative to C was assembly" is simply
           | incorrect.
        
             | notacoward wrote:
             | The fact that those things _existed_ does not refute the GP
             | 's point. Many companies well into the 80s at least had
             | programmers and codebases that had to be weaned away from
             | assembly, despite the fact that higher-level alternatives
             | had existed for a while. That's just empirical fact,
             | regardless of the reasons. I myself started programming in
             | 68K assembly on the original Mac because _I couldn 't
             | afford_ a compiler and interpreted languages (there were at
             | least Lisp and Forth) couldn't be used to write things like
             | desk accessories. Remember, gcc wasn't always something one
             | could just _assume_ for any given platform. The original
             | statement is correct; yours is not, because of limited
             | perspective.
        
             | jacquesm wrote:
             | For practical purposes - involving real world constraints
             | in memory size, cpu power and storage - C was _the_ tool of
             | choice, it ran on just about everything, allowed you to get
             | half decent performance without having to resort to
             | assembler and was extremely memory efficient compared to
             | other languages. Compilers were affordable (Turbo C for
             | instance, but also many others such as Mark Williams C)
             | even for young people like me (young then...) and there was
             | enough documentation to get you off the ground. Other
             | languages may have been available (besides the ubiquitous
             | BASIC, and some niche languages such as FORTH) but they
             | were as a rule either academic, highly proprietary and
             | very, very expensive.
             | 
             | So that left C (before cfront was introduced). And we ran
             | with it, everybody around was using C, there wasn't the
             | enormous choice in languages that you have today, you
             | either programmed in C or you were working in assembler for
             | serious and time-critical work.
        
             | marcosdumay wrote:
             | On practice those were either proprietary or required beefy
             | machines that the people writing C couldn't get.
             | 
             | On the limited environment where C got created, it was the
             | only option. And everybody suddenly adopted that limited
             | environment, because it was cheap. And then it improved
             | until you could use any of those other languages, but at
             | that point everybody was using C already.
        
               | StableAlkyne wrote:
               | Fortran predates C by over a decade, and dozens of
               | compilers existed for it by the mid-60s. Much of that
               | legacy code is still in use to this day in scientific
               | computing. One example: John Pople won his Nobel prize
               | for work he did (and implemented in Fortran) in
               | computational chemistry - the prize was delayed until '98
               | but he did the work in the 60s. The software he
               | commercialized it into, Gaussian, still gets releases to
               | this day and remains one of the most popular
               | computational chemistry packages.
               | 
               | It's really dependent on which field you're in. Not all
               | scientific computing requires a beefy computer, but for a
               | very long time it (and I guess LISP) dominated scientific
               | computing. That said, I think it's a very good point to
               | bring up the network effect of using C - if I need to
               | hire a developer in 1985, it's probably easier to find
               | someone with industry (not academic) experience who knows
               | C than it is to find someone who knows Fortran.
               | 
               | I do kinda prefer Fortran to C though, it's so much
               | cleaner to do math in. Maybe somewhere there's an
               | alternate universe where IBM invented C and Bell invented
               | Fortran to win the popularity war.
        
               | jacquesm wrote:
               | I tried to get access to FORTRAN and it was just way too
               | expensive and required machines that I would not have
               | been able to get close to. C ran on anything from Atari
               | STs, IBM PCs, Amiga's and any other machine that an
               | ordinary person could get their hands on.
               | 
               | The other mainstream language at the time was BASIC,
               | comparable to the way PHP is viewed today by many.
               | 
               | And with the advent of the 32 bit x86 era GCC and djgpp
               | as well as early Linux systems really unlocked a lot of
               | power. Before then you'd have to have access to a VAX or
               | a fancy workstation to get that kind of performance out
               | of PC hardware. It's funny how long it took for the 386
               | to become fully operational, many years after it was
               | launched you had to jump through all kinds of hoops to be
               | able to use the system properly, whereas on a 68K based
               | system costing a tiny fraction that was considered
               | completely normal.
        
               | StableAlkyne wrote:
               | My perspective is a bit biased by scientific computing, I
               | do more of that than enterprise stuff (and Python has
               | been fine for personal use). It's cool to see the
               | perspective of someone who was around for the early
               | stages of it though.
               | 
               | How did people see Fortran back then - nowadays it's seen
               | as outdated but fast, but was it seen as interesting, and
               | what drove you to seek it out?
               | 
               | Other side question if it's okay, I keep seeing
               | references to VAXen around historical documents and
               | usenet posts from the 80s and 90s, what made them special
               | compared to other hardware you had back then?
        
               | jacquesm wrote:
               | My one experience with FORTRAN was when working for a big
               | dutch architect who made spaceframes, I built their cad
               | system and a colleague built the finite element analysis
               | module based on an existing library. We agreed on a
               | common format and happily exchanged files with
               | coordinates, wall thicknesses and information about the
               | materials the structure was made out of and forces in one
               | direction and a file with displacements in the other. It
               | worked like a charm (combined C and FORTRAN). I thought
               | it was quite readable, it felt a bit archaic but on the
               | whole not more archaic than COBOL which I had also worked
               | with.
               | 
               | The reason that library existed in FORTRAN was that it
               | had a native 'vector' type and allowed for decent
               | optimization on the proper hardware (ie: multiply and
               | accumulate) which we did not have. But the library had
               | been validated and was approved for civil engineering
               | purposes, porting it over would have been a ton of work
               | and would not have served any purpose, besides it would
               | have required recertification.
               | 
               | As for VAXen: A VAX 11/780 is a 32 bit minicomputer,
               | something the size of a large fridge (though later there
               | were also smaller ones and today you could probably stick
               | one on a business card). It had a - for the time - a
               | relatively large amount of memory, and was a timesharing
               | system, in other words, multiple people used the same
               | computer via terminals.
               | 
               | They weren't special per se other than that a whole raft
               | of programmers from those days cut their teeth on them
               | either because they came across them in universities or
               | because they worked with them professionally. They were
               | 'affordable' in the sense that you did not need to be a
               | multinational or a bank in order to buy one, think a few
               | hundred thousand $ (which was still quite a fortune back
               | then).
               | 
               | I had occasional access to one through the uni account of
               | a friend, but never did any serious work with them. The
               | first powerful machine I got my hands on was the Atari
               | ST, which had a 68K chip in it and allowed the connection
               | of a hard drive. Together those two things really boosted
               | my capabilities, suddenly I had access to a whole 512K of
               | RAM (later 1M) and some serious compute. Compared to a
               | time shared VAX it was much better, though the VAX had
               | more raw power.
               | 
               | Concurrent to that I programmed on mainframes for a bank
               | for about a year or so, as well as on the BBC Micro
               | computer (6502 based) and the Dragon 32 (a UK based Color
               | Computer clone).
               | 
               | Fun times. Computing back then was both more and less
               | accessible than it is today. More because the machines
               | were _so_ much simpler, less because you didn 't have the
               | internet at your disposal to look stuff up.
        
               | grumpyprole wrote:
               | IIRC, Mac OS Classic was written in Pascal, as were other
               | operating systems. C just won the popularity contest.
        
               | marcosdumay wrote:
               | That was about a decade after C had already won.
        
               | grumpyprole wrote:
               | The Xerox Alto was early 70's and that GUI OS was written
               | in Pascal. I always thought Pascal was better designed
               | and less bizarre than C. Null terminated strings were
               | particularly a bad idea.
        
               | marcosdumay wrote:
               | Oh, by 73. Impressive. I didn't know that.
               | 
               | Still, that was a much more powerful machine than the
               | ones people wrote C for. And when the cheap segment of
               | those "fridge computers" became powerful enough to run
               | whatever you wanted to put on it, people started using
               | small workstations. And when those workstations became
               | powerful enough, we got PCs.
               | 
               | It's only when the PCs got powerful enough that we could
               | start to ignore the compiler demands and go with whatever
               | language fit us better. (The later reductions all were
               | based on cross-compiling, so they don't matter here.)
        
             | mcguire wrote:
             | How many Lisp Machines did Texas Instruments sell? (I mean
             | outside of that one lab at UT Austin.) :-)
             | 
             | I'm talking about applications developers who came out of
             | the small mainframe/minicomputer world of the 70s and into
             | the workstation/microcomputer world of the 80s. They
             | started with assembly, and prying them off of it was as
             | hard as convincing engineers to use FORTRAN. Convincing
             | those application developers to use a garbage collected
             | language, Java, was hard _in the 90s._
        
           | pjmlp wrote:
           | Only inside Bell Labs, the world outside was enjoying high
           | level systems programming languages since 1958 with the
           | introduction of JOVIAL.
        
           | Gibbon1 wrote:
           | When talking about hot path optimizations though assembly is
           | still a good alternative.
        
         | ianlevesque wrote:
         | Thanks for giving an actual example of such optimizations. In
         | my personal experience my C++ (and Rust) code was often
         | outperformed by the JVM's optimizations so I've found it hard
         | to relate to the tradeoffs C++ developers assume are obvious to
         | the rest of us.
        
           | grumpyprole wrote:
           | +1. Part of the problem is that x86 assembly is hardly
           | programming the metal anymore. The performance
           | characteristics of processors has changed over the years
           | also, compilers can be more up-to-date than humans.
        
             | overgard wrote:
             | x86 assembly doesn't represent the actual opcodes the CPU
             | executes anymore, but it's still the low level "API" we
             | have to the CPU. Even if assembly isn't programming to the
             | metal, it's definitely more to the metal than C, and C is
             | more to the metal than Java, etc. Metalness is a gradient
        
               | grumpyprole wrote:
               | Lol, I like the word "metalness".
        
               | SubjectToChange wrote:
               | _Metalness is a gradient_
               | 
               | It would be better to say that "Metalness" is a sort of
               | "Feature Set". IMO, most programmers would tend to agree
               | that C++ is far closer to Java than C is, yet C++ is
               | every bit as low level as C is. Indeed, even a managed
               | language like C# supports raw pointers and even inline
               | assembly if one is willing to get their hands dirty.
        
           | kllrnohj wrote:
           | It would be fascinating if you could give any such comparison
           | examples. The only time I've seen JVM come anywhere close to
           | C++ in normal usage is if the C++ code was written like it
           | was Java - that is, lots of shared_ptr heap allocations for
           | nearly everything. Or perhaps you're one of the rare few that
           | write Java code like it was C instead? You can definitely get
           | something fast like that, but it seems all too rare.
        
           | overgard wrote:
           | I think the reason to use C/C++ over java has less to do with
           | the various optimizations and more to do with control over
           | memory layout (and thus at least indirect control over cache
           | usage and so on). Plus you remove a lot of "noise" in terms
           | of performance (GC hiccups, JIT translation, VM load time,
           | etc.).
        
           | lbrandy wrote:
           | I struggle to resonate with what you are saying, as my
           | experience is the opposite. I'm curious where this
           | discrepancy is rooted. Reckless hypothesis: are you working
           | on majority latency or majority throughput sensitive systems?
           | 
           | I have seen so, so, so many examples of systems where
           | latencies, including and especially tail latencies, end up
           | mattering substantially and where java becomes a major
           | liability.
           | 
           | In my experience, actually, carefully controlling things like
           | p99 latency is actually the most important reason C++ is
           | preferred rather than the more vaguely specified
           | "performance".
        
             | ianlevesque wrote:
             | The specific example that comes to mind was translating a
             | Java application doing similarity search on a large dataset
             | into fairly naive Rust doing the same. Throughput I guess.
             | It may be possible to optimize Rust to get there but it's
             | also possible to (and in this case did) end up with less
             | understandable code that runs at 30% the speed.
             | 
             | Edit: And probably for that specific example it'd be best
             | to go all the way to some optimized library like FAISS, so
             | maybe C++ still wins?
        
             | grumpyprole wrote:
             | I've seen C++ systems that are considerably slower than
             | equivalent Java systems, despite the lack of stack
             | allocation and boxing in Java. It's mostly throughput,
             | malloc is slow, the C++ smart pointers cause memory
             | barriers and the heap fragments. Memory management for
             | complex applications is hard and the GC often gives better
             | results.
        
               | vvanders wrote:
               | I've seen so may flat profiles due to shared_ptr. Rust
               | has done a lot of things right but one thing it really
               | did well was putting a decent amount of friction into
               | std::sync::Arc<T>(and offering std::rc::Rc<T> when you
               | don't want atomics!) vs &mut T or even Box<T>. Everyone
               | reaches for shared_ptr when 99% of the time unique_ptr is
               | the correct option.
        
               | Gibbon1 wrote:
               | From my experience with embedded coding you are correct.
               | Most stuff lives and dies enclosed in a single call chain
               | and isn't subject to spooky action at a distance. And
               | stuff that is I often firewall it behind a well tested
               | API.
        
         | latenightcoding wrote:
         | Read your article (and cloudfare's) and as someone who uses
         | musttail heavily I don't understand the hype, as you mentioned
         | in your blog: you can get tail call optimizations with (-O2),
         | musttail just gives you that guarantee which is nice, but the
         | article makes it sound as if it unlocks something that was not
         | possible before and interpreters will greatly benefit from it,
         | but it's more reasonable to ask your user to compile with
         | optimizations on than it is to ask them to use a compiler that
         | supports musttail (gcc doesn't). Moreover, musstail has a lot
         | of limitations it would be hard to use in more complex
         | interpreter loops
        
           | mtklein wrote:
           | Ordinarily you're at the whim of the optimizer whether calls
           | in tail position are made in a way that grows the stack or
           | keeps it constant. musttail guarantees that those calls can
           | and are made in a way that does not let the stack grow
           | unbounded, even without other conventional -On optimization.
           | This makes the threaded interpreter design pattern safe from
           | stack overflow, where it used to be you'd have to make sure
           | you were optimizing and looking carefully at the output
           | assembly.
           | 
           | If nothing else musttail aids testing and debugging.
           | Unoptimized code uses a lot more stack, both because it
           | hasn't spent the time to assign values to registers, but
           | people often debug unoptimized code because having values
           | live on the stack makes debugging easier. The combination of
           | unoptimized code and calls in tail position not made in a way
           | that keeps stack size constant means you hit stack overflow
           | super easily. musttail means that problem is at least
           | localized to the maximum stack use of each function, which is
           | typically not a problem for small-step interpreters.
           | Alternatives to musttail generally involve detecting somehow
           | whether or not enough optimization was enabled and switching
           | to a safer, slower interpreter if not positive... but that
           | just means you're debug and optimized builds work totally
           | differently, not at all ideal!
        
         | jjoonathan wrote:
         | Perf is a niche, here's another: address space lets you talk to
         | hardware.
         | 
         | VM and OS abstractions have been so successful that you can go
         | a whole career without talking directly to hardware, but
         | remember, at the bottom of the abstraction pile something has
         | to resolve down to voltages on wires. Function calls, method
         | names, and JSON blobs don't do that. So what does? What
         | abstraction bridges the gap between ASCII spells and physical
         | voltages?
         | 
         | Address space. I/O ports exist, but mostly for legacy/standard
         | reasons. Address space is the versatile and important bridge
         | out of a VM and into the wider world. It's no mistake that C
         | and C++ let you touch it, and it's no mistake that other
         | languages lock it away. Those are the correct choices for their
         | respective abstraction levels.
        
           | packetlost wrote:
           | Idk what you're on about, I mmaped a `/dev/uio` from Python
           | this morning. Yeah, I had to add it in a .dts file and
           | rebuild my image, but even slow as shit high level languages
           | like Python let you bang on registers if you really want to.
        
             | notacoward wrote:
             | That worked because you were on an OS that supported it,
             | using a device with simple enough behavior that things like
             | timing or extra/elided writes didn't matter. It's great
             | when that works, but there are _very_ many environments and
             | devices for which that option won 't exist.
        
           | SubjectToChange wrote:
           | Perhaps, but C and C++ assume flat address spaces and modern
           | hardware includes many programmable devices with their own
           | device memory, e.g. GPUs. Naturally this discontinuity causes
           | a great deal of pain and many schemes have been developed to
           | bridge this gap such as USM (Unified Shared Memory).
           | 
           | Personally I would like to see a native language which
           | attempts to acknowledge and work with disjoint and/or "far
           | away" address spaces. However the daunting complexity of such
           | a feature would likely exclude it from any portable
           | programming language.
        
             | vvanders wrote:
             | Those disjoint memory addresses can be an absolute pain to
             | deal with, ask anyone who had to spend time dragging
             | performance out of the PS3 despite it being faster on
             | paper. UMA/USM can also bring it's own set of issues when
             | you have pathological access patterns that collide with
             | normal system memory utilization.
             | 
             | For what its worth UMA/USM wasn't build to bridge a gap but
             | rather to offer greater flexibility in resource utilization
             | for embedded platforms, that's been moving upstream(along
             | with tiling GPUs) over the years. With UMA you can page in
             | other data on a use-case basis which is why they were
             | relatively popular in phones, if you don't have a bunch of
             | textures loaded on the GPU you can give that space back to
             | other programs. Although come to think of it we used to
             | stream audio data from GPU memory on certain consoles that
             | didn't have large discrete system memory(the bus connecting
             | System <-> GPU memory had some pretty harsh restrictions so
             | you had to limit it to non-bursty, low throughput data
             | which audio/music fit well into).
        
           | wrs wrote:
           | Rust is making advances here (look at "embedded Rust"
           | efforts). I am curious since I haven't written kernel code
           | since C went sideways: how easy is it to write a driver that
           | has to manipulate registers with arbitrary behavior at
           | arbitrary addresses with modern C compilers and avoid all
           | undefined behavior? I seem to recall Linus has a rant on
           | this.
        
         | SubjectToChange wrote:
         | Of course the problem is that the vast corpus of legacy C code
         | was not written with such aggressive compilers in mind.
        
         | jfengel wrote:
         | With modern CPUs, that kind of hand-tuned assembly gets rarer
         | and rarer. Pipelines and caching and branch prediction make it
         | hard to know what's actually going to be faster. And even when
         | you do know enough about the CPU, you only know that CPU --
         | sometimes only one model of a CPU.
         | 
         | There's still a niche for it, but it's tiny and it keeps
         | getting tinier.
        
           | izacus wrote:
           | That... really isn't all that true. I find that myth being
           | mostly perpetuated by people who don't do any kind of
           | performance work or have any understanding just how terribly
           | unperformant most code out there is.
        
             | jfengel wrote:
             | That is my observation as a compiler writer.
        
         | khuey wrote:
         | > When I'm working in C, I'm frequently watching the assembly
         | language output closely, making sure that I'm getting the
         | optimizations I expect. I frequently find missed optimization
         | bugs in compilers.
         | 
         | Do you repeat this exercise when you upgrade or change
         | compilers?
        
           | 8n4vidtmkvmk wrote:
           | If the assembly is that important I'd find a way to put it
           | into my unit tests. Create a "golden" for the assembly and it
           | should trigger a diff if it changes.
        
           | jacquesm wrote:
           | The easy way to achieve that is to freeze the assembly once
           | it is generated and to keep the C inlined around as
           | documentation, as well as a way to regenerate the whole thing
           | should it come to that (and then, indeed you'll need to audit
           | the diff to make sure the code generator didn't just optimize
           | away your carefully unrolled loops).
        
             | asvitkine wrote:
             | Is there any tooling for that or are you talking
             | hypothetically?
        
               | jacquesm wrote:
               | Just standard unix tooling, what else do you need? It's
               | as powerful a set of text manipulation tools as you could
               | wish for.
        
       | [deleted]
        
       | omoikane wrote:
       | Regarding uninitialized variables, there is a proposal to make
       | them default to zero-initialized:
       | 
       | http://wg21.link/P2723
       | 
       | Under "5. Performance" section, it claims the performance impact
       | to be negligible, and there is also a mechanism to opt-out.
        
       | deadletters wrote:
       | There are a lot of warning and error options. Turn the guiderails
       | and sanitizers on during development and testing. Turn the
       | optimizations on when you ship.
       | 
       | Check out: -wall -werror
       | https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
        
       | xjay wrote:
       | C = Control
       | 
       | I think in the world of C, the compiler assumes the author knows
       | what they're doing, and historically, you were probably supposed
       | to use a separate tool, a linter [1] (static analysis tool), to
       | help catch mistakes.
       | 
       | HN's hard disks were a recent (2022-07) victim of a firmware
       | developer's lack of understanding of integer overflow exceptions.
       | [2] The firmware was likely written in C, or "C++ C". A fix was
       | released in 2020. Another reminder to update the firmware of
       | these disks. [3]
       | 
       | ++crash;
       | 
       | [1] https://en.wikipedia.org/wiki/Lint_(software)
       | 
       | [2] https://en.wikipedia.org/wiki/Integer_overflow
       | 
       | [3] https://www.thestack.technology/ssd-death-bug-40000-hours-
       | sa...
        
         | agwa wrote:
         | Undefined behavior is the opposite of programmer control. As
         | the examples in the blog post show, you can write code that
         | explicitly deferences a NULL pointer, or enters an infinite
         | loop, and the compiler will think _surely the programmer didn
         | 't mean to do that_, and literally remove code that you wrote.
         | 
         | It's true that historically C has not done much to protect
         | programmers from their mistakes, but historically mistakes just
         | meant suffering the natural consequences (such as a SIGSEGV on
         | NULL pointer dereference). But these days, when you make a
         | mistake, C compilers will exploit your mistake to the maximum
         | extent possible, including changing the meaning of your
         | programs.
        
         | Rexxar wrote:
         | IMHO, having an additional debug mode where all optimisations
         | steps are performed but with asserts inserted to validate all
         | preconditions would mitigate a lot of problems.
         | 
         | For example adding a "assert(x < MAX_INT - 100)" in first
         | example.
         | 
         | Running test suites on this debug binaries would find a lot of
         | problems.
        
       | metadat wrote:
       | This is likely in response to this relevant related submission
       | discussed yesterday:
       | 
       |  _A Guide to Undefined Behavior in C and C++ (2010)_
       | 
       | https://news.ycombinator.com/item?id=37165042 (166 comments)
       | 
       | A worthwhile read, IMO. Cheers.
        
       | mannyv wrote:
       | If you don't like undefined behavior then avoid it. It's not
       | hard, unless you're not paying attention.
        
       | not2b wrote:
       | The reason for leaving integer overflow undefined was not
       | primarily because of one's complement machines. It was for loop
       | optimization.
       | 
       | Consider for (int i = 0; i < ARRAY_WIDTH; i++) out[i] = in[i +
       | offset];
       | 
       | Assuming that i + offset, &out + i, and &in + i + offset do not
       | overflow allows the loop to be cleanly vectorized without
       | checking for wraparound.
       | 
       | The compiler developers in the 80s were trying to come up with
       | rules that didn't require C to be 5x slower than Fortran on
       | scientific applications, and dealing with the consequences of
       | a[i] being equivalent to *(&a + i).
        
         | IshKebab wrote:
         | That sounds very unlikely given that _unsigned_ overflow _is_
         | defined. The original motivation for undefined behaviour was
         | not performance. I 'm pretty sure autovectorisation was not a
         | thing in 1989. The author's theory sounds far more likely.
         | 
         | I'd love to see an actual citation though if you aren't just
         | guessing.
        
         | dundarious wrote:
         | I don't think that was the historical justification, but
         | regardless, it is a terribly limiting and oblique hack of a
         | convention for guiding the compiler. Why should this guidance
         | only be possible for signed values? Why have such an important
         | distinction be controlled in such an indirect fashion?
         | 
         | Just 2 reasons why I prefer explicit vectorization anywhere
         | that I feel it is important to have vectorized code.
        
         | andersa wrote:
         | It sure would be nice if modern CPUs had two sets of integer
         | instructions: one that wraps, and one that triggers an
         | exception on wrap, with zero overhead in non-wrapping case.
         | 
         | Then we could compile all code with the latter, except for
         | specifically marked edge cases where wrap is a desired part of
         | the logic.
        
           | tpolzer wrote:
           | That doesn't help at all if your loop variable is a 32 bit
           | int that your compiler decided to transform away into
           | vectorized loads from a 64 bit pointer.
           | 
           | But that's exactly one of the transformations that get
           | enabled by assuming undefined overflow.
        
             | andersa wrote:
             | But in that case, it's not going to be wrapping either.
             | We'll just read beyond the end of the buffer, which a
             | bounds check should catch.
             | 
             | Or perhaps I'm not thinking of the specific sequence that
             | would 1) not wrap during modifying index and 2) not hit
             | bounds check after.
             | 
             | It would need to be a requirement that compilers can't
             | upcast all your ints to 64 bit ones, do all the math, and
             | then write them back - would need specific instructions for
             | each size.
        
           | fulafel wrote:
           | x86 had overflow checking support (via OF flag and INTO insn)
           | but it got slowed down and later dropped from 64 bit mode.
        
           | solarexplorer wrote:
           | MIPS for example has this. It has `addu` for normal integer
           | addition that does not trap and `add` if you want to trap on
           | overflows.
        
           | eklitzke wrote:
           | You can compile code with -fwrapv and for most programs the
           | overhead is minimal (the exception being that if you're
           | writing number crunching code, the overhead is going to be
           | huge). For my personal projects I have -fwrapv as part of the
           | default compiler flags, and I remove the flag in opt builds.
           | I honestly haven't caught that many bugs using it, but for
           | the few bugs it did catch it saved me a lot of debugging
           | time.
        
           | explaininjs wrote:
           | Language-level concern imo. https://doc.rust-
           | lang.org/nightly/core/num/struct.Wrapping.h...
        
             | andersa wrote:
             | It could be, if the hardware supported it. Consider this
             | quote from that page:
             | 
             | "in some debug configurations overflow is detected and
             | results in a panic"
             | 
             | That's not good enough. We want to _always_ detect it! Many
             | critical bugs are caused by this in production builds too.
             | Solving it at the language level would require inserting
             | branches on every integer operation which is obviously not
             | acceptable.
        
               | tialaramex wrote:
               | > That's not good enough. We want to always detect it
               | 
               | So, select the configuration where that's the behaviour?
               | overflow-checks = true
               | 
               | > Solving it at the language level would require
               | inserting branches on every integer operation
               | 
               | Yes, so that's what you have to do _if_ you actually want
               | this, if you won 't pay for it then you can't have it.
        
               | andersa wrote:
               | Well, the whole point of my post was that I would really
               | like a hardware feature that does it without overhead.
               | How that would work behind the scenes, I have no idea.
               | Not a hardware engineer.
        
               | tialaramex wrote:
               | If you really want it, use it. Hardware vendors optimise
               | stuff they see being done, they don't optimise stuff that
               | somebody mentioned on a forum they'd kinda like but have
               | never used because it was expensive. _Maybe_ if you have
               | Apple money and can buy ARM or something then you could
               | just express such whims, but for mere mortals that 's not
               | an option.
               | 
               | Newer CPUs in several lines clearly optimise to do
               | Acquire-Release because while it's not the only possible
               | concurrency ordering model, it's the one the programmers
               | learned, so if you make that one go faster your benchmark
               | numbers improve.
               | 
               | Modern CPUs often have onboard AES. That's not because it
               | won a competition, not directly, it's because everybody
               | uses it - without hardware support they use the same
               | encryption in software.
               | 
               | The Intel 486DX and then the Pentium happened because it
               | turns out that people like FPUs, and eventually enough
               | people were buying an FPU for their x86 computer that you
               | know, why not sell them a $100 CPU and a $100 FPU as a
               | single device for $190, they save $10 and you keep the
               | $80+ you saved because duh, that's the same device as the
               | FPU nobody is making an actual separate FPU you fools.
               | 
               | Even the zero-terminated string, which I think is a
               | terrible idea, is sped up on "modern" hardware because
               | the CPU vendors know C programs will use that after the
               | 1970s.
        
           | weinzierl wrote:
           | ARM actually kind of has that. The register file has an
           | overflow flag that is not cleared on subsequent operations
           | (sticky overflow). So instead of triggering an exception,
           | which is prohibitively expensive, you can do a series of
           | calculations and check afterwards if an overflow happened in
           | any of them. A bit like NaN for floating point. From what I
           | understand the flag alone is still costly, so we will have to
           | see if it survives.
        
             | spookie wrote:
             | That's quite interesting and reasonable
        
             | andersa wrote:
             | It doesn't matter if triggering the exception is expensive.
             | At that point overflow has already occured, so your program
             | state is now nonsense, and you might as well just let it
             | crash unhandled. Much better outcome than reading memory at
             | some mysterious offset.
             | 
             | If just having the ability for an exception to occur during
             | an instruction causes overhead, that would be a big problem
             | though.
             | 
             | Edit to add: We need to do the check on _every_ operation.
             | Just going through one iteration of the loop might have
             | already corrupted some arbitrary memory, for example.
             | Manually inserted checks on some flag bits don 't scale to
             | securing real programs.
        
               | Findecanor wrote:
               | > At that point overflow has already occured, so your
               | program state is now nonsense, and you might as well just
               | let it crash unhandled.
               | 
               | The trick is to check and clear the flag before any
               | instruction that would have a side-effect, that depends
               | on the arithmetic result.
               | 
               | IEEE 754-compliant floating point units have a similar
               | behaviour with NaN that is a bit more versatile: an
               | arithmetic instruction results in NaN if any operand is
               | NaN, but an instruction with side-effect (compare,
               | convert or store) will raise an exception if given a NaN.
        
               | weinzierl wrote:
               | Think about it like that. If you allow an exception you
               | essentially create many branches with all their negative
               | consequences. With the sticky bit you combine them to one
               | branch (that is still expensive[1]).
               | 
               | [1] https://news.ycombinator.com/item?id=8766417
        
             | [deleted]
        
         | Filligree wrote:
         | Maybe the idea hadn't been invented back then, but it seems
         | obvious to me that the correct response is an iterator
         | protocol. Or at least a hardcoded for-in syntax.
        
           | kimixa wrote:
           | If you don't initialize the initial iterator state, or
           | compare with something that isn't a valid iterator of the
           | same container, you kinda end up with the same issues.
           | 
           | The complaint here is that the warnings/errors for the
           | integer case don't seem to be on by default. With the
           | warnings command line flags enabled, this case is easily
           | detected by the compiler, same as if it was some iterator
           | object.
        
           | dahfizz wrote:
           | Care to explain? An iterator is a nice high level concept,
           | but the CPU still has to do the &in + i + offset arithmetic.
           | I don't see how replacing `i` with syntactic sugar changes
           | the need to check for overflow.
        
             | nostrademons wrote:
             | I think the point the GP is making is that with an iterator
             | protocol, _the iterator implementation_ itself is free to
             | make a different choice on implementation strategies, based
             | on the shape of the data and the hardware available, and
             | this is transparent to client code. So for example, a
             | container containing only primitive ints or floats on a
             | machine with a NVidia Hopper GPU might choose to allocate
             | arrays as a multiple of 64, and then iterate by groups of
             | 64, taking advantage of the full warp without needing any
             | overflow checks. Obviously a linked list or an array of
             | strings couldn 't do this, but then, they wouldn't want to,
             | and hiding the loop behind an iterator lets the container
             | choose the appropriate loop structure for the format and
             | hardware.
             | 
             | I've heard criticisms of C and C++ that they are
             | simultaneously too high-level and too low-level. Too high-
             | level in that the execution model doesn't actually match
             | the sort of massively parallel numeric computations that
             | modern hardware gives, and too low-level that the source
             | code input into the compiler doesn't give enough
             | information about the real structure of the program to make
             | decisions that really matter, like algorithm choice.
             | 
             | It's interesting that the most compute-intensive machine
             | learning models are actually implemented in Python, which
             | doesn't even pretend to be low-level. The reason is because
             | the actual computation is done in GPU/TPU-specific
             | assembly, so Python just holds the high-level intent of the
             | model and the optimization occurs on the primitives that
             | the processor actually uses.
        
               | PaulDavisThe1st wrote:
               | "It's interesting that a lot of performance-critical code
               | tends to be written in C++, which sometimes pretends not
               | to be that low level. The reason is because the actual
               | performance critical code is really running CPU-specific
               | assembly, so C++ just holds the high level intent of the
               | model and the optimization happens on the primitives that
               | the processor actually uses."
        
               | dahfizz wrote:
               | > the iterator implementation itself is free to make a
               | different choice on implementation strategies
               | 
               | That's just UB with more steps. What will the spec say?
               | "Behavior of integer overflow is undefined. Unless the
               | overflow happens within an iterated for loop, in which
               | case the behavior is undefined and the iterator can do
               | whatever it wants".
               | 
               | > I've heard criticisms of C and C++ that they are
               | simultaneously too high-level and too low-level.
               | 
               | I've heard this as well, and I think there is some truth
               | to it, but C is the least-bad offender relative to any
               | other language.
               | 
               | C maps extremely well to assembly. The fact that assembly
               | no longer perfectly captures the implementation of the
               | CPU has nothing to do with C. Every other general
               | purpose[1] language has to target the same abstraction
               | that C does.
               | 
               | Given that reality, C in fact maps better to the hardware
               | than any other language. Because it is faster than any
               | other language. Any higher level language that gives the
               | compiler more information about algorithm choice is
               | slower than C is. That's the bottom line.
               | 
               | [1] This is ignoring proprietary, hardware specific tools
               | like CUDA. That's clearly in a different category when
               | discussing programming languages, IMO.
        
               | nostrademons wrote:
               | The reasoning behind the decision to make integer
               | overflow UB changes. As the thread starter mentioned,
               | that reasoning was loops, so you don't need an overflow
               | counter for everyday loops. Take loops out of the
               | equation, and take certain high-performance integer
               | computations where arguably you should have a dedicated
               | FixedInt type, and the logical spec behavior might be
               | silent promotion to BigInt (like JS, Python 3, Lisp,
               | Scheme, Haskell) or a panic (like in Rust).
               | 
               | > [1] This is ignoring proprietary, hardware specific
               | tools like CUDA. That's clearly in a different category
               | when discussing programming languages, IMO.
               | 
               | Arguably they should be part of the conversation. One
               | main reason for the recent ascendancy of NVidia over
               | Intel is that they're basically unwrapping all the layers
               | of microcode translation that Intel uses to make a modern
               | superscalar processor act like an 8086, and saying "Here,
               | we're going to devote that silicon to giving you more
               | compute cores, you figure out how to use them
               | effectively."
        
               | commonlisp94 wrote:
               | > implemented in Python,
               | 
               | A program which constructs an AST out of python classes
               | and spits out GPU code is a compiler. The python is never
               | executed.
               | 
               | Trivially, compilers can generate code faster than their
               | hose language, but that doesn't make the host language
               | fast. The compiler would be even faster if it were
               | written in C++.
        
           | alexvitkov wrote:
           | Adding an iterator in C++ means adding at least 2 more
           | objects, multiple function calls, operator overload,
           | templates, the whole package.
           | 
           | You don't trust your compiler to optimize the sane trivial C
           | case, but you trust it to optimize all that garbage away?
        
             | RicardoLuis0 wrote:
             | if your data's sequential, creating an iterator in C++ is
             | as simple as returning a begin and end pointer, and will be
             | optimized away by any level other than O0
             | 
             | https://godbolt.org/z/WEjzEr5j4
        
               | josefx wrote:
               | But iterating over pointers is once again optimized with
               | lots of undefined behavior at the corners. So you are
               | replacing one source of undefined behavior with another.
        
               | AYoung010 wrote:
               | Replacing undefined behavior at the program-level with
               | undefined behavior written and tested as part of the
               | standard library, usually vendored and distributed in
               | concert with the compiler, seems like an obvious net-
               | positive to me.
        
               | commonlisp94 wrote:
               | > with lots of undefined behavior at the corners.
               | 
               | What behavior is undefined in incrementing a pointer
               | between a begin and end range?
        
               | josefx wrote:
               | Of course a basic iteration between begin()/end() will
               | never contain out of range elements, but neither will
               | valid increment between two integers. No need for
               | iterators in that case either.
               | 
               | Say I want to do something fancy, like getting element
               | number 11 from an array.
               | 
               | With an integer index I can pass 11, with random access
               | iterators I can use begin() + 11.
               | 
               | Now my array only has five elements. So I check.
               | 
               | 11 < 5? Comparison valid, successfully avoided a crash.
               | 
               | begin() + 11 < end() ? How where the rules for pointer
               | comparison again, something about within an allocation
               | and one past the end?
        
               | yazaddaruvala wrote:
               | Pointer arithmetic optimization based on undefined
               | behavior is a problem regardless.
               | 
               | Life is always better after minimizing the total number
               | of types of undefined behavior.
        
             | commonlisp94 wrote:
             | A pointer is a valid C++ iterator.
             | 
             | > but you trust it to optimize all that garbage away?
             | 
             | Yep, if you learn about compilers you learn what kind of
             | optimizations are easy and hard for them to make. The kind
             | that just flattens a few levels of nested definitions are
             | easier.
        
         | titzer wrote:
         | The solution is to have the compiler automatically split
         | iteration into a known in-bounds part and a possibly-out-of-
         | bounds part. In this case, generating an additional check that
         | {ARRAY_WIDTH < (INT_MAX - offset)} would be sufficient to
         | guarantee that {i + offset} doesn't wrap around, enabling
         | further reasoning in a specialized copy of the loop. (In this
         | example, it's unclear what the relation to ARRAY_WIDTH is to in
         | and out).
         | 
         | The HotSpot C2 compiler (and AFAIK, Graal) do partitition
         | iteration spaces in this way.
         | 
         | This does have some complexity cost in the compiler, of course,
         | and it produces more code, especially if the compiler generates
         | multiple copies of the loop. But given that relatively few
         | super-hot loops of this kind are in typical programs, it is
         | worth it.
        
           | logicchains wrote:
           | >But given that relatively few super-hot loops of this kind
           | are in typical programs, it is worth it.
           | 
           | In C programs these kind of super-hot loops are quite common,
           | because if someone didn't have many such loops they probably
           | wouldn't need to write in C/C++. And if C had the same
           | overhead as Java in these cases, then people who needed to
           | squeeze out every drop of performance would use a different
           | language.
        
             | titzer wrote:
             | > these kind of super-hot loops are quite common
             | 
             | People say this, then they write microbenchmarks. Then
             | compilers optimize the heck out of these microbenchmarks
             | (and numerical kernels). Rinse and repeat for several
             | decades, and every dirty trick that you can think of gets
             | blessed under the guise of UB.
             | 
             | When in reality, real programs do not spend all their time
             | in one (or even a handful) of super-hot loops. C was
             | designed to let compilers have a field day so that 1980s
             | supercomputers could get 5% more FP performance, by hook or
             | by crook. This kind of dangerous optimizations do not make
             | much different for the vast majority of application code,
             | the majority of which spends its time chasing pointers and
             | chugging through lots of abstraction layers.
        
               | TapamN wrote:
               | >C was designed to let compilers have a field day so that
               | 1980s supercomputers could get 5% more FP performance, by
               | hook or by crook.
               | 
               | C was designed to help create Unix. It just happened to
               | turn out to be something that could be compiled into more
               | efficient code than most other languages at the time,
               | without being overly difficult to work with.
               | 
               | From what I understand, 80s supercomputers were more
               | likely to run Fortran. Fortran has fewer problems with
               | pointer aliasing than C, so a Fortran compiler could
               | generate better code than a C compiler for the type of
               | code they would be running.
        
               | haberman wrote:
               | > When in reality, real programs do not spend all their
               | time in one (or even a handful) of super-hot loops.
               | 
               | This paper from 2015 finds that a large amount of CPU in
               | Google data centers is spent in a relatively small number
               | of core components, known as the "datacenter tax": https:
               | //static.googleusercontent.com/media/research.google.c...
               | > [We] identify common building blocks in the lower
               | levels of the software stack.         > This "datacenter
               | tax" can comprise nearly 30% of cycles         > across
               | jobs running in the fleet, which makes its constituents
               | > prime candidates for hardware specialization in future
               | server         > systems-on-chips.
               | 
               | Some of the components they identify are memmove()
               | (definitely a loop), protocol buffers (proto parsing is a
               | loop), and compression/decompression (a loop).
        
           | kllrnohj wrote:
           | The idea that only a tiny portion of code is "hot" is just
           | not true.
           | 
           | But you're also missing the entire idea anyway. It's not
           | about bounds checking, it's about at what point overflow
           | occurs. `int` is a sized type, so if overflow is defined for
           | it then it has to also overflow at that size. This prevents
           | using native size of the machine if it's larger than that of
           | `int`. Which these days it very often is since int is 32bit.
           | So you couldn't promote it to a 64bit relative address as
           | then it'll overflow at 64bits instead of 32bits.
        
       | crabbone wrote:
       | This is a really, really misguided take on C and C++...
       | 
       | They don't prioritize anything. They are just bad languages with
       | a random assortment of features mostly reflecting how people
       | thought about computers in the 70's.
       | 
       | Just think about this: vectorization is one of the obvious ways
       | to get better performance in a wide range of situations. Neither
       | C nor C++ have any support for that. Parallelism -- C++ kinda has
       | something... C has zilch. Memory locality anyone?
       | 
       | I mean, Common Lisp has a bunch of tools to aid compiler in
       | optimizing code _in the language itself_ , whereas C has like...
       | "inline" and the ability to mess with alignment in structs, and I
       | cannot really think about anything else. Stuff like UB isn't
       | making the language perform better or easier to optimize. It's
       | more of a gimmick that compiler authors for C and C++ found to
       | produce more efficient code. It's a misfeature, or a lack of
       | feature, that allowed for accidental beneficial side-effects.
       | Intentional optimization devices in a language are tools that
       | _prove the code to be intentionally correct first_ , and that
       | proof allows the compiler to elide some checks or to generate a
       | more efficient equivalent code, based on the knowledge of the
       | correctness of the proof (or, at least, upon explicit
       | instructions from the code author to generate "unsafe" code).
        
         | fooker wrote:
         | Wow, don't get so invested in hating a technology, it's just a
         | tool.
        
         | kazinator wrote:
         | > _vectorization is one of the obvious ways to get better
         | performance in a wide range of situations._
         | 
         | Or, well, in a narrow range of situations where you have
         | certain kinds of hardware with vectorization support.
         | 
         | > _C has zilch_
         | 
         | C has operating systems written in it which use parallel
         | processing internally and make parallel processing available to
         | applications. True, that may be done using techniques that are
         | not described in ISO C.
         | 
         | Common Lisp has undefined behavior, a familiar example being
         | (rplacd '(1 . 2) 3): clobbering a literal object.
         | 
         | Optimizations in Common Lisp are exactly like UB in C: you make
         | declarations which specify that certain things hold true, and
         | in an unsafe mode like (declare (optimize (safety 0) (speed
         | 3))), the compiler blindly trusts your assertions and generates
         | code accordingly. If you asserted that some variable is a
         | fixnum, but it's actually a character string, that character
         | string value's bit pattern will likely be treated as a fixnum.
         | Or other consequences.
         | 
         | Common Lisp is nice in that you can control speed and safety
         | tradeoffs on a fine granularity, but you do trade safety to get
         | speed.
         | 
         | Common Lisps (not to mention Schemes) have extensions, just
         | like C implementations. In plenty of code you see things like
         | #+allegro (do this) #+sbcl (do that).
        
       | thesuperbigfrog wrote:
       | "Performance versus correctness" is the same design tradeoff as
       | "Worse is Better":
       | 
       | https://www.dreamsongs.com/RiseOfWorseIsBetter.html
       | 
       | However our tolerance to accept "worse" over "better" is waning
       | since we have more capable hardware, better tools, and "worse"
       | leads to more problems later such as security vulnerabilities.
        
       | ngrilly wrote:
       | Masterful conclusion in the last paragraph!
        
       | strangescript wrote:
       | If only we had a modern language that did both... (which was
       | strangely omitted from this article)
        
       | aatd86 wrote:
       | > C and C++ do not require variables to be initialized on
       | declaration (explicitly or implicitly) like Go and Java. Reading
       | from an uninitialized variable is undefined behavior.
       | 
       | Took me some time to understand that part :)
       | 
       | I always thought that an unassigned variable of type slice T was
       | deemed uninitialized. (but then, what of assigning nil to a
       | previously non-nil variable? Is it considered deinitialized?)
       | 
       | In fact, at the language level it could be considered
       | uninitialized/deinitialized. That's the variable typestate. (cf
       | definite assignment, ineffectual assignment analysis)
       | 
       | For the compiler, it still "points" to a memory location so it is
       | initialized.
       | 
       | Am I right? (more than 344 comments, no one will find this one
       | question lol :-)
        
       | kazinator wrote:
       | Correctness can come from the programmer in ways that performance
       | cannot.
       | 
       | We usually aim for correctness, and in small programs we often
       | achieve it 100%.
       | 
       | 100% maximum performance is rarely achieved in a high level
       | language.
       | 
       | If you try hard at absolute correctness, you can get there; not
       | so with performance.
       | 
       | So, obviously, that must mean performance is harder than
       | correctness.
       | 
       | If you turn a performance aspect of a system into a correctness
       | requirement (e.g. real time domain, whether soft or hard) you
       | have hard work ahead of you.
       | 
       | In programming, we often make things easier for ourselves by
       | sacrificing performance. Oh, this will be executed only three
       | times over the entire execution lifetime of the program, so
       | doesn't have to run fast. Thus I can just string together several
       | library functions in a way that is obviously correct, rather than
       | write loops that require proof.
       | 
       | That said, if you have certain kinds of correctness requirements,
       | C becomes verbose. What is easy to express in C is an operation
       | on inputs which have already been sanitized (or in any case are
       | assumed to) so that the operation will correctly execute. It's
       | not a priority to make it convenient to do that validating, or to
       | just let the operation execute with inputs and catch the problem.
       | 
       | E.g. say that we want to check whether there would be overflow if
       | two numeric operands were multiplied. It gets ugly.
        
         | dralley wrote:
         | Performance can absolutely come from the programmer. It's a
         | question of defaults. One can default to correctness and opt-in
         | to performance (in the places where critically needed, often a
         | small portion of the program) much more easily than one can
         | default to performance and opt-in to correctness.
        
           | dahfizz wrote:
           | > Performance can absolutely come from the programmer.
           | 
           | To a point, sure. But your language and tools dictate a upper
           | bound on speed. You are never going to get a trading app
           | written in Python to be faster than your competitor's app
           | written in C.
        
           | [deleted]
        
       | Joel_Mckay wrote:
       | Professional C/C++ coders tend to recognize the history of cross-
       | platform compatibility issues, and tend to gravitate to a simpler
       | subset of the syntax to avoid compiler ambiguity (GNU gcc is
       | ugly, but greatly simplified the process).
       | 
       | Try anything fancy, and these languages can punish you for it
       | later on down the life-cycle. However, claiming it lacks
       | correctness shows a level of ignorance.
       | 
       | Best of luck, =)
        
       | andai wrote:
       | This needs a NSFW tag! Good lord...
        
       | philosopher1234 wrote:
       | >For example, a common thing programmers expect is that you can
       | test for signed integer overflow by checking whether the result
       | is less than one of the operands, as in this program:
       | #include <stdio.h>              int f(int x) {
       | if(x+100 < x)                 printf("overflow\n");
       | return x+100;         }
       | 
       | >Clang optimizes away the if statement. The justification is that
       | since signed integer overflow is undefined behavior, the compiler
       | can assume it never happens, so x+100 must never be less than x.
       | Ironically, this program would correctly detect overflow on both
       | ones'-complement and two's-complement machines if the compiler
       | would actually emit the check.
       | 
       | My god...
        
         | pdw wrote:
         | This is why I hate it when people describe C as "portable
         | assembler". The Usenet comp.lang.c FAQ was already thirty years
         | ago warning people not to write overflow checks like this.
        
         | coliveira wrote:
         | You cannot check for overflow like this, because you're causing
         | it. There are other ways to do this without adding first:
         | #include <limits.h>            int safe_add(int a, int b) {
         | if (a > 0 && b > INT_MAX - a) {             /* deal with
         | overflow... */         } else if (a < 0 && b < INT_MIN - a) {
         | /* deal underflow.. */         }         return a + b;       }
        
         | pclmulqdq wrote:
         | This is why people who put "C/C++" on their resume don't
         | usually know either language. In C++, integers are defined to
         | be two's complement now, so that check is acceptable.
        
           | rsc wrote:
           | Not true, as the post explains.
        
             | pclmulqdq wrote:
             | C++20 made the switch. See P1236. It appears C23 has
             | followed suit.
        
               | rsc wrote:
               | If P1236 (https://www.open-
               | std.org/jtc1/sc22/wg21/docs/papers/2018/p12...) is really
               | what it says ("alternative wording") then I don't believe
               | that's true. Certainly P0907R3 (https://www.open-
               | std.org/jtc1/sc22/wg21/docs/papers/2018/p09...) is clear:
               | 
               | > Status-quo If a signed operation would naturally
               | produce a value that is not within the range of the
               | result type, the behavior is undefined. The author had
               | hoped to make this well-defined as wrapping (the
               | operations produce the same value bits as for the
               | corresponding unsigned type), but WG21 had strong
               | resistance against this.
               | 
               | My understanding is that the 2s complement change in
               | C++20 ended up defining bitwise operations but not
               | arithmetic.
        
               | uecker wrote:
               | Two's complement does not mean signed overflow became
               | defined. So the check is still wrong. And no, I do not
               | think making it defined would lead to more correct
               | programs. You then simply have really difficult wrap-
               | around bugs instead of signed overflow which you can be
               | found more easily with UB sanitizer or turn into traps at
               | run-time.
        
               | e4m2 wrote:
               | It made the switch, sure, but overflow remains undefined.
        
           | woodruffw wrote:
           | That, or they know that C23 includes two's complement[1].
           | 
           | [1]: https://en.cppreference.com/w/c/23
        
             | pdw wrote:
             | Integer overflow is still undefined behavior in C23.
        
         | zer8k wrote:
         | I'm not convinced a "common" C program would ever do such a
         | thing. Even the "Effective C" book (one of the best books on C,
         | imo) discusses why this is wrong and why you should take
         | advantage of `limits.h` for bounds checking.
         | 
         | This is just bad programming. The compiler, like usual, is
         | correct because you're not in reality checking _anything_. You
         | 've made an obviously non-sensical statement that the
         | overflowed value will be less than the value. Compiler
         | optimizes it away. You can argue the semantics of UB here but
         | this particular UB is borderline a solved problem in any
         | practical sense.
         | 
         | To be fair, a static analyzer should be able to catch this.
        
           | _dain_ wrote:
           | >You've made an obviously non-sensical statement--
           | 
           | Therefore it should _fail to compile_.
           | 
           | You can even spin it as a performance enhancement: the whole
           | program can be optimized away!
        
             | layer8 wrote:
             | The problem is, that's not how the logical inference in a
             | compiler/optimizer works. It's very difficult to translate
             | such an optimization back to "this statement has been
             | optimized away", in the general case.
        
               | _dain_ wrote:
               | If it's so difficult to figure out if the consequences of
               | their optimization game are sensible or not, then they
               | shouldn't play it in the first place.
               | 
               | Who actually wants it to behave this way, other than
               | compiler engineers competing on microbenchmarks? Who is
               | this language even _for_?
        
               | layer8 wrote:
               | It's a side effect of desirable optimizations for UB-free
               | code. You can't have both at the same time.
        
               | _dain_ wrote:
               | Desirable for whom? Are the beneficiaries of it going to
               | pay for the externalities they are generating, like
               | polluters should?
               | 
               | You can say "ordinary workaday programmers benefit from
               | speed improvements en passant", but they weren't really
               | given a choice in the matter, were they? When programmers
               | are given an explicit binary choice of "correct, but
               | slightly slower", and "wrong, but slightly faster", they
               | pick the former in practically all cases (or they should,
               | at any rate). But they can't make this choice; the
               | compiler and spec writers go behind their backs and
               | construct these inscrutable labyrinths, then blame
               | everyone else for getting lost in them.
        
         | lelanthran wrote:
         | I think what really annoys me is that this looks like actual
         | malice on the part of the standards writers, and less severe
         | malice on the part of the compiler authors.
         | 
         | I know I should attribute it to stupidity instead but ...
         | 
         | The standards writers could have made all UB implementation
         | defined, but they didn't. The compiler authors could have made
         | all Uab implementation defined, and _they_ didn 't.
         | 
         | Take uninitialised memory as an example, or integer overflow:
         | 
         | The standard could have said "will have a result as documented
         | by the implementation". They didn't.
         | 
         | The implementation can choose a specific behaviour such as
         | "overflow results depend on the underlying representation" or
         | "uninitialised values will evaluate to an unpredictable
         | result."
         | 
         | But nooooo... The standard keeps adding more instances of UB in
         | each revision, and the compiler authors refuse to take a stand
         | on what overflow or uninitialised values should result in.
         | 
         | Changing the wording so that all UB is now implementation
         | defined does not affect blegacy code at all, except by forcing
         | the implementation to document the behaviour and preventing a
         | compiler from simply optimising out code.
        
           | layer8 wrote:
           | > The standards writers could have made all UB implementation
           | defined, but they didn't.
           | 
           | This is not possible without performance impact, because, in
           | the general case, whether a program constitutes UB depends on
           | data and/or previous program flow (halting problem), and thus
           | would require additional runtime checks even for code that
           | happens to never run into UB.
        
           | torstenvl wrote:
           | The standard does make all definable UB implementation
           | defined. The compiler writers intentionally misread the
           | standard to allow these kinds of optimizations.
           | 
           | C17 Draft, SS 3.4.3: "Possible undefined behavior ranges
           | from...
           | 
           | . . . ignoring the situation completely with unpredictable
           | results, to . . .
           | 
           | . . . behaving during translation or program execution in a
           | documented manner characteristic of the environment (with or
           | without the issuance of a diagnostic message) . . .
           | 
           | . . . to terminating a translation or execution (with the
           | issuance of a diagnostic message)."
           | 
           | Compiler writers like to language-lawyer this in two ways.
           | First, they interpret "possible" to mean that this is a non-
           | exhaustive list, but that isn't how interpretation of such a
           | document typically works. Expressio unius est exclusio
           | alterius -- the express inclusion of one is the exclusion of
           | others -- means that any list is exhaustive unless explicitly
           | indicated otherwise. In other words, the reality is that,
           | according to the standard, these are the only possible
           | options.
           | 
           | Second, they interpret "ignoring the situation completely" to
           | mean "actively seeking out the situation and deleting whole
           | sections of code based on that." This is quite self-evidently
           | a dishonest interpretation.
        
             | cesarb wrote:
             | The optimization opportunities do indeed come from the
             | first option, "ignoring the situation completely with
             | unpredictable results". That is: the compiler assumes that
             | the undefined behavior will not happen, and optimizes based
             | on it. They do not "actively seek out the situation", it's
             | in fact the opposite, they assume that the situation simply
             | won't happen. And "deleting whole sections of code" is just
             | the normal "unreachable code" optimization: code which
             | cannot be reached on any feasible execution path will be
             | deleted.
        
               | [deleted]
        
               | torstenvl wrote:
               | That is an obvious misreading.
               | 
               | Assume you have an unsigned char array r[] declared with
               | 100 elements and the compiler encounters the following
               | line of code:
               | 
               | r[200] = 0xff;
               | 
               | "ignoring the situation completely with unpredictable
               | results" means translating that to the appropriate object
               | code without bounds checking and no guarantees are made
               | about whether that's a segfault or what your environment
               | does.
               | 
               | "assum[ing] that the situation simply won't happen" means
               | eliding the code entirely.
               | 
               | The first is required by the standard. The second is
               | prohibited by the standard.
               | 
               | There is no section of any version of the standard that
               | permits compilers to assume that anything which might be
               | UB or predicated on UB is dead code.
               | 
               | Compilers _must_ either ignore the fact that it 's UB, or
               | behave in a documented implementation-defined manner, or
               | stop translation.
        
             | lelanthran wrote:
             | While you have convinced me that the compiler authors are
             | more culpable than the standards committee, I still feel
             | that they are ultimately responsible - the buck stops with
             | them.
             | 
             | They're free to, and easily able to, add the word
             | "exhaustively" when listing possible behaviours, but they
             | don't. They can even do away with it entirely and replace
             | it with implementation-defined, which makes compilers non-
             | conformant if they optimise out code.
        
             | pdw wrote:
             | You're quoting from a footnote. The footnotes are not part
             | of the normative text, so how you choose to interpret it is
             | irrelevant.
             | 
             | The actual definition of undefined behavior given by SS
             | 3.4.3 is: "behavior, upon use of a nonportable or erroneous
             | program construct or of erroneous data, for which this
             | International Standard imposes no requirements."
             | 
             | Since no requirement is imposed, compilers can do as they
             | want.
        
               | torstenvl wrote:
               | > _You 're quoting from a footnote. The footnotes are not
               | part of the normative text_
               | 
               | I am not quoting from a footnote.
               | 
               | Footnote 1 pertains to SS 1.
               | 
               | Footnote 2 pertains to SS 3.19.5.
               | 
               | There are no footnotes pertaining to SS 3.4.3.
               | 
               | And do you have a citation for the proposition that
               | footnotes or notes of any other kind are not normative?
        
               | pdw wrote:
               | Apologies, it's a note, not a footnote. But notes in ISO
               | standards aren't normative either.
               | 
               | This is a general principle of ISO standards, see https:/
               | /share.ansi.org/Shared%20Documents/Standards%20Activi...:
               | 
               | > 6.5 Other informative elements
               | 
               | > 6.5.1 Notes and examples integrated in the text
               | 
               | > Notes and examples integrated in the text of a document
               | shall only be used for giving additional information
               | intended to assist the understanding or use of the
               | document. They shall not contain requirements ("shall";
               | see 3.3.1 and Table H.1) or any information considered
               | indispensable for the use of the document, e.g.
               | instructions (imperative; see Table H.1), recommendations
               | ("should"; see 3.3.2 and Table H.2) or permission ("may";
               | see Table H.3). Notes may be written as a statement of
               | fact.
        
           | kelnos wrote:
           | I think it's a bit out of line to call this malice.
           | 
           | The C standard was written when people wanted a language that
           | was easy to read and write, but could have similar
           | performance to hand-written assembly. Performance, as the
           | article title points out, was a bigger priority than
           | correctness or eliminating foot guns.
           | 
           | Most of us here weren't around when many/most programs were
           | written in assembly, and when machines were very constrained
           | in how much memory they had and how many CPU cycles they
           | could spare. Nowadays, performance concerns usually come
           | behind speed and correctness of development. So it's hard to
           | truly feel what trade offs the original designers of C (and
           | the original C89 standards committee) had to make.
           | 
           | I'm not convinced implementation-defined behavior is really
           | any better, though. Packagers and end-users should not have
           | to ensure that the code their compiling is "compatible" with
           | the compiler or hardware architecture they want to use.
           | Developers shouldn't have to put a "best compiled with FooC
           | compiler" or "only known to work on x86-64". That would not
           | be an improvement.
           | 
           | I do agree that, these days, there's no excuse for writing
           | language specifications that include undefined behavior. But
           | C has a lot of baggage.
        
             | pjmlp wrote:
             | That is usually a myth.
             | 
             | From the people that were there at the time.
             | 
             | "Oh, it was quite a while ago. I kind of stopped when C
             | came out. That was a big blow. We were making so much good
             | progress on optimizations and transformations. We were
             | getting rid of just one nice problem after another. When C
             | came out, at one of the SIGPLAN compiler conferences, there
             | was a debate between Steve Johnson from Bell Labs, who was
             | supporting C, and one of our people, Bill Harrison, who was
             | working on a project that I had at that time supporting
             | automatic optimization...The nubbin of the debate was
             | Steve's defense of not having to build optimizers anymore
             | because the programmer would take care of it. That it was
             | really a programmer's issue.... Seibel: Do you think C is a
             | reasonable language if they had restricted its use to
             | operating-system kernels? Allen: Oh, yeah. That would have
             | been fine. And, in fact, you need to have something like
             | that, something where experts can really fine-tune without
             | big bottlenecks because those are key problems to solve. By
             | 1960, we had a long list of amazing languages: Lisp, APL,
             | Fortran, COBOL, Algol 60. These are higher-level than C. We
             | have seriously regressed, since C developed. C has
             | destroyed our ability to advance the state of the art in
             | automatic optimization, automatic parallelization,
             | automatic mapping of a high-level language to the machine.
             | This is one of the reasons compilers are ... basically not
             | taught much anymore in the colleges and universities."
             | 
             | -- Fran Allen interview, Excerpted from: Peter Seibel.
             | Coders at Work: Reflections on the Craft of Programming
        
               | lelanthran wrote:
               | I fail to see to the point of your post in context of
               | this thread.
               | 
               | What, in the parent post, do you consider a myth?
        
               | pjmlp wrote:
               | C's original performance.
               | 
               | Optimization taking advantage of UB had to be introduced
               | during the 1990's to make it into a reality.
               | 
               | Until the widespread of 32 bit hardware, even junior
               | Assembly devs could relatively easy outperform C
               | compilers.
               | 
               | Which is what the interview is about, originally C lacked
               | the optimization tools, relying on developers, with tons
               | of inline Assembly.
        
               | lelanthran wrote:
               | > Which is what the interview is about, originally C
               | lacked the optimization tools, relying on developers,
               | with tons of inline Assembly.
               | 
               | I'm afraid that is not the conclusion I draw from the
               | snippet you posted.
               | 
               | It's very clear _to me_ that the HLL under question at
               | that time _prevented_ the programmer from performing the
               | low-level optimisations that the C programmer could
               | optionally do. The debate appeared to be be whether to
               | let the language exclusively optimise the code, or just
               | do no optimisation and let the programmer do it.
               | 
               | This is why the sentence ": Oh, yeah. That would have
               | been fine. And, in fact, you need to have something like
               | that, something where experts can really fine-tune
               | without big bottlenecks because those are key problems to
               | solve. " is in there - it's because the HLL languages
               | literally wouldn't let the programmer optimise at the
               | level that C would let the programmer optimise.
               | 
               | Honestly, if the proponent for the HLL language didn't
               | add in "Use the HLL, but not for OS and low-level code
               | where you really need to fine-tune", you'd have a point,
               | but they said it, and so you don't.
        
               | pjmlp wrote:
               | Right, that is why C code was polluted with inline
               | Assembly until optimizers started taking advantage of UB.
               | 
               | Inline Assembly isn't C.
        
             | lelanthran wrote:
             | > I think it's a bit out of line to call this malice.
             | 
             | I apologise, but note (warning: weasel words follow) I was
             | careful to say it _looks_ like malice to me, not that I
             | have any indication that it actually was malice.
             | 
             | > I'm not convinced implementation-defined behavior is
             | really any better, though. Packagers and end-users should
             | not have to ensure that the code their compiling is
             | "compatible" with the compiler or hardware architecture
             | they want to use.
             | 
             | I think it is better, purely because then the resulting
             | code can't simply be omitted when an integer may overflow,
             | the _code_ still has to be emitted by the compiler.
             | 
             | Right now all the worst bits of UB has to do with the
             | compiler optimising out code. With IB replacing UB, the
             | implementation will have to pick a behaviour and then stick
             | with it. Much safer.
        
           | nottorp wrote:
           | > less severe malice on the part of the compiler authors.
           | 
           | This is optimization run amok and I'd call it malice. If
           | C/C++ are designed to let me shoot myself in the foot, the
           | compiler should let me shoot myself in the foot instead of
           | erasing my code.
        
         | boryas wrote:
         | -fwrapv and it's all good :)
        
           | not2b wrote:
           | It really isn't optional as a C developer to turn on warnings
           | and get your code warning-free. This will eliminate the most
           | common unbounded behavior issues, like uninitialized
           | variables, though unfortunately not all of them.
        
         | gavinhoward wrote:
         | This is why I implemented two's-complement with unsigned types,
         | that don't have UB on overflow.
        
           | kragen wrote:
           | like x > 2147483547u in this case?
        
             | gavinhoward wrote:
             | Correct. It will still overflow, but it's not UB and won't
             | be optimized away.
        
               | kragen wrote:
               | by 'overflow' do you mean 'set the high bit'
        
               | gavinhoward wrote:
               | Correct.
               | 
               | I have routines that take the unsigned values and
               | interpret them as signed, two's-complement values.
               | 
               | So you would write this:                   val = temp1 +
               | temp2;         if (y_ssize_lt(val, temp1)) y_trap();
               | 
               | That `y_ssize_lt()` function computes whether `val` is
               | less than `temp1` as though they were signed,
               | two's-complement integers. But because they are actually
               | unsigned, the compiler cannot be malicious and delete
               | that code.
        
               | kragen wrote:
               | i see, thanks
               | 
               | maybe we should develop a nonmalicious compiler
        
         | tempodox wrote:
         | This has to be the worst part of UB: That the compiler can
         | assume it never happens. This way UB can affect your program
         | even if the code that would exhibit the behavior is never
         | executed.
        
         | AlotOfReading wrote:
         | What the article misses is that the code _wouldn 't_ work on
         | all hardware. Take MIPS for example, where the signed overflow
         | would generate a hardware exception that the OS might have
         | implemented to do anything from nothing to killing the
         | generating process.
         | 
         | C was never standardized solely on the basis of what's most
         | performant. The vast majority of explicit UB is there because
         | someone knew of a system or situation where that assumption
         | wasn't true.
        
           | lelanthran wrote:
           | > C was never standardized solely on the basis of what's most
           | performant. The vast majority of explicit UB is there because
           | someone knew of a system or situation where that assumption
           | wasn't true.
           | 
           | So? They could have called it implementation defined. The
           | reasoning you present was broken then, as it is broken now.
           | 
           | Your MIPS example displays no reason for UB to exist.
        
             | AlotOfReading wrote:
             | Implementation defined means _it has a defined behavior_ on
             | every implementation. In the MIPS case defining that
             | behavior would force the compiler to generate strictly
             | signed instructions for signed values _and_ define how the
             | runtime platform handles these interrupts, rather than
             | leaving the compiler free to generate  "whatever works".
             | 
             | Look, a lot of UB is frankly stupid and shouldn't be in a
             | modern language spec, including signed overflow. I'm not
             | defending that, only giving an example where you have to
             | break compatibility to eliminate it.
        
               | lelanthran wrote:
               | > In the MIPS case defining that behavior would force the
               | compiler to generate strictly signed instructions for
               | signed values and define how the runtime platform handles
               | these interrupts,
               | 
               | I respectfully disagree: in this particular scenario,
               | it's enough for the compiler vendor to write "generates
               | an interrupt on overflow" with no indication of what the
               | handling should be, and still be well within
               | _implementation-defined_ behaviour.
               | 
               | After all, the standard specifies what `raise()` does,
               | and what `signal()` does, but doesn't specify how the
               | runtime is going to handle `raise(signum)` when the
               | program has not yet called `signal(signum, fptr)`.
               | 
               | This scenario you presented displays _exactly_ why UB
               | should be replaced with IB in the standard. I 've yet to
               | see one good reason (including performance reasons) for
               | why the dereferencing of a NULL pointer (for example)
               | cannot be documented for the implementation.
               | 
               | With UB, we have real-world examples of a NULL pointer
               | dereference causing compilers to omit code resulting in a
               | program that continued to run but with security
               | implications. If changed to IB, the compiler would be
               | forced to emit that code and let the dereference crash
               | the program (much better).
        
       | justincredible wrote:
       | [dead]
        
       | hot_gril wrote:
       | This is a cool summary of UB, but speaking to the title... Well
       | yeah. Did anyone suggest otherwise?
        
         | SenAnder wrote:
         | A periodic reminder is good.
        
         | uecker wrote:
         | It should be "C and C++ Implementations" because nothing in the
         | standard requires UB to be exploited for optimization instead
         | of adding run-time checks.
        
           | hot_gril wrote:
           | The standard also doesn't require UB to have run-time checks,
           | which is probably for the goal of performance. There was an
           | implementation in mind when it was designed.
        
             | uecker wrote:
             | There were C implementations with bounds checking or
             | implementations that trap on use of an invalid pointer or
             | on read of uninitialized variables etc. A large part of UB
             | was introduced for such reasons and not for performance.
             | The standard was (and still is) about allowing a wide range
             | of implementations.
        
         | philosopher1234 wrote:
         | To me this article is interesting because
         | 
         | * it is thorough, detailed and very thoughtful
         | 
         | * rsc's essays usually end up with a major Go language change,
         | so there's a good chance this article is the seed of some
         | change to Go and undefined behavior, or correctness or
         | performance.
         | 
         | Even if it's not a major upcoming change (I bet it is, tho) rsc
         | is an extremely insightful dev.
        
           | hot_gril wrote:
           | Yeah, I agree. I was editing my comment to praise the
           | article's body as you were replying.
        
           | rsc wrote:
           | There is no major upcoming Go change related to this post.
           | This is just a much longer version of
           | https://research.swtch.com/plmm#ub that I drafted years ago
           | and finally cleaned up enough to post.
        
             | philosopher1234 wrote:
             | It's fine, I'm not sad or anything
        
       | coliveira wrote:
       | The issue of UB has everything to do with how compilers implement
       | it. If people are having problems, they should complain to
       | compiler writers. They always have the option of creating slower
       | code that checks for obvious problems like initialized variables.
       | However, if a company/project writes a compiler that is a little
       | slower than the competitor, people with almost always complain
       | that it is a bad compiler. So the result is what you have
       | nowadays: they're always looking for every small opportunity to
       | generate faster code at the expense of safety.
        
         | weinzierl wrote:
         | _" [..] this International Standard imposes no requirements on
         | the behavior of programs that contain undefined behavior. "_
         | 
         | What you describe fits for "implementation defined behavior".
         | If you want to write code that works with different compilers,
         | a single compiler with every UB defined, gains you nothing. If
         | you don't need that flexibility, you can just use a language
         | without UB in the first place.
        
           | dale_glass wrote:
           | You do. Take the EraseAll example
           | 
           | Ideally it's a compile time error. Less ideally it jumps to a
           | NULL pointer and immediately crashes.
           | 
           | Either means the programmer must fix the code, which also
           | stops being a problem on other compilers.
        
             | weinzierl wrote:
             | _" Ideally it's a compile time error."_
             | 
             | I agree with that, but the standard does not agree with
             | both of us. My point is that choosing C or C++ makes sense
             | if you see an advantage in programming against an
             | ubiquitous and almost universal standard. If you have the
             | freedom to implement against a particular compiler
             | implementation there is no good argument to not also taking
             | the freedom to choose a different language altogether.
             | 
             | I know in the real world this is not so easy. I worked in
             | automotive with their certified C compilers long enough and
             | I wouldn't have had the choice to select another language.
             | Doesn't mean everyone wouldn't have been better off with a
             | language without UB in the first place and I think we are
             | getting there.
        
               | dale_glass wrote:
               | Nonsense. The standard literally doesn't bind us in any
               | way whatsoever. UB means there are no rules, anything
               | goes. If the compiler is free to make demons fly out of
               | my nose, it's equally free to produce a nice error
               | message to the effect of "Don't do that".
               | 
               | I'd much prefer if the standards people started defining
               | every possible instance of UB as a fatal error or as an
               | implementation defined behavior, but that's not strictly
               | required.
        
               | layer8 wrote:
               | Some instances of UB are data-dependent, and some would
               | require solving the halting problem to statically
               | distinguish them from non-UB. What you propose is
               | therefore not generally possible at compile time, and at
               | runtime only with considerable performance impact.
        
               | weinzierl wrote:
               | Thanks for pointing that out. Detecting UB is hard and
               | sometimes impossible in C and C++. If you design your
               | language accordingly you can avoid that. At least I think
               | that is how Rust deals with it, but I'm happy to be
               | convinced otherwise.
        
               | coliveira wrote:
               | If some form of UB is impossible to detect, then the
               | compiler cannot do anything about it either, making the
               | whole debate useless. Any action taken by a compiler
               | relating to UB must be for some cause that is detectable
               | at compilation time.
        
               | layer8 wrote:
               | That's not quite true, the compiler could add code to
               | detect all UB at runtime, and then abort or whatever.
               | It's just that this would also pessimize UB-free code,
               | and most compilers opt to not do that, at least in
               | release mode.
        
               | weinzierl wrote:
               | That is not true and very important to understand. The
               | optimizations a compiler does by eliding UB stuff are
               | often very effective and I think no one in the thread has
               | questioned that. It is a fallacy, though, to think that
               | these optimizations could be replaced by diagnostics.
               | That would be hard in most cases and sometimes
               | impossible.
        
               | dale_glass wrote:
               | So fix it to the extent possible. Eg, this:
               | https://t.co/Z1Ib2fIyu6
               | 
               | Is easily fixable.
               | 
               | A. static Function Do; -- This is a syntax error,
               | initialization is mandatory.
               | 
               | B. It's initialized to nullptr, and compiled to jmp 0x0
        
               | layer8 wrote:
               | Yes, you can fix it in some cases, but the real errors
               | usually happen in complex code where it is much harder or
               | impossible to detect at compile time. The examples in TFA
               | are very simplified for exposition purposes, they are not
               | representative of ease-of-detection.
        
               | dale_glass wrote:
               | I don't see how this particular case could ever be hard
               | to deal with.
               | 
               | Using uninitialized memory is UB, so initialize every
               | byte of memory allocated to NULL. Dereference NULL
               | pointers as-is. Done.
               | 
               | I'd be quite happy with a best effort approach. If you
               | can't be perfect in every case, at least handle the low
               | handling fruit.
        
               | layer8 wrote:
               | This approach would also impact UB-free code that
               | initializes the memory later. C compilers don't want to
               | pessimize such code. The difficulty lies in identifying
               | only code with actual UB.
        
               | dale_glass wrote:
               | That's perfectly fine with me. If the compiler can prove
               | the code is initialized twice, then it's free to
               | deduplicate it. Otherwise I'll happily eat the cost and
               | preserve more of my sanity when debugging.
        
               | layer8 wrote:
               | _You_ 'll happily eat the cost, but the target audience
               | of C compilers traditionally doesn't. That's the point
               | made by TFA. C prioritizes performance over safety.
        
               | weinzierl wrote:
               | I agree with you more than you think.
               | 
               |  _" if the standards people started defining every
               | possible instance of UB as a fatal error or as an
               | implementation defined behavior,"_
               | 
               | We can wish for that, but it does not and never will.
               | 
               | Writing C or C++, first and foremost, means writing code
               | against the standard and we have to deal with what the
               | standard says.
               | 
               | Of course we are free to give up on the standard and
               | target a particular implementation of C or C++ in a
               | particular compiler. If you also like to to call that
               | writing C or C++ I would not argue with you. It is still
               | a very different thing because you give up the primary
               | advantage of C and C++ that you can compile basically
               | everywhere. My point now is that if you give that up it
               | makes no sense to stick with C or C++ in the first place
               | and in this day and age.
        
               | dale_glass wrote:
               | > My point now is that if you give that up it makes no
               | sense to stick with C or C++ in the first place and in
               | this day and age.
               | 
               | You wouldn't be giving anything up. The standard defining
               | something as UB means you absolutely shouldn't be doing
               | that. So a compiler doing something defined in case of UB
               | can't harm you in any way, and doesn't deviate from the
               | standard, because the standard prescribes no rules in the
               | case you're invoking.
        
               | weinzierl wrote:
               | With giving up I meant giving up portability by targeting
               | a particular implementation of C or C++ in a particular
               | compiler. The standard is what it is and discussing a
               | hypothetical standard is moot.
        
               | dale_glass wrote:
               | You don't give up any portability.
               | 
               | If the standard says that say, dereferencing a NULL
               | pointer is UB it means you're not supposed to do that
               | ever, on any OS or compiler. A compiler can choose to do
               | something sensible like producing a fatal error message
               | without any downside, since per standard that's not ever
               | supposed to happen anyway.
        
               | weinzierl wrote:
               | To not giving up portability _all_ compilers would have
               | to do what you propose. They won 't as long as the
               | standard doesn't mandate it. So we are back at the point
               | where we agree that the standard contains some
               | unfortunate things.
               | 
               | Now, standard conformant compilers do exactly what you
               | propose, they are just not C or C++ compilers and the
               | standard is not the C or C++ standard.
        
               | dale_glass wrote:
               | > To not giving up portability all compilers would have
               | to do what you propose.
               | 
               | Not in a lot of cases. Eg, let's suppose that GCC defines
               | a NULL pointer dereference to act exactly like a normal
               | one. That is, the compiler dereferences the pointer, and
               | whatever the CPU/OS says is going to happen when you read
               | from 0x0, happens.
               | 
               | Meanwhile, Clang continues with behaviors that can say
               | lead to a function being entirely replaced with "return
               | true".
               | 
               | There's no problem whatsoever with this. You're still not
               | supposed to dereference null pointers. It's just that on
               | GCC in particular you get a predictable error. And being
               | an error you can't really rely on it -- your program
               | still crashes and crashes are still undesirable, and
               | therefore you will fix whatever leads to that outcome.
               | And on Clang maybe you don't crash but the program
               | doesn't do what you expect it to, which is still a bug to
               | be fixed. You have a bug in both versions which manifests
               | in different ways (but that is fine, because UB says
               | anything goes so there's no requirement whatsoever for
               | both compilers to have the program break identically),
               | but it's the same bug that needs the same fix.
               | 
               | After the bug fix, your GCC compiled version doesn't
               | crash and runs correctly, and your Clang compiled version
               | doesn't crash and runs correctly. The behavior in the end
               | is identical.
        
               | coliveira wrote:
               | > Writing C or C++, first and foremost, means writing
               | code against the standard
               | 
               | This was never true and never will be. There are tons of
               | C and C++ code that cannot be compiled in more than a few
               | compilers. The standard is the minimum denominator, but
               | all compilers have something beyond the standard that is
               | used in practice. Just check the Linux code, for example.
               | 
               | > if you give that up it makes no sense to stick with C
               | or C++
               | 
               | No, the whole point of UB is that every compiler can do
               | what it wants in that situation. C/C++ actively embrace
               | differences between compilers, while trying to
               | standardize the core meaning of the language.
        
               | weinzierl wrote:
               | Linux is an excellent example. There has been a fork that
               | made it compile with the Intel compiler and if I remember
               | correctly it was moderately faster. Of course it went
               | nowhere.
               | 
               | Writing standard conformant code is hard and you do it if
               | you have a good reason to do it. A lot of software has.
               | 
               | The Linux kernal hasn't and that is fine. If your project
               | also doesn't have that constraint, good for you. 30 years
               | ago you would still choose C and a particular compiler
               | and you could get things done. Nowadays, why bother? You
               | could choose a language that has no UB, compiles on any
               | hardware that is reasonably common and is still
               | performant.
        
         | Thiez wrote:
         | Why complain to the compiler writers? As you say, people want
         | the fastest compilers for their language, so compilers will
         | prioritize that over other concerns. Users may rant and
         | complain, but they won't use a slower compiler.
         | 
         | If you really want less UB, switch to a different language _or_
         | change the language! Complain to the standards committee, have
         | them define behavior that is currently undefined, or impose
         | restrictions on allowable behaviors. Compilers are always going
         | to optimize to the extent that the language standard allows, so
         | change the standard.
        
         | pyrolistical wrote:
         | > However, if a company/project writes a compiler that is a
         | little slower than the competitor, people with almost always
         | complain that it is a bad compiler.
         | 
         | That is bullshit. There are plenty of project that would gladly
         | trade performance for more correctness. I would go as far to
         | say most projects would make that choice if articles like this
         | get mindshare.
         | 
         | "It's mostly as fast as clang but errors upon UB" is an easy
         | sell
        
       | zwieback wrote:
       | Clearly we can have compilers and static analyzers that can catch
       | a lot of what's UB in the standards.
       | 
       | To me the only real question is: what should the default be for
       | your compiler? I don't want to be flooded with warnings when I'm
       | working on a small file of handcrafted near-assembly code but I'm
       | willing to change my compiler/tool options away from some more
       | conservative, non-standard default settings.
        
         | eatonphil wrote:
         | It isn't super obvious as someone interested in C or C++ what
         | compiler flags and static analyzers I should turn on.
         | 
         | I mean (and I'm asking because I'd genuinely like to bookmark
         | someplace) is there a group that keeps an up-to-date list of
         | aggressive flags and static analyzers to use?
        
           | yaantc wrote:
           | For static analysis I use CodeChecker, it's a wrapper on top
           | of the Clang static analyzer and Clang tidy (linter). Now
           | also supports cppcheck, but I disabled it (too many false
           | alarms). It's free and open source, and I find it useful.
           | Make sure you use it with a version of LLVM/Clang with
           | support of Microsoft z3 enabled (it's the case in Debian
           | stable, so should be OK in most distros).
           | 
           | For the flags I would start with "-Wall -Werror", then maybe
           | disable some warnings based on the code base / use.
           | 
           | All this assuming a GCC/Clang friendly code base.
        
           | soulbadguy wrote:
           | -Wall -Werror (in cland and in think on GCC) is a good way to
           | start. In general, clang is great at warning at UB. Also,
           | address-san is a great tool
        
           | dbremner wrote:
           | I'm not sure about static analyzers, but here are the clang
           | warnings I use for my personal C++20 projects. You will get
           | an enormous number of warnings from typical C++ code. I fix
           | all of them, but doubt it would make sense to do so in a
           | commercial environment.
           | 
           | -Weverything - this enables _every_ clang warning
           | 
           | -Wno-c++98-compat - warns about using newer C++ features
           | 
           | -Wno-c++98-compat-pedantic - warns about using newer C++
           | features
           | 
           | -Wno-padded - warns about alignment padding. I optimize
           | struct layout, so this warning only reports on cases I
           | couldn't resolve.
           | 
           | -Wno-poison-system-directories - I'm not cross-compiling
           | anything.
           | 
           | -Wno-pre-c++20-compat-pedantic - warns about using newer C++
           | features
        
           | zwieback wrote:
           | Not that I know of, probably something you'll have to dig
           | into based on your use case. I've used Coverity for static
           | analysis, which is great but pricey. We have a corporate
           | license so no-brainer for me.
        
       | [deleted]
        
       | k4st wrote:
       | > It would certainly not hurt performance to emit a compiler
       | warning about deleting the if statement testing for signed
       | overflow, or about optimizing away the possible null pointer
       | dereference in Do().
       | 
       | I think that the nature of Clang and LLVM makes this kind of
       | reporting non-trivial / non-obvious. Deletions of this form may
       | manifest as a result of multiple independent optimizations being
       | brought to bear, culminating in noticing a trivially always-
       | taken, or never-taken branch. Thus, deletion is the just the last
       | step of a process, and at that step, the original intent of the
       | code (e.g. an overflow check) has been lost to time.
       | 
       | Attempts to recover that original intent are likely to fail. LLVM
       | instructions may have source location information, but these are
       | just file:line:column triples, not pointers to AST nodes, and so
       | they lack actionability: you can't reliably go from a source
       | location to an AST node. In general though, LLVM's modular
       | architecture means that you can't assume the presence of an AST
       | in the first place, as the optimization may be executed
       | independent of Clang, e.g. via the `opt` command-line tool. This
       | further implies that the pass may operate on a separate machine
       | than the original LLVM IR was produced, meaning you can't trust
       | that a referenced source file even exists. There's also the DWARF
       | metadata, but that's just another minefield.
       | 
       | Perhaps there's a middle ground with `switch`, `select`, or `br`
       | instructions in LLVM operating on `undef` values, though.
        
       | pogopop77 wrote:
       | C and C++ are most often used when performance is the highest
       | priority. Undefined behavior is basically the standards committee
       | allowing the compiler developers maximum flexibility to optimize
       | for performance over error checking/handling/reporting. The
       | penalty is that errors can become harder to detect.
       | 
       | It appears the author is a Go advocate. I assume they are valuing
       | clearly defined error checking/handling/reporting (the authors
       | definition of correctness) over performance. If that's what you
       | are looking for, consider Go.
        
         | kelnos wrote:
         | I think this allowance is a mistake.
         | 
         | I suspect that there is some huge number of developer hours
         | that have been wasted, and huge amount of money wasted, on
         | cleaning up after security breaches and finding and fixing
         | security issues. I suspect that those numbers dwarf any losses
         | that might have arisen due to reduced developer productivity or
         | reduced performance when using a (hypothetical) C-like language
         | that doesn't allow the compiler to do these sorts of things.
        
           | PaulDavisThe1st wrote:
           | The allowance wasn't "tradeoff performance time with
           | developer time".
           | 
           | The point of undefined behavior is "if we specify this, we
           | cause problems for some of the compile targets for this
           | language".
        
             | patmorgan23 wrote:
             | Yes, but then complier vendors started abusing UB to
             | increase performance and while silently decreasing
             | safety/correctness. If the compiler creates a security bug
             | by optimizing away a bounds check the programmer explicitly
             | put there, that's a problem.
             | 
             | https://thephd.dev/c-undefined-behavior-and-the-
             | sledgehammer...
        
           | jerf wrote:
           | "a (hypothetical) C-like language that doesn't allow the
           | compiler to do these sorts of things."
           | 
           | It's not very hypothetical in 2023. There are plenty of
           | languages whose compilers don't do this sort of thing and
           | attain C-like performance. There isn't necessarily a single
           | language that exactly and precisely replaces C right now, but
           | for any given task where you would reach for C or C++ there's
           | a viable choice, and one likely to be better in significant
           | ways.
           | 
           | I also feel like this is missed by some people who defend
           | this or that particularly treatment of a particular undefined
           | behavior. Yeah, sure, I concede that given the history of
           | where C is and how it got there and in your particular case
           | it may make sense. But the thing is, my entire point is _we
           | shouldn 't be here in the first place_. I don't care about
           | why your way of tapping a cactus for water is justifiable
           | after all if you consider the full desert context you're
           | living in, I moved out of the desert a long time ago. Stop
           | using C. To a perhaps lesser but still real extent, stop
           | using C++. Whenever you can. They're not the best option for
           | very many tasks anymore, and if we discount "well my codebase
           | is already in that language and in the real world I need to
           | take switching costs into account" they may already not be
           | the best option for anything anymore.
        
             | spookie wrote:
             | I'm sorry, but the world doesn't really align with these
             | ideas, sometimes that is. It's understandable, but at the
             | same time it really isn't.
        
               | jerf wrote:
               | I assume you're referring to "my codebase is already in
               | C/C++"? Which I did acknowledge?
               | 
               | Because otherwise, what the world is increasingly not
               | aligning with is using C when you shouldn't be. Security
               | isn't getting any less important and C isn't getting any
               | better at it.
        
             | dahfizz wrote:
             | I'll stop using C when there is a faster alternative. When
             | nanoseconds count, there is no competition.
        
               | jerf wrote:
               | You don't need one faster. You need one _as_ fast. These
               | options generally exist. Rust seems to have crept its way
               | right up to  "fast as C"; it isn't really a distinct
               | event, but https://benchmarksgame-
               | team.pages.debian.net/benchmarksgame/... (it tends to
               | better on the lower ones so scroll a bit). There are some
               | other more exotic options.
               | 
               | C isn't the undisputed speed king any more. It hasn't
               | necessarily been "roundly trounced", there isn't enough
               | slack in its performance for that most likely, but it is
               | not the undisputed speed king. It turns out the corners
               | it cuts are just corners being cut; they aren't actually
               | necessary for performance, and in some cases they can
               | actually inhibit performance. See the well-known aliasing
               | issues with C optimizations for an example. In general I
               | expect Rust performance advantages to actually get
               | _larger_ as the programs scale up in size and Rust
               | affords a style that involves less copying just to be
               | safe; benchmarks may actually undersell Rust 's
               | advantages in real code on that front. I actually
               | wouldn't be surprised that Rust is in practice a
               | straight-up faster language than C on non-trivial code
               | bases being developed in normal ways; it is unfortunately
               | a very hard assertion to test because pretty much by
               | definition I'm talking about things much larger than a
               | "benchmark".
        
             | optymizer wrote:
             | This reasoning is why software keeps getting slower and
             | more bloated, build times increase, and latency goes up
             | despite having orders of magnitude more compute power.
        
               | jerf wrote:
               | If whatever language you're thinking of does that, it
               | isn't one of the ones I'm talking about. I sure as heck
               | aren't talking about Python here. Think Rust, D, Nim, in
               | general the things floating along at the top of the
               | benchmarks (that's not a complete list either).
        
         | jeremyloy_wt wrote:
         | > It appears the author is a Go advocate
         | 
         | A bit of an understatement. The author is the current Golang
         | project lead and a member since it's inception
        
         | pjmlp wrote:
         | Nowadays, in 1980's....
         | 
         | "Oh, it was quite a while ago. I kind of stopped when C came
         | out. That was a big blow. We were making so much good progress
         | on optimizations and transformations. We were getting rid of
         | just one nice problem after another. When C came out, at one of
         | the SIGPLAN compiler conferences, there was a debate between
         | Steve Johnson from Bell Labs, who was supporting C, and one of
         | our people, Bill Harrison, who was working on a project that I
         | had at that time supporting automatic optimization...The nubbin
         | of the debate was Steve's defense of not having to build
         | optimizers anymore because the programmer would take care of
         | it. That it was really a programmer's issue.... Seibel: Do you
         | think C is a reasonable language if they had restricted its use
         | to operating-system kernels? Allen: Oh, yeah. That would have
         | been fine. And, in fact, you need to have something like that,
         | something where experts can really fine-tune without big
         | bottlenecks because those are key problems to solve. By 1960,
         | we had a long list of amazing languages: Lisp, APL, Fortran,
         | COBOL, Algol 60. These are higher-level than C. We have
         | seriously regressed, since C developed. C has destroyed our
         | ability to advance the state of the art in automatic
         | optimization, automatic parallelization, automatic mapping of a
         | high-level language to the machine. This is one of the reasons
         | compilers are ... basically not taught much anymore in the
         | colleges and universities."
         | 
         | -- Fran Allen interview, Excerpted from: Peter Seibel. Coders
         | at Work: Reflections on the Craft of Programming
        
           | marcosdumay wrote:
           | Hum... We have to move further than this citation. The 1980s
           | C was much more secure than our current one.
           | 
           | The undefined behavior paradoxes were only added by the 90s,
           | when optimizing compilers became a logic inference engine,
           | feed with the unquestionable truth that the developer never
           | exercises UB.
           | 
           | Just because it was a sane language for kernel development at
           | the 1980s, it doesn't mean it is one now.
        
             | pjmlp wrote:
             | Yeah, because the only way to achieve performance in 1980's
             | C on 16 bit platforms was to litter it with inline
             | Assembly, thus UB based optimisation was born to win all
             | those SPEC benchmarks in computer magazines.
        
               | marcosdumay wrote:
               | Funny thing in that the same thing stopping people to
               | write those crazy1 optimizers at the 1980s was exactly
               | the lack of capacity of the computers to run them.
               | 
               | What means that they appeared exactly at the time the
               | need for them became niche. And yet everybody adopted
               | them due to the strong voodoo bias we all have at
               | computer-related tasks.
               | 
               | 1 - They _are_ crazy. They believe the code has no UB
               | even though they can prove it has.
        
           | titzer wrote:
           | A great quote. She's absolutely right. C has absolutely
           | polluted people's understanding of what compiler
           | optimizations _should_ be. Compilers should make a program go
           | faster, invisibly. C makes optimization _everyone 's_ problem
           | because the language is so absolutely terrible at defining
           | its own semantics and catching program errors.
        
         | btilly wrote:
         | Given who invented it, Go can be thought of as, "What C might
         | have been if we could have done it."
         | 
         | Go really is in many ways more similar to early C in spirit
         | than modern C is.
        
           | akshayshah wrote:
           | Expanding this for those not familiar with Go's history: Ken
           | Thompson, formerly of Bell Labs and co-creator of B and Unix,
           | was deeply involved in Go's early days. Rob Pike, also ex-
           | Bell Labs, was also one of Go's principal designers.
           | 
           | I can't find a source now, but I believe Rob described Russ
           | Cox as "the only programmer I've met as gifted as Ken." High
           | praise indeed.
        
           | anthk wrote:
           | Go it's modelled after plan9'C [1-9]c = go cross compiling,
           | and Limbo so it has a lot of sense. Both come from the same
           | people after all. Unix->Unix8 -> Plan9 -> Go.
        
         | mxmlnkn wrote:
         | The undefined behavior I struggle with keeps me from better
         | performance though. I have something like [(uint32_t value) >>
         | (32 - nbits)] & (lowest nbits set). For the case of nbits=0, I
         | would expect it to always return 0, even if the right shift of
         | a 32-bit value by 32 bits is undefined behavior, then bit-wise
         | and with 0 should make it always result in 0. But I cannot
         | leave it that way because the compiler thinks that undefined
         | behavior may not happen and might optimize out everything.
        
           | titzer wrote:
           | Exactly. The irony in all of this is that C is _not_ a
           | portable assembler. It 'd be better if it _were_ [1]!
           | 
           | If you want the _exact_ semantics of a hardware instruction,
           | you cannot get it, because the compiler reasons with C 's
           | _abstract_ machine that assumes your program doesn 't have
           | undefined behavior, like signed wraparound, when in some
           | situations you in fact _do_ want signed wraparound, since
           | that 's what literally every modern CPU does.
           | 
           | [1] If the standard said that "the integer addition operator
           | maps to the XYZ instruction on this target", that'd be
           | something! But then compilers would have to reason about
           | machine-level semantics to make optimizations. In reality,
           | C's spec is designed by compiler writers for compiler
           | writers, not for programs, and not for hardware.
        
           | zzo38computer wrote:
           | I think that the undefinde behaviour should be partially
           | specified. In the case you describe, it should require that
           | it must do one of the following:
           | 
           | 1. Return any 32-bit answer for the right shift. (The final
           | result will be zero due to the bitwise AND, though,
           | regardless of the intermediate answer.) The intermediate
           | answer must be "frozen" so that if it is assigned to a
           | variable and then used multiple times without writing to that
           | variable again then you will get the same answer each time.
           | 
           | 2. Result in a run-time error when that code is reached.
           | 
           | 3. Result in a compile-time error (only valid if the compiler
           | can determine for sure that the program would run with a
           | shift amount out of range, e.g. if the shift amount is a
           | constant).
           | 
           | 4. Have a behaviour which depends on the underlying
           | instruction set (whatever the right shift instruction does in
           | that instruction set when given a shift amount which is out
           | of range), if it is defined. (A compiler switch may be
           | provided to switch between this and other behaviours.) In
           | this case, if optimization is enabled then there may be some
           | strange cases with some instruction sets where the optimizer
           | makes an assumption which is not valid, but bad assumptions
           | such as this should be reduced if possible and reasonable to
           | do so.
           | 
           | In all cases, a compiler warning may be given (if enabled and
           | detected by the compiler), in addition to the effects above.
        
             | mxmlnkn wrote:
             | I wanted to reply that your point 3 should already be
             | possible with C++ constexpr functions because it doesn't
             | allow undefined behavior. But I it seems I was wrong about
             | that or maybe I'm doing it wrong:
             | [[nodiscard]] constexpr uint64_t         getBits( uint8_t
             | nBits )         {             return BITBUFFER >> ( 64 -
             | nBits ) & ( ( 1ULL << nBits ) - 1U );         }
             | int main()         {             std::cerr << getBits( 0 )
             | << "\n";             std::cerr << getBits( 1 ) << "\n";
             | return 0;         }
             | 
             | The first output will print a random number,
             | 140728069214376 in my case, while the second line will
             | always print 1. However, when I put the ( ( 1ULL << nBits )
             | - 1U ) part into a separate function and print the values
             | for that, then getBits( 0 ) suddenly always returns 0 as if
             | the compiler understands suddenly that it will and with 0.
             | template<uint8_t nBits>         [[nodiscard]] constexpr
             | uint64_t         getBits2()         {             return
             | BITBUFFER >> ( 64 - nBits ) & ( ( 1ULL << nBits ) - 1U );
             | }
             | 
             | In this case, the compiler will only print a warning when
             | trying to call it with getBits2<0>. And here I kinda
             | thought that constexpr would lead to errors on undefined
             | behavior, partly because it always complains about
             | uninitialized std::array local variables being an error.
             | That seems inconsistent to me. Well, I guess that's what
             | -Werror is for ...
             | 
             | Compiled with -std=c++17 and clang 16.0.0 on godbolt:
             | https://godbolt.org/z/qxxWW93Tx
        
               | gpderetta wrote:
               | Unfortunately constexpr doesn't imply constant
               | evaluation. Your function can still potentially be
               | executed at runtime.
               | 
               | If you use the result in an expression that requires a
               | constant (an array bound, a non-type template parameter,
               | a static_assert, or, in c++20, to initialize a constinit
               | variable), then that will force constant evaluation and
               | you'll see the error.
               | 
               | Having said that, compilers have bugs (or simply not
               | fully implemented features), so it is certainly possible
               | that both GCC and clang will fail to correctly catch
               | constant time evaluation UB in some circumstances.
        
               | mxmlnkn wrote:
               | Ah thanks, I was not aware that these compile-time checks
               | are only done when it is evaluated in a compile-time
               | evaluating context.
               | 
               | To add to your list, using C++20 consteval instead of
               | constexpr also triggers the error.
        
         | TwentyPosts wrote:
         | Eh. The existence of Rust (and Zig, to a lesser extent) prove
         | that you can, in fact, have both: Highest performance and safe,
         | properly error checked code without any sort of UB.
         | 
         | UB is used for performance optimizations, yes, but all of these
         | difficult to diagnose UB issues and bugs happen because C++
         | makes it laughably easy to write incorrect code, and (as shown
         | by Rust) this is by no means a requirement for fast code.
        
           | alphanullmeric wrote:
           | You can do any rust optimization yourself in C++ (ie.
           | aliasing assumptions), whereas rust makes the other way
           | around very difficult, often forcing you to use multiple
           | layers of indirection where c++ would allow a raw pointer, or
           | forcing an unwrap on something you know is infallible when
           | exceptions would add no overhead, etc. Rust programmers want
           | people to believe that whatever appeases the supposedly zero
           | cost borrow checker is the fastest thing to do even though it
           | has proven to be wrong time and time again. I can't tell you
           | how many times I've seen r/rust pull the "well why do you
           | want to do that" or "are you sure it even matters" card every
           | time rust doesn't allow you to write optimized code.
        
             | thedracle wrote:
             | > when exceptions would add no overhead
             | 
             | Isn't the overhead for C++ exceptions quite significant,
             | especially if an exception is thrown?
             | 
             | Exception handling can also increase the size of the binary
             | because of the additional data needed to handle stack
             | unwinding and exception dispatch.
             | 
             | I think a number of optimizations are made quite a bit more
             | complex by exception handling as well.
        
               | tialaramex wrote:
               | The argument for the Exception price is that we told you
               | Exceptions were for Exceptional situations. This argument
               | feels reasonable until you see it in context as a library
               | author.
               | 
               | Suppose I'm writing a Clown Redemption library. It's
               | possible to Dingle a Clown during redemption, but if the
               | Clown has already dingled that's a problem so... should I
               | raise an exception? Alice thinks obviously I should raise
               | an exception for that, she uses a lot of Clowns, the
               | Clown Redemption library helps her deliver high quality
               | Clown software and it's very fast, she has never dingled
               | a Clown and she never plans to, the use of exceptions
               | suits Alice well because she can completely ignore the
               | problem.
               | 
               | Unfortunately Bob's software handles primarily dingling
               | Clowns, for Bob it's unacceptable to eat an exception
               | every single damn time one of the Clowns has already been
               | dingled, he _demands_ an API in which there 's just a
               | return value from the dingling function which tells you
               | if this clown was already dingled, so he can handle that
               | appropriately - an exception is not OK because it's
               | expensive.
               | 
               | Alice and Bob disagree about how "exceptional" the
               | situation is, and I'm caught in the middle, but _I_ have
               | to choose whether to use exceptions. I can 't possibly
               | win here.
        
               | alphanullmeric wrote:
               | Like I said, this argument doesn't work because you can
               | use options in c++ but you can't use exceptions in rust.
               | So when there's an occasion where you want to avoid the
               | overhead of an option or result in rust - well too bad.
        
               | kllrnohj wrote:
               | Exceptions cost performance when thrown whereas return
               | values _always_ cost performance.
               | 
               | If all you care about is outright performance, having the
               | option for exceptions is easily the superior choice. The
               | binary does get bigger but those are cold pages so who
               | cares (since exceptions are exceptional, right?)
        
             | celeritascelery wrote:
             | > You can do any rust optimization yourself in C++ (ie.
             | aliasing assumptions)
             | 
             | I don't think that is entirely true. C++ doesn't have any
             | aliasing requirements around pointers, so if the compiler
             | sees two pointers it has to assume they might alias (unless
             | the block is so simple it can determine aliasing itself,
             | which is usually not the case), but in Rust mutable
             | references are guaranteed to not alias.
             | 
             | This was part of the reason it took so long to land the
             | "noalias" LLVM attribute in Rust. That optimization was
             | rarely used in C/C++ land so it had not been battle tested.
             | Rust found a host of LLVM bugs because it enables the
             | optimization everywhere.
        
               | LegionMammal978 wrote:
               | While standard C++ has no equivalent of a noalias
               | annotation, it's wrong to say that it has no aliasing
               | requirements. To access an object behind a pointer (or a
               | glvalue in general), the type of the pointer must be
               | (with a few exceptions) similar to the type of the
               | pointee in memory, which is generally the object
               | previously initialized at that pointer's address. This
               | enables type-based alias analysis (TBAA) in the compiler,
               | where if a pointer is accessed as one type, and another
               | pointer is accessed as a dissimilar type, then the
               | compiler can assume that the pointers don't alias.
               | 
               | Meanwhile, Rust ditches TBAA entirely, retaining only
               | initialization state and pointer provenance in its memory
               | model. It uses its noalias-based model to make up for the
               | lack of type-based rules. I'd say that this is the right
               | call from the user's standpoint, but it can definitely be
               | seen as a tradeoff rather than an unqualified gain.
        
             | cortesoft wrote:
             | Isn't that the point of unsafe blocks in rust? So you can
             | write optimized code when you need to and the rust borrow
             | checker won't let you?
        
               | gmueckl wrote:
               | Unsafe blocks are subject to the same borrow checking
               | that the rest of the language is.
        
               | tialaramex wrote:
               | That is correct. However, raw pointers are not borrow
               | checked, in _safe_ Rust they 're largely useless, but in
               | unsafe Rust you can use raw pointers if that's what you
               | need to do to get stuff done.
               | 
               | As an example inside a String is just a Vec<u8> and
               | inside the Vec<u8> is a RawVec<u8> and _that_ is just a
               | pointer, either to nothing in particular or to the bytes
               | inside the String if the String has allocated space for
               | one or more bytes - plus a size and a capacity.
        
           | overgard wrote:
           | > C++ makes it laughably easy to write incorrect code
           | 
           | It also provides a lot of mechanisms and tools to produce
           | correct safe code, especially modern C++. Most codebases
           | you're not seeing a lot of pointer arithmetic or void pointer
           | or anything of that nature. You hardly even see raw pointers
           | anymore, instead a unique_ptr or a shared_ptr. So yes, you
           | _can_ write incorrect code because it 's an explicit design
           | goal of C++ not to treat you like a baby, but that doesn't
           | mean that writing C++ is inherently like building a house of
           | cards.*
        
           | adamdegas wrote:
           | The Computer Language Benchmarks Game has C++ outperforming
           | Rust by around 10% for most benchmarks. Binary trees is 13%
           | faster in C++, and it's not the best C++ binary tree
           | implementation I've seen. k-nucleotide is 32% faster in C++.
           | Rust wins on a few benchmarks like regex-redux, which is a
           | pointless benchmark as they're both just benchmarking the
           | PCRE2 C library, so it's really a C benchmark.
           | 
           | > because C++ makes it laughably easy to write incorrect code
           | 
           | I was going to ask how much you actually program in C++, but
           | I found a past comment of yours:
           | 
           | > I frankly don't understand C++ well enough to fully judge
           | about all of this
        
             | estebank wrote:
             | > Rust wins on a few benchmarks like regex-redux, which is
             | a pointless benchmark as they're both just benchmarking the
             | PCRE2 C library, so it's really a C benchmark.
             | 
             | The Rust #1 through #6 entries use the regex crate, which
             | is pure-Rust. Rust #7[rust7] (which is not shown in the
             | main table or in the summary, only in the "unsafe"
             | table[detail]) uses PCRE2, and it is interestingly also
             | faster than the C impl that uses PCRE2[c-regex] as well (by
             | a tiny amount). C++ #6[cpp6], which appears ahead of Rust
             | #6 in the summary table (but isn't shown in the comparison
             | page)[comp], also uses PCRE2 and is closer to Rust #7.
             | 
             | [comp]: https://benchmarksgame-
             | team.pages.debian.net/benchmarksgame/...
             | 
             | [detail]: https://benchmarksgame-
             | team.pages.debian.net/benchmarksgame/...
             | 
             | [rust7]: https://benchmarksgame-
             | team.pages.debian.net/benchmarksgame/...
             | 
             | [c-regex]: https://benchmarksgame-
             | team.pages.debian.net/benchmarksgame/...
             | 
             | [cpp6]: https://benchmarksgame-
             | team.pages.debian.net/benchmarksgame/...
        
             | thedracle wrote:
             | I mean, it's outperforming C as well in that particular
             | benchmark.
             | 
             | Lies, damn lies, and benchmarks?
             | 
             | I can at least say, the performance difference between C,
             | C++, and Rust, is splitting hairs.
             | 
             | If you want to write something performant, low level, with
             | predictable timing, all three will work.
             | 
             | I'm spending a lot of time building projects with Rust &
             | C++ these days. The issue/tradoff isn't performance with
             | C++, but that C++ is better for writing unsafe code than
             | Rust.
             | 
             | https://www.p99conf.io/2022/09/07/uninitialized-memory-
             | unsaf...
        
         | tylerhou wrote:
         | The author, Russ Cox, was one of the inventors of Go.
        
         | wheelerof4te wrote:
         | res, err := InterceptNuke(enemyNuke)       if err != nil {
         | fmt.Println("NUCLEAR LAUNCH DETECTED!")         log.Fatal(err)
         | } else {        fmt.Printf("Phew, we're safe. Nuke intercepted
         | after %d seconds.\n", res)       }
         | 
         | Very clearly defined errors indeed.
        
       | oude wrote:
       | I guess we are rediscovering the 40 year old wisdom:
       | https://www.dreamsongs.com/RiseOfWorseIsBetter.html
        
       | kelnos wrote:
       | On one hand this is sort of "duh" (none of the examples in the
       | article were surprises to me). But I think it's very useful to
       | phrase it this way. So many people seem to shrug off the dangers
       | in using C (C++ of course has its issues, though I'd argue it's
       | easier these days to use C++ correctly), especially for security-
       | critical code. It may be a helpful argument to point out that C &
       | C++ were not designed with the correctness of programs in mind.
        
       | dale_glass wrote:
       | I think DJB at some point expressed the desire for a boring
       | compiler with an absolute minimum of UB, and I concur.
       | 
       | I want some sort of flag that disables all this nonsense.
       | 
       | * Uninitialized variable? Illegal, code won't compile.
       | 
       | * Arithmetic overflow? Two's complement
       | 
       | * Null pointer call? Either won't compile, or NULL pointer
       | dereference at runtime worst case.
       | 
       | Yeah, I'm aware -fwrapw and the like exist. But it'd be nice to
       | have a -fno-undefined-behavior for the whole set.
        
         | coliveira wrote:
         | Just turn on all warnings in your C++ compiler, and make
         | warnings the same as error. For example, uninitialized
         | variables are easy to catch as a warning and then turned into
         | an error. More sophisticated compilers can warn about the other
         | issues too.
        
           | UncleMeat wrote:
           | This handles a few cases but nowhere near all of them. Null
           | pointer dereference, use-after-free, data races, and much
           | much more are all global properties with no hope of the
           | compiler protecting you.
        
             | coliveira wrote:
             | We're only considering UB conditions here, not errors in
             | general that may be impossible to detect. Every UB
             | condition can be detected by the compiler because, after
             | all, the compiler needs to check for UB to generate code.
             | All it takes is for the compiler to generate warning/errors
             | when this occurs. If this is not done by your compiler, ask
             | them to add this feature instead of just complaining about
             | UB in general.
        
               | UncleMeat wrote:
               | All of the things I described are UB.
        
         | TillE wrote:
         | Stuff like this sounds great but if you dig into the details of
         | actually trying to implement it, it's not at all simple, and it
         | would inevitably slow down the already-terrible compilation
         | speed of C++.
         | 
         | We have static analysis tools which do a decent job. But like,
         | there's a reason Clang has UBSan to detect undefined behavior
         | _at runtime_. It 's a hard problem.
        
           | kelnos wrote:
           | Not really, at least for two of the three examples mentioned.
           | 
           | Failing compilation on an uninitialized variable is easy. The
           | compiler can already warn about this situation (and gcc and
           | clang at least allow you to promote individual warnings to
           | errors). Making this default would be simple, and not at all
           | a performance concern.
           | 
           | Allowing signed arithmetic to overflow (in a defined 2's
           | complement manner) would be just exactly what the hardware
           | does on modern machines, so there'd be no slowdown there.
           | Sure, the compiler would no longer be allowed to omit code
           | that _checks_ for overflow, but that 's fine: if the
           | programmer truly doesn't care, they won't write that check in
           | the first place.
           | 
           | (Changing these two behaviors might have backward-
           | compatibility concerns, though.)
           | 
           | You are of course correct that NULL dereference checking is
           | would incur a performance penalty at runtime. However, the
           | compiler should be able to catch some subset of cases at
           | compile time. At the very least, there could be a mode where
           | it could at least _warn_ you at compile-time that some
           | dereferences could result in SIGSEGV. Unfortunately, I think
           | it would be hard to get that warning to a point where there
           | weren 't a lot of false positives, so such a warning would be
           | routinely ignored to the point of uselessness.
        
             | agwa wrote:
             | > _You are of course correct that NULL dereference checking
             | is would incur a performance penalty at runtime. However,
             | the compiler should be able to catch some subset of cases
             | at compile time. At the very least, there could be a mode
             | where it could at least warn you at compile-time that some
             | dereferences could result in SIGSEGV._
             | 
             | SIGSEGV on null pointer deference is not the problem. It's
             | actually fine, in the sense that it predictably terminates
             | the program. The problem is that modern optimizing
             | compilers don't guarantee that SIGSEGV will happen; they
             | may rewrite your program to do insane shit instead, as the
             | example in the blog post shows. So we don't need NULL
             | checks at runtime; we just need the compiler to stop doing
             | insane optimizations.
        
             | lelanthran wrote:
             | > Failing compilation on an uninitialized variable is easy.
             | 
             | On uninitialised _variable_ , sure, but impossible to tell
             | at compile time if the program is using an uninitialised
             | _value_.
             | 
             | Compiler authors also appear, to me, to be malicious.
             | 
             | In the past, they worked hard to provide warnings when they
             | detected uninitialised variables.
             | 
             | In the rare case, now, that the compiler is able to tell
             | that a line is using an uninitialised value, instead of
             | issuing a warning, it simply removes the offending code
             | altogether.
             | 
             | If compiler authors now were more like compiler authors of
             | the past, they'd value the user interface enough to make
             | any and all dead code a compile error.
             | 
             | Imagine if past compiler authors had their compiler, when
             | seeing the use of an uninitialised _variable_ , simply go
             | ahead and remove any code that used that variable.
             | 
             | Imagine how poor a user experience that would be
             | considered, and then ask yourself why they feel it is okay
             | now to simply remove dead code.
             | 
             | There is no situation where actual source code lines should
             | be removed rather than warned.
             | 
             | Typing on phone so not going to go into all the data flow
             | explanations.
        
         | kelnos wrote:
         | > _Arithmetic overflow? Two 's complement_
         | 
         | I don't really want this either; I'd rather the program abort.
         | The vast majority of situations where I'm using signed
         | arithmetic, I never want a positive number to overflow to
         | negative (and vice versa).
         | 
         | Unsigned arithmetic is already designed to wrap back to zero;
         | that's useful for things like sequence numbers where wrapping
         | is ok after you "run out".
        
           | toast0 wrote:
           | This is possible, but unless processor accelerated comes at a
           | great cost. You'd need a branch after every math, unless the
           | compiler could prove the math wouldn't overflow.
           | 
           | The ARM mode described elsewhere in the thread where there's
           | an overflow flag that persists across operations would help;
           | then you could do a check less frequently.
           | 
           | A mode were you get a processor exception would be great, if
           | adding that doesn't add significant cost to operations that
           | don't overflow. Assuming the response to such an exception is
           | expected to be a core dump, the cost of generating such an
           | exception can be high; of course if someone builds their
           | bignumber library around the exception, that won't be great.
        
             | kccqzy wrote:
             | > You'd need a branch after every math
             | 
             | A predictable branch is basically free. In your case such a
             | branch is almost never taken.
        
               | celeritascelery wrote:
               | "basically free" is very different then actually "free".
               | Adding a ton of extra branches comes at a cost. Even
               | though it is cheap it still takes an instructions that
               | could be used for something else. You fill your branch
               | prediction target cache must faster, ejecting branches
               | you actually care about. It also makes it harder for the
               | compiler to move code around and harder for the
               | prefetcher to break data dependencies. This all adds up
               | to non-trival overhead. You can tell your C compiler to
               | always check arithmetic, but most don't because of the
               | cost.
        
               | [deleted]
        
             | pdw wrote:
             | Trapping on integer overflow was completely standard before
             | C came along. Fortran and Pascal compilers did it. (Lisp
             | compilers transparently switched to bignums :)
        
         | UncleMeat wrote:
         | This already mostly exists. It means building with
         | asan/msan/tsan/ubsan enabled. And... you'll slow your code down
         | by several factors.
        
         | adrian_b wrote:
         | With gcc you can use e.g. "-fsanitize=undefined,address
         | -fsanitize-undefined-trap-on-error".
         | 
         | In my opinion, any C or C++ program must always be compiled
         | with such options by default, both for debug and release
         | builds.
         | 
         | Only when an undesirable influence on performance is measured,
         | the compilation options can be relaxed, but only for those
         | functions where this matters.
        
       | eatonphil wrote:
       | Maybe I'm completely misunderstanding things, but this article
       | mentions arithmetic overflow as being a problem in C and C++ but
       | isn't this exactly a problem in Go as well? There's no checked
       | arithmetic in Go, right?
       | 
       | I guess the difference between Go and C/C++ is that Go at least
       | won't optimize away the overflow check? But it still doesn't
       | insert it for you either.
        
         | nteon wrote:
         | In Go, overflow is not checked, but it is well-defined in the
         | spec including `programs may rely on "wrap around"`:
         | https://go.dev/ref/spec#Integer_overflow
        
       | beltsazar wrote:
       | > To some extent, all languages do this: there is almost always a
       | tradeoff between performance and slower, safer implementations.
       | 
       | Well, probably except Rust, from which you get both performance
       | and safety. Sometimes it's with a cost of ergonomics, though.
       | Some people, including the Rust's creator himself, would trade
       | performance for better ergonomics in the case of asynchronous
       | programming. They prefer having a non-zero-cost green threads to
       | the current state of async prog in Rust.
        
         | cmovq wrote:
         | Rust does tradeoff performance for safety. Example: integer
         | division [1].
         | 
         | [1]: https://godbolt.org/z/71sxnfff5
        
         | jakobnissen wrote:
         | Not always all of the performance. Rust encourages array access
         | with superfluous bounds checks, unnecessary UTF8 validation,
         | and uses tagged sum types over the more memory efficient raw
         | union types.
        
       | _dain_ wrote:
       | one day, a bug like this will take down an airliner or a power
       | grid and kill thousands of people. and there will be a
       | congressional hearing about it. and some ageing compiler
       | engineers will have to explain to a panel of irate and
       | disbelieving senators the concept of UB. they will have to
       | explain that, while computers _can_ add two numbers correctly, we
       | choose to make them not do that. for performance.
        
       | renox wrote:
       | Note that in Zig an _unsigned overflow_ is also an UB in release
       | unchecked mode. Don 't like it? Don't use unchecked mode or use
       | wrapping operators..
        
       | muldvarp wrote:
       | > In effect, Clang has noticed the uninitialized variable and
       | chosen not to report the error to the user but instead to pretend
       | i is always initialized above 10, making the loop disappear.
       | 
       | No. This is what I call the "portable assembler"-understanding of
       | undefined behavior and it is entirely false. Clang does not need
       | to pretend that i is initialized with a value larger than 10,
       | there is no requirement in the C standard that undefined behavior
       | has to be explainable by looking at it like a portable assembler.
       | Clang is free to produce whatever output it wants because the
       | behavior of that code is literally _undefined_.
       | 
       | Also, compilers don't reason about code the same way humans do.
       | They apply a large number of small transformations, each of these
       | transformations is very reasonable and it is their combination
       | that results in "absurd" optimization results.
       | 
       | I agree that undefined behavior is a silly concept but that's the
       | fault of the standard, not of compilers. Also, several projects
       | existed that aimed to fully define undefined behavior and produce
       | a compiler for this fully defined C, none of them successful.
        
         | mpweiher wrote:
         | 1. The people creating the C standard were adamant that just
         | following the standard was not sufficient to produce a "fit-
         | for-purpose" compiler. This was intentional.
         | 
         | 2. They were also adamant that being a "portable assembler"
         | with predictable, machine-level semantics was an explicit goal
         | of the standard.
         | 
         | 3. The C standard actually does have text giving a list of
         | acceptable behaviours for a compiler and "silently remove the
         | code" is not in that list. And this text used to be normative,
         | but was later made non-normative.
         | 
         | So I blame the people who messed with the standard, and guess
         | who those people were?
        
           | pwdisswordfishc wrote:
           | Citation needed. Which people? What did they actually say?
           | What was the text that supposedly forbade this interpretation
           | of UB? Please don't tell me this is again that tired wankery
           | over "permissible" versus "possible". As if the choice of
           | synonym mattered.
        
             | mpweiher wrote:
             | "Permissible" and "possible" are not synonyms.
        
             | 3836293648 wrote:
             | It's a rather infamous change between C89 and C99 where the
             | description of UB was changed from basically don't do this
             | to please do this and compilers can do whatever they want
             | if you do
        
               | _kst_ wrote:
               | The definition of "undefined behavior" did not change in
               | the way you describe between C89/C90 and C99. In both
               | editions, one possible consequence of undefined behavior
               | is "ignoring the situation completely with unpredictable
               | results" -- i.e., compilers can do whatever they want.
               | 
               | There is no "don't do this" or "please do this" in either
               | edition. Both merely describe the possible consequences
               | if you do.
               | 
               | C90: undefined behavior: Behavior, upon use of a
               | nonportable or erroneous program construct, of erroneous
               | data, or of indeterminately-valued objects, for which the
               | Standard imposes no requirements. Permissible undefined
               | behavior ranges from ignoring the situation completely
               | with unpredictable results, to behaving during
               | translation or program execution in a documented manner
               | characteristic of the environment (with or without the
               | issuance of a diagnostic message), to terminating a
               | translation or execution (with the issuance of a
               | diagnostic message).
               | 
               | If a "shall" or "shall not" requirement that appears
               | outside of a constraint is violated, the behavior is
               | undefined. Undefined behavior is otherwise indicated in
               | this Standard by the words "undefined behavior" or by the
               | omission of any explicit definition of behavior. There is
               | no difference in emphasis among these three; they all
               | describe "behavior that is undefined."
               | 
               | C99: undefined behavior behavior, upon use of a
               | nonportable or erroneous program construct or of
               | erroneous data, for which this International Standard
               | imposes no requirements NOTE Possible undefined behavior
               | ranges from ignoring the situation completely with
               | unpredictable results, to behaving during translation or
               | program execution in a documented manner characteristic
               | of the environment (with or without the issuance of a
               | diagnostic message), to terminating a translation or
               | execution (with the issuance of a diagnostic message).
               | EXAMPLE An example of undefined behavior is the behavior
               | on integer overflow.
               | 
               | (Some of the wording in the C90 definition was moved to
               | the Conformance section in C99.)
        
               | mpweiher wrote:
               | "Permissible" [?] "possible"
        
               | _kst_ wrote:
               | True -- but how does that affect the semantics?
               | 
               | Both definitions say that undefined behavior can be dealt
               | with by "ignoring the situation completely with
               | unpredictable results". There are no restrictions on what
               | can happen.
               | 
               | (The standard joke is that it can make demons fly out of
               | your nose. Of course that's not physically possible, but
               | it would not violate the standard.)
        
           | moefh wrote:
           | > The C standard actually does have text giving a list of
           | acceptable behaviours for a compiler
           | 
           | The exact opposite is explicitly stated in the standard (from
           | C11 section 3.4.3):                   undefined behavior
           | behavior, upon use of a nonportable or erroneous program
           | construct or of erroneous data, for which this International
           | Standard imposes no requirements
           | 
           | The standard then lists some examples of undefined behavior,
           | and it's true that "silently removing the code" is not in the
           | list. Still, I think it's pretty clear that it's acceptable
           | behavior, since the standard _just_ stated it _imposes no
           | requirements_.
        
             | User23 wrote:
             | I think you're misinterpreting. Maybe it's clearer if we
             | elide the relative subclause:                 undefined
             | behavior            behavior ... for which this
             | International Standard imposes no requirements
             | 
             | That is an obvious definition of what is undefined
             | behavior. It's not giving license to do whatever. That said
             | the ship has sailed and what implementors do obviously
             | matters more than what the standard says.
        
               | iso8859-1 wrote:
               | If there are no requirements on what it's doing, how is
               | that not a license to do whatever?
               | 
               | There is not even a requirement that a theoretical
               | program that contains e.g. only preceding code, would
               | still maintain any invariants. So I don't see what an
               | instance of "whatever" that violates "no requirements"
               | would look like.
        
             | mpweiher wrote:
             | " _Permissible_ undefined behavior ranges from ignoring the
             | situation completely with unpredictable results, to
             | behaving during translation or program execution in a
             | documented manner characteristic of the environment (with
             | or without the issuance of a diagnostic message), to
             | terminating a translation or execution (with the issuance
             | of a diagnostic message). "
             | 
             | Note "permissible" and "ranges from ... to".
             | 
             | Again, this used to be normative in the original ANSI
             | standard. It was changed in later versions to no longer be
             | normative. Exactly as I wrote.
        
               | mike_hock wrote:
               | Which is logically equivalent to imposing no
               | requirements. "ignoring the situation completely with
               | unpredictable results" does not meaningfully constrain
               | the possible behaviors.
        
               | mpweiher wrote:
               | That turns out not to be the case.
               | 
               | "Ignoring" is not "taking action based on"
               | 
               | Ignoring is, for example, ignoring the fact that an array
               | access is out of bounds and performing the array access.
               | 
               | Ignoring is _not_ noticing that there is undefined
               | behavior and removing the access and the entire loop that
               | contains the access. Or a safety check.
        
           | _kst_ wrote:
           | C is not a "portable assembler".
           | 
           | An assembly language program specifies a series of CPU
           | instructions.
           | 
           | A C program specifies runtime behavior.
           | 
           | That's a _huge_ semantic difference.
        
             | mpweiher wrote:
             | "Committee did not want to force programmers into writing
             | portably, to preclude the use of C as a "high-level
             | assembler:"
             | 
             | https://www.open-std.org/JTC1/SC22/WG14/www/docs/n897.pdf
             | 
             | p10, line 39
             | 
             | "C code can be portable. "
             | 
             | line 30
        
         | petergeoghegan wrote:
         | > No. This is what I call the "portable
         | assembler"-understanding of undefined behavior and it is
         | entirely false.
         | 
         | "C has been characterized (both admiringly and invidiously) as
         | a portable assembly language" - Dennis Ritchie
         | 
         | The idea of C as a portable assembler is not without its
         | problems, to be sure -- it is an oxymoron at worst, and a
         | squishy idea at best. But the tendency of compiler people to
         | refuse to take the idea seriously, even for a second, just
         | seems odd. The Linux kernel's memory-barriers.txt famously
         | starts out by saying:
         | 
         | "Some doubts may be resolved by referring to the formal memory
         | consistency model and related documentation at tools/memory-
         | model/. Nevertheless, even this memory model should be viewed
         | as the collective opinion of its maintainers rather than as an
         | infallible oracle."
         | 
         | Isn't that consistent with the general idea of a portable
         | assembler?
         | 
         | > I agree that undefined behavior is a silly concept but that's
         | the fault of the standard, not of compilers.
         | 
         | The people that work on compilers have significant overlap with
         | the people that work on the standard. They certainly seem to
         | share the same culture.
        
           | qsort wrote:
           | > But the tendency of compiler people to refuse to take the
           | idea seriously, even for a second, just seems odd.
           | 
           | It's not taken seriously because it shouldn't be taken
           | seriously. It's a profoundly ignorant idea that's entirely
           | delusional about reality. Architectures differ in ways that
           | are much more profound than how parameters go on the stack or
           | what arguments instructions take. As a matter of fact the C
           | standard bends over backwards in the attempt of _not_
           | specifying a memory model.
           | 
           | Any language that takes itself seriously is defined in terms
           | of its abstract machine. The only alternative is the Perl
           | way: "the interpreter is the specification", and I don't see
           | how that's any better.
        
             | mpweiher wrote:
             | That's an interesting opinion.
             | 
             | But it has very little to do with the C programming
             | language.
        
             | nickelpro wrote:
             | > As a matter of fact the C standard bends over backwards
             | in the attempt of not specifying a memory model.
             | 
             | I mean, C explicitly specifies a memory model and has since
             | C11
        
             | petergeoghegan wrote:
             | > It's not taken seriously because it shouldn't be taken
             | seriously
             | 
             | I really don't know what you're arguing against. I never
             | questioned the general usefulness of an abstract machine. I
             | merely pointed out that a large amount of important C code
             | exists that is in tension with the idea that of an all
             | important abstract machine. This is an empirical fact. Is
             | it not?
             | 
             | You are free to interpret this body of C code as "not true
             | ISO C", I suppose. Kind of like how the C standard is free
             | to remove integer overflow checks in the presence of
             | undefined behavior.
        
             | grotorea wrote:
             | I wonder what's the best solution here then. A different
             | language that actually is portable assembly, or has less
             | undefined behaviour or simpler semantics (e.g RIIR), or
             | making -O0 behave as portable assembly?
        
               | _kst_ wrote:
               | Step 1: Define just what "portable assembly" actually
               | means.
               | 
               | An assembly program specifies a sequence of CPU
               | instructions. You can't do that in a higher-level
               | language.
               | 
               | Perhaps you could define a C-like language with a more
               | straightforward abstract machine. What would such a
               | language say about the behavior of integer overflow, or
               | dereferencing a null pointer, or writing outside the
               | bounds of an array object?
               | 
               | You could resolve some of those things by adding
               | mandatory run-time checks, but then you have a language
               | that's at a higher level than C.
        
               | dale_glass wrote:
               | > Perhaps you could define a C-like language with a more
               | straightforward abstract machine. What would such a
               | language say about the behavior of integer overflow
               | 
               | Whatever the CPU does. Eg, on x86, twos complement.
               | 
               | > or dereferencing a null pointer
               | 
               | Whatever the CPU does. Eg, on X86/Linux in userspace, it
               | segfaults 100% predictably.
               | 
               | > or writing outside the bounds of an array object?
               | 
               | Whatever the CPU does. Eg, on X86/Linux, write to
               | whatever is next in memory, or segfault.
               | 
               | > You could resolve some of those things by adding
               | mandatory run-time checks, but then you have a language
               | that's at a higher level than C.
               | 
               | No checks needed. Since we're talking about "portable
               | assembly", we're talking about translating to assembly in
               | the most direct manner possible. So dereferencing a NULL
               | pointer literally reads from address 0x0.
        
               | dooglius wrote:
               | > What would such a language say about the behavior of
               | integer overflow
               | 
               | Two's complement (i.e. the result which is equivalent to
               | the mathematical answer modulo 2^{width})
               | 
               | > dereferencing a null pointer
               | 
               | A load/store instruction to address zero.
               | 
               | > writing outside the bounds of an array object
               | 
               | A store instruction to the corresponding address. It's
               | possible this could overwrite something important on the
               | stack like a return address, in which case the compiler
               | doesn't have to work around this (though if the compiler
               | detects this statically, it should complain rather than
               | treating it as unreachable)
        
         | badsectoracula wrote:
         | > I agree that undefined behavior is a silly concept but that's
         | the fault of the standard, not of compilers.
         | 
         | While undefined behavior is the standard's fault, its
         | interpretation is up to the compiler.
        
         | overgard wrote:
         | > No. This is what I call the "portable
         | assembler"-understanding of undefined behavior and it is
         | entirely false. Clang does not need to pretend that i is
         | initialized with a value larger than 10, there is no
         | requirement in the C standard that undefined behavior has to be
         | explainable by looking at it like a portable assembler. Clang
         | is free to produce whatever output it wants because the
         | behavior of that code is literally _undefined_.
         | 
         | You're kind of lawyering this. Sure, it's "undefined", but is
         | that useful to anyone outside of compiler writers? How useful
         | is it to have a program that's very fast but entirely wrong? If
         | the behavior is undefined, I want an error, not a free license
         | for the compiler to do whatever the hell it wants.
        
           | Vvector wrote:
           | Doesn't -Wall give you the error you want?
        
             | robinsonb5 wrote:
             | Not always - in some cases you need -Wextra or even
             | -Weverything to get a warning. [Edit: was thinking of GCC
             | here, and yesterday's similar thread - clang may be more
             | forthcoming with warnings, I don't know.]
        
         | anon946 wrote:
         | Is all UB silly? E.g., wouldn't fully defining what happens
         | when one goes beyond the end of an array impose a non-trivial
         | performance hit for at least some code?
        
         | grumpyprole wrote:
         | > Also, compilers don't reason about code the same way humans
         | do.
         | 
         | Not these compilers for sure. But I don't agree that all
         | compilers are broken.
         | 
         | > They apply a large number of small transformations, each of
         | these transformations is very reasonable and it is their
         | combination that results in "absurd" optimization results.
         | 
         | Humans use techniques like natural deduction to apply a series
         | of transformations that do not lead to absurd results.
        
         | jonas21 wrote:
         | That's why the author prefaced the sentence with " _In effect_
         | ". The effect in this case is the same as if the compiler had
         | pretended i was initialized to a high value - the loop is
         | omitted.
        
           | plorkyeran wrote:
           | It's still fundamentally a misunderstanding of what the
           | compiler is doing, and thinking about it that way is just
           | going out of your way to cause confusion. The compiler saw a
           | use of an uninitialized value, concluded that it must not be
           | reachable as that's the only way for it to be a legal C
           | program, and then deleted the dead code. You can make up all
           | sorts of other behaviors that the compiler could have done
           | which would have had the same result in a specific scenario,
           | but why would you?
        
             | stanleydrew wrote:
             | I don't think the author misunderstands what the compiler
             | is doing, just saying that it's probably unexpected
             | behavior from the perspective of the program's author.
        
             | afiori wrote:
             | I don't think that the compiler approach is try and fix the
             | code
             | 
             | > it must not be reachable as that's the only way for it to
             | be a legal C program, and then deleted the dead code.
             | 
             | Rather it would make more sense if the approach was more
             | like "this branch is UB so I can whatever is most
             | convenient in terms of optimization" in this case it was
             | merging the 2 branches but discarding the code for the UB
             | branch.
             | 
             | But from a behavioral point of view all these formulations
             | describe the same result
        
               | plorkyeran wrote:
               | The compiler isn't trying to "fix" anything and I'm not
               | sure where that idea came from. The core concept is that
               | the compiler _assumes_ the program is valid and optimizes
               | based on that assumption. If UB would occur if certain
               | values are passed as an argument to a function, the
               | compiler assumes that those values won 't be passed. If
               | UB occurs unconditionally in a function, the compiler
               | assumes that the function won't be called.
        
               | afiori wrote:
               | > The core concept is that the compiler assumes the
               | program is valid
               | 
               | I was referring to this.
               | 
               | IIRC the spec language is that the compiler is free to
               | assume that UB never happens, but from an operational
               | perspective I believe that the compiler simply stop
               | caring about those cases.
               | 
               | by this I mean that for code like
               | #include <stdio.h>              int main() {
               | printf("Hello, ");             0/0;             *(NULL);
               | printf("World!\n");             return 0;         }
               | 
               | most compiler will just pretend that _bad lines_ did not
               | exists
               | 
               | https://godbolt.org/#z:OYLghAFBqd5TKALEBjA9gEwKYFFMCWALug
               | E4A...
        
         | mrighele wrote:
         | > Clang is free to produce whatever output it wants because the
         | behavior of that code is literally _undefined_.
         | 
         | > They apply a large number of small transformations, each of
         | these transformations is very reasonable
         | 
         | One of those transformation detects that we are reading
         | uninitialized memory and acts accordingly. Given that most of
         | the case where we do it is by mistake, I think that doing
         | anything other than raising an error (or at least a big
         | warning) is not a very reasonable thing to do. For those case
         | where such behavior is desired, a compiler flag could be
         | provided.
         | 
         | The fact that the standard allows the current behavior, doesn't
         | mean that the compiler should do it.
        
         | weinzierl wrote:
         | Exactly, and in addition to that, making the loop disappear is
         | not only good for performance but from a compiler perspective
         | _much_ easier than producing a helpful error message that
         | explains the situation well.
        
         | User23 wrote:
         | > Clang is free to produce whatever output it wants because the
         | behavior of that code is literally _undefined_
         | 
         | The standard says no such thing. It lists a number of
         | acceptable ways to handle undefined behavior and none of them
         | are do anything you like.
        
         | spookie wrote:
         | > Also, compilers don't reason about code the same way humans
         | do. They apply a large number of small transformations, each of
         | these transformations is very reasonable and it is their
         | combination that results in "absurd" optimization results.
         | 
         | So true.
         | 
         | It baffles me how sometimes I hear colleagues of mine just
         | "assuming" that the compiler will deal with something.
         | Something that does require a really high level reasoning to
         | come true.
         | 
         | It's absurd.
        
           | somenameforme wrote:
           | IMO it all comes down to the, 'Premature optimization is the
           | root of all evil.' That saying was, at its best, not great.
           | But it seems at some point down the road the 'premature' part
           | of that sentence was lost to history and it became something
           | much worse. It goes some way towards explaining why we now
           | need machines that would be considered supercomputers, just a
           | couple of decades prior, to run a text editor.
           | 
           | Unreal Engine is a beautiful example of this. The source code
           | of that engine is absolutely full of 'premature
           | optimizations.' For instance repeatedly converting a
           | Quaternion to a Rotator (Euler angles) is basically never
           | going to get red-lined on a profiler, let alone be a
           | bottleneck. Yet there's a caching class used in the engine
           | for this exact purpose. It's all of these little 'premature
           | optimizations' that end up creating a system that runs vastly
           | better than most any other engine out there, feature for
           | feature.
        
             | mike_hock wrote:
             | It's a dogma pendulum like "gotos are evil," "YAGNI,"
             | object-oriented design (is good/bad), etc.
             | 
             | A pattern or anti-pattern gets identified and lessons drawn
             | from it. Then people take the lesson as dogma and drive it
             | to absurd extremes. Naturally this doesn't have the desired
             | effect, and then the baby gets thrown out with the
             | bathwater, people do the exact opposite and take _that_ to
             | absurd extremes.
             | 
             | Caching potentially expensive (relatively speaking)
             | operations isn't "premature optimization," it's a design
             | principle. It's much harder to retrofit something like this
             | into a project after the fact than having and using it from
             | the start.
             | 
             | "Premature optimization" is applying complicated micro-
             | optimizations early on that won't survive any moderate code
             | change.
        
           | 6D794163636F756 wrote:
           | It depends on the compiler really. C is nice is that you can
           | choose a compiler fit for your purpose but it makes my next
           | example harder to I'm going to discuss go. I was doing
           | leetcode recently and one of the problems was to combine some
           | set of sorted linked arrays into one bigger array. There are
           | a few ways to do this but I ran two of them to see which was
           | better.
           | 
           | 1.) I select the lowest from the set of first elements in the
           | arrays, add it to the end of my new list, then increment the
           | iterator for that array. 2.) Dump all of them into one array
           | then sort the new array entirely.
           | 
           | Surprisingly the second option was faster and didn't have a
           | higher memory cost. From what I could tell this was because
           | it used fewer memory allocations, could run better
           | optimizations, and the standard library in go has a good
           | sorting algorithm. One of the greatest skills I think we can
           | develop as programmers is knowing when to trust the compiler
           | and when not to.
           | 
           | All of this is to say I agree with you on principle but there
           | is nuance and a degree to which we can trust the compiler,
           | but only if we've chosen one that we are familiar with and
           | that is well suited for the task at hand.
        
             | almostnormal wrote:
             | Not necessarily just a matter of details of the assembly,
             | but also of cache utilization.
        
       ___________________________________________________________________
       (page generated 2023-08-18 23:01 UTC)