They don't prioritize performance over correctness, they prioritize programmer control over compiler/runtime control.
Your example is a slippery slope but I get your point. I agree that there needs to be a “reasonable UB”
But I’ve moved on from c++.
And more: the problem faced by the ANSI committee wasn't something where they were tempted to "cheat" by defining undefined behavior at all. It's that there was live C code in the world that did this stuff, for real and valid reasons. And they knew if they published a language that wasn't compatible no one would use it. But there were also variant platforms and toolchains that didn't do things the same way. So instead of trying to enumerate them all individually (which probably wasn't possible anyway), they identified the areas where they knew they could define firm semantics and allowed the stuff outside that boundary to be "undefined", so existing environments could continue to implement them compatibly.
Is that a good idea for a new language? No. But ANSI wasn't writing a new language. They were adding features to the language in which Unix was already written.
Of course it can be difficult to know when you've unintentionally hit UB, which leaves room for footguns. This is probably an unpopular opinion, but to me that's not an argument for rolling back UB-based optimizations; it's an argument for better diagnostics (are you *sure* you meant to do this), rigorous testing, and for eliminating some particularly tricky instances of UB in future revisions of the standard.
What could you possibly want "reasonable UB" for? If what you want is actually just Implementation Defined that already exists and is fine, no need to invent some notion of "reasonable UB".
> It's that there was live C code in the world that did this stuff, for real and valid reasons.
If you allow undefined behavior, then you can move towards a more strictly defined behavior without any forward compatibility risk without breaking all live C code. For instance in the `EraseAll` example you can define the behavior in a more useful way rather than saying 'anything at all is allowed'.
These things are both true:
> C and C++ Prioritize Performance over Correctness
> C/C++ date from an era where "correctness" in the sense the author means wasn't a feasible feature.
So correctness wasn't feasible, and therefore wasn't a priority. The machines were limited, and so performance was the priority.
As a summary: The most common way given in C textbooks to iterate over an array is "for (int i = 0; i < n; i++) { ... array[i] ... }". The problem comes from these three facts: (1) i is a signed integer; (2) i is 32-bit; (3) pointers nowadays are usually 64-bit. That means that a compiler that can't prove that the increment on "i" won't overflow (perhaps because "n" was passed in as a function parameter) has to do a sign extend on every loop iteration, which adds extra instructions in what could be a hot loop, especially since you can't fold a sign extending index into an addressing mode on x86. Since this pattern is so common, compiler developers are loath to change the semantics here--even a 0.1% fleet-wide slowdown has a cost to FAANG measured in the millions.
Note that the problem goes away if you use pointer-width indices for arrays, which many other languages do. It also goes away if you use C++ iterators. Sadly, the C-like pattern persists.
[1]: https://gist.github.com/rygorous/e0f055bfb74e3d5f0af20690759...
In C and C++, it's easy to write incorrect code, and difficult to write correct code.
In Rust, it's also difficult to write correct code, but near-impossible to write incorrect code.
The new crop of languages that assert the inclusion of useful correctness-assuring features such as iterators, fat-pointer collections, and GC/RC (Go, D, Nim, Crystal, etc.) make incorrect code hard, but correct code easy. And with a minimal performance penalty! In the best-case scenarios (for example, Nim with its RC and no manual heap allocations, which is very easy to achieve since it defaults to hidden unique pointers), we're talking about only paying a 20% penalty for bounds-checking compared to raw C performance. For the ease of development, maintenance, and readability, that's easy to pay.
These things didn't become undefined behavior. They became implementation defined behavior. The distinction is that for implementation defined behavior, a compiler has to make a decision consistently.
The big point of implementation defined behavior is 1s vs 2s complement. I believe shifting bits off the end of an unsigned int is also considered implementation defined.
For implementation defined behavior, the optimization of "assume it never happens" isn't allowed by the standard.
Except most bugs are about unforeseen states (solved by limiting code paths and states) or a disconnect between the real world and the program.
So it’s very possible to write incorrect code in rust…
[1]: https://gavinhoward.com/2023/02/why-i-use-c-when-i-believe-i...
Anything from making it implementation defined to unspecified behavior to just throwing a diagnostic warning or having a clang-tidy performance rule.
I'm also incredibly suspicious of the idea that FAANG in particular won't accept minor compiler slowdowns for useful safety. Google and Apple for example have both talked publicly about how they're pushing bounds checking by default internally and you can see that in the Apple Buffer hardening RFC and the Abseil hardening modes.
But if one doesn't find that exciting, at least they'd better blaze through the critical sections as fast as possible. And double check that O2 is enabled (/LTCG too if on Windows).
In Swift, for example, `Int` is 64 bits wide on 64-bit targets. If we ever move to 128-bit CPUs, the Swift project will be forced to decide to stick to their guns or make `Int` 64-bits on 128-bit targets.
To be clear, you're proposing putting a warning on "for (int i = 0; i < n; i++)"? The most common textbook way to write a loop in C?
> I'm also incredibly suspicious of the idea that FAANG in particular won't accept minor compiler slowdowns for useful safety.
I worked on compilers at FAANG for quite a while and know quite well how these teams justify their existence. Telling executives "we cost the company $1M a quarter, but good news, we made the semantics of the language easier for programming language nerds to understand" instead of "we saved the company $10M last quarter" is an excellent strategy for getting the team axed next time downsizing comes around.
Making int 32-bit also results in not-insignificant memory savings.
Saturation breaks the successor relation S(x) != x. Sometimes you want that, but it's extremely situational and rarely do you want saturation precisely at the type max. Saturation is better served by functions in C.
Trapping is fine conceptually, but it means all your arithmetic operations can now error. That's a severe ergonomic issue, isn't particularly well defined for many systems, and introduces a bunch of thorny issues with optimizations. Again, better as functions in C.
On the other hand, wrapping is the mathematical basis for CRCs, Error correcting codes, cryptography, bitwise math, and more. There's no wasted bits, it's the natural implementation in hardware, it's familiar behavior to students from a young age as "clock arithmetic", compilers can easily insert debug mode checks for it (the way rust does when you forget to use Wrapping<T>), etc.
It's obviously not perfect either, as it has the same problem of all fixed size representations in diverging from infinite math people are actually trying to do, but I don't think the alternatives would be better.
I think that’s the main problem: C-style “arrays are almost identical to pointers” and C-style for loops may be good ideas for the first version of your compiler, but once you’ve bootstrapped your compiler, you should ditch them.
C++ has two issues with UB: 1) potentially UB operations are on by default and not opt-in at a granular level and 2) there are a whole lot of them.
Rust has shown that mortals can write robust code in a language with UB if unsafe operations are opt-in and number of rules is small enough that you can write tooling to help catch errors (miri). And Rust can be much more aggressive than GCC or clang at making UB-based optimizations, particularly when it comes to things like pointer aliasing.
As for your compiler statement — yes. At least at Apple, there is ongoing clang compiler work, focused on security, that actively makes things slower, and there has been for years.
Clang tidy and the core guidelines have already broken the textbook Hello, World! with performance-avoid-endl warning, so I don't see why the common textbook way to write things should be our guiding principle here. Of course, the common textbook way to write things would continue working regardless, it'd just have a negligible performance cost.
But checking for signed overflow is also simply with C23: https://godbolt.org/z/ebKejW9ao
And yet, Google is willing to take a performance hit of not 0.1% but 0.3% for improved safety: https://security.googleblog.com/2024/11/retrofitting-spatial...
And obviously there are better justifications for this than "we made the semantics of the language easier for programming language nerds to understand".
There are not. For all the noise that we make on message boards about signed overflow being UB, there are very few actual security problems traceable to it. The problems from it are largely theoretical.
That entire blog post talking about the 0.3% regression illustrates just how much of an effort it is, and how unusual it is, to make those kinds of changes. The reason why Google engineers managed to justify that change is that memory safety results in actual measurable security improvements by enforcing spatial safety. Signed overflow being UB just doesn't make the cut (and let's be clear: at that scale the cost of a 0.3% regression is measured in millions per year).
Edit: of course, I completely forgot that Windows chose LLP64, not LP64, for x86_64 and AArch64. Raymond Chen has commented on this [1], but only as an addendum to reasons given elsewhere which have since bitrotted.
[1]: https://devblogs.microsoft.com/oldnewthing/20050131-00/?p=36...
We're at C23 now and I don't think that section has changed? Anyone know why the committee won't revisit it?
Is it purely, "pragmatism," or dogma? (Are they even distinguishable in our circles...)
"The code was wrong, so it was OK that I made it slower" is a message board argument, not a business argument.
> As for your compiler statement — yes. At least at Apple, there is ongoing clang compiler work, focused on security, that actively makes things slower, and there has been for years.
The performance of code that runs on consumer devices has less of a measurable economic impact than that of code that runs on the server.
I don't know what this means. The optimization becomes invalid if fwrapv is mandated, so compilers can't do it anymore.
Why would that be problematic?
For example, I just checked C99[1]: it says in many places "If <X>, the behavior is undefined". It also says in even more places "<X> is implementation-defined" (although from my cursory inspection, most -- but not all -- of these seem to be about the behavior of library functions, not the compiler per se).
So it seems to me that the standards writers were actually very particular about the difference between implementation-defined behavior and undefined behavior.
C and C++ prioritize performance over correctness - https://news.ycombinator.com/item?id=37178009 - Aug 2023 (543 comments)
Study after study shows that's not true, unless you include buffer overflows and the various "reading and writing memory I didn't mean to because this pointer is wrong now" classes of bugs.
It's possible to write logic errors, of course. You have to try much harder to write invalid state or memory errors.
for (int i = 0; i != n; i++)
sum += a[i];
If n is -2, will this function include the element at a[(uint64_t)INT_MAX + 1] in the sum? If signed overflow is UB, then the answer is "maybe", and thus i and n can legally be promoted to 64-bit. If signed overflow is unspecified, but the result must still fit in a 32-bit integer, then the answer is "no", and i and n can't legally be promoted.There's a 4th reasonable choice: pretend it doesn't happen. Now, before you crucify me for daring to suggest that undefined behavior can be a good thing, let me explain:
When you start working on a lot of peephole optimizations, you quickly come to the discovery that there are quite a few cases where two pieces of code are almost equivalent, except that they end up giving different answers if someone overflowed (or some other edge case you don't really care about). Rather interestingly, even if you put a lot of effort into a compiler to make it aggressively infer that code can't overflow, you still run into problems because those assumptions don't really compose well (e.g., knowing that (A + (B + C)) can't overflow doesn't mean that ((A + B) + C) can't overflow--imagine B = INT_MAX and C = INT_MIN to see why).
And sure, individual peephole optimizations don't make much of a performance effect. But they can sometimes have want-of-a-nail side effects, where a failure because of inability to assume nonoverflow in one place causes another optimization to fail to kick in and the domino effect results in measurable slowdowns. In one admittedly extreme example, I've seen a single this-might-overflow result in a 10× slowdown, since it alone was responsible for the autoparallelization framework to fail to kick in. This is happened enough to me that there are times I just want to shake the computer and scream "I DON'T FUCKING CARE ABOUT EDGE CASES, JUST GIVE ME THE DAMN FASTEST CODE."
The problem with undefined behavior isn't that it risks destroying your code if you hit it (that's a good thing!); the problem is that it too frequently comes without a way to opt-out of it. And there is room to argue if it should be opt-in or opt-out, but completely absent is a step too far for me.
(Slight apologies for the rant, I'm currently in the middle of tracking down a performance hit caused by... inability to infer non-overflow of an operation.)
My canonical example of such a case is what happens if you call qsort where the comparison function is "int compare(const void*, const void*) { return 1; }".
On the contrary I'd argue that the idea that any arbitrary bug can have any arbitrary consequence whatsoever is odd.
There's nothing odd about expecting that the extent of an operation is bounded in space and time, it's a position that has a great body of research backing it.
> The question is: should compiler authors be able to do whatever they want? I argue that they should not.
My question is: I see so many C programmers bemoaning the fact that modern compilers exploit undefined behavior to the fullest extent. I almost never see those programmers actually writing a "reasonable"/"friendly"/"boring" C compiler. Why is no one willing to put their ~money~ time where their mouth is?
What I mean to say is that the "problem" of undefined behavior does seem to be intentionally introduced by the authors of the standard, not an oversight.
I don't consider destroying code semantics if you hit it a good thing, especially when there's no reliable and automatic way to observe it.
Actually, you don't. Unspecified behavior means that you have some sort of limited "blast radius" for the behavior; in LLVM's terminology, it would be roughly equivalent to "freeze poison"--i.e., the overflow returns some value, which is chosen nondeterministically, but that is the limit you can do with it. By contrast, the way LLVM handles undefined overflow is to treat it as "poison", which itself is already weaker than C/C++'s definition of UB (which LLVM also has) [1].
Now poison seems weirder in that it has properties like "can be observed to be a different value by different uses (even within the same instruction)." But it also turns out from painful experience that if you don't jump to that level of weird behavior, you end up accidentally making optimizations like "x * 2" -> "x + x" illegal because oops our semantics accidentally makes the number of uses of a value something that can't be increased because formal semantics are hard. (Hats off to Nuno Lopes et al for working on this! We need more formalism in our production compilers!)
[1] IMHO, C itself could benefit from fronting a definition of poison-like UB to the standard, rather than having just one kind of undefined behavior. But that also requires getting the committee to accept that UB isn't an inherently evil thing that needs to be stamped out at all costs, and I don't have the energy to push hard on that front myself.
See here for that in action, as well as one way to test it that does work: https://godbolt.org/z/sca6hxer4
If you're on C23, uercker's advice to use these standardized functions is the best, of course.
> the behavior isn't constrained
C++23, at least, does say
> The range of possible behaviors is usually delineated by this document.
So there are sometimes some constraints.
Because it is not much harder to simply write a new language and you can discard all the baggage? Lots of verbiage gets spilled about undefined behavior, but things like the preprocessor and lack of "slices" are way bigger faults of C.
Proebsting's Law posits that compiler optimizations double performance every 20 years. That means that you can implement the smallest handful of compiler optimizations in your new language and still be within a factor of 2 of the best compilers. And people are doing precisely that (see: Zig, Jai, Odin, etc.).
C and C++ programmers complain about UB, but they don't really care.
TCC is probably the closest thing we have to that, and for me personally, I made all of my stuff build on it. I even did extra work to add a C99 (instead of C11) mode to make TCC work.
>knowing that (A + (B + C)) can't overflow doesn't mean that ((A + B) + C) can't overflow
Here, the associative property works for unsigned integers, but those don't get the optimizations for assuming overflow can't happen, which feels very disappointing. Again, adding more types would make this an option.
Nitpicking, the test itself should avoid overflowing. Instead, test "a <= UINT_MAX - b" to prove no overflow occurs.
For signed integers, we need to prove the following without overflowing in the test: "a+b <= INT_MAX && a+b >= INT_MIN". The algorithm follows: test "b >= 0", which implies "INT_MAX-b <= INT_MAX && a+b >= INT_MIN", so then test "a <= INT_MAX-b". Otherwise, "b < 0", which implies "INT_MIN-b >= INT_MIN && a+b <= INT_MAX", so then test "a >= INT_MIN-b".
Let's take something as simple as divide by zero. Now, suppose you have a bunch of code with random arithmetic operations.
A compiler can not optimize this code at all without somehow proving that all denominators are non zero. What UB brings you is that you can optimize the program based on the assumption that UB never occurs. If it actually does, who cares, the program would have done something bogus anyway.
Now think about pointer dereferences, etc etc.
Why? Overflowing is well defined for unsigned.
You don't have to use their compilers. Most people do, because they either share this "minority" world view or don't care.
And I don't know why now everything has to be beginner friendly. Then just use a high level language. C++ is never advertised as a high level language it's a mid-level language.
Still with that even C++ has never shut down my computer or bricked my computer even once.
All these young people are just too spoiled.
For me as an experienced developer Python has more undefined behaviour because it executes slightly different every time. The delays are different, threads work inconsistently.
It's a bit like the metaphor that our brain is so big with so many small rules that it feels like you have free will.
That's how Python feels. Somewhere in the millions of lines it generates there are bugs. I can sense it. Not enough to make it crash but they are there.
I similar to the Microsoft guys inventing time-travel UB which lead to their compiler being broken. As a standard committee we need to push back against such nonsense instead of endorsing it. I am all for formalizing this stuff precisely though.
There are also sets of events where the failing risk is an acceptable tradeoff. Even the joke can come up empty, if you want to cross the 8bit lane the fastest possible and dont mind failing/dying some times it might be worth it.
Also all the over emphasis on security is starting to be a pet peeve of mine. It sounds like all software should be secure by default and that is also false. When I develop a private project or a project with a threat scenario that is irrelevant i dont want to pay the security setup price, but it seems nowadays security became a tax. Cases in point:
I cannot move my hard disk from one computer to another because secure boot was enabled by default without jumping hoops.
I cannot install self signed certificates for my localhost without jumping hoops.
I cannot access many browser APIs from an HTTP endpoint even if that endpoint is localhost. In that case i cannot do anything about it, the browser just knows better for my safety.
I cannot have a localhost server serving mixed content. I mean come on why should i care about CORS locally for some google font.
I cannot use docker build kit with a private registry with HTTP but to use a self signed certificat I need to rebuild the intermediate container.
I must be nagged to use the latest compatibility breaking version library version for my local picture server because of a new DoS vulnerability.
[...] On and on, and being a hacker/tinkerer is a nightmare of proselytizing tools and communities. I am a build engineer at heart and even I sometimes just want to develop and hack, not create the next secure thing that does not even start up
This is like being in my home and the contractor forcing me to use keys to open every door to the kitchen, bedroom or toilet. The threat model is just not applicable, let me be.
Rust makes it near-impossible to make typos in strings or errors in math formulas? That's amazing! So excited to try this out!
UB was introduced to allow for variant/incompatible platform behavior (in your example, how the hardware treats a divide by zero condition) in a way that allowed pre-existing code to remain valid on the platform it was written, but to leave the core language semantics clear for future code.
(See https://llvm.org/devmtg/2016-11/Slides/Lopes-LongLivePoison.... for the start of the discussion that leads to the development of the current situation).
The practical solution is simply -fsanitize=signed-integer-overflow. If you need complete assurance that there can not be a trap at run-time, in the rare case where I wanted this, just looking at the optimized code and making sure the traps have been optimized out was surprisingly effective.