Most active commenters
  • AlotOfReading(9)
  • pcwalton(9)
  • uecker(9)
  • grandempire(4)
  • jcranmer(4)
  • gavinhoward(3)
  • bluGill(3)
  • dcrazy(3)
  • bobmcnamara(3)
  • moefh(3)

92 points endorphine | 128 comments | | HN request time: 2.935s | source | bottom
1. dataflow ◴[] No.43536838[source]
It's not really that they prioritize performance over correctness (your code becomes no more correct if out-of-bounds write was well-defined to reboot the machine...), it's that they give unnecessary latitude to UB instead of constraining the valid behaviors to the minimal set that are plausibly useful for maximizing performance. E.g. it is just complete insanity to allow signed integer overflow to format your drive. Simply reducing it to "produces an undefined result" would seem plenty adequate for performance.
replies(1): >>43536958 #
2. Brian_K_White ◴[] No.43536927[source]
They let the programmer be the ultimate definer of correctness.

They don't prioritize performance over correctness, they prioritize programmer control over compiler/runtime control.

replies(2): >>43536968 #>>43537845 #
3. hn-acct ◴[] No.43536958[source]
The author points out near the bottom that “performance” was not one of the original justifications for its UB decisions, afatct.

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++.

replies(1): >>43537203 #
4. hn-acct ◴[] No.43536968[source]
But they don’t really when some compilers silently remove code without mentioning it.
replies(1): >>43537170 #
5. ajross ◴[] No.43537017[source]
This headline is badly misunderstanding things. C/C++ date from an era where "correctness" in the sense the author means wasn't a feasible feature. There weren't enough cycles at build time to do all the checking we demand from modern environments (e.g. building medium-scale Rust apps on a Sparcstation would be literally *weeks* of build time).

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.

replies(5): >>43537270 #>>43537327 #>>43537466 #>>43537560 #>>43537849 #
6. meling ◴[] No.43537030[source]
I love the last sentence: “…, if you set yourself the goal of crossing an 8-lane freeway blindfolded, it does make sense to focus on doing it as fast as you possibly can.”
replies(2): >>43537714 #>>43544408 #
7. on_the_train ◴[] No.43537040[source]
Honey it's time for your daily anti C++ post
replies(2): >>43537508 #>>43537623 #
8. Calavar ◴[] No.43537170{3}[source]
The compiler removes code under the assumption that your code doesn't have UB. If your code has UB, that's a bug. "When my code is buggy the compiler outputs a buggy executable, but it's buggy in a different way than I want" has always struck me as somewhat of an odd complaint.

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.

replies(2): >>43537387 #>>43539105 #
9. tialaramex ◴[] No.43537203{3}[source]
> I agree that there needs to be a “reasonable UB”

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".

10. bgirard ◴[] No.43537270[source]
Did anything prevent them from transitioning undefined behavior towards defined behavior over time?

> 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'.

replies(1): >>43537710 #
11. VWWHFSfQ ◴[] No.43537327[source]
I don't think the headline is misunderstanding anything.

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.

12. adgjlsfhk1 ◴[] No.43537387{4}[source]
the problem is that any ub that is too difficult for a compiler to turn into a compile time error is also too difficult for humans to reliably prevent.
replies(1): >>43538038 #
13. pcwalton ◴[] No.43537392[source]
I was disappointed that Russ didn't mention the strongest argument for making arithmetic overflow UB. It's a subtle thing that has to do with sign extension and loops. The best explanation is given by ryg here [1].

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...

replies(6): >>43537702 #>>43537771 #>>43537976 #>>43538026 #>>43538237 #>>43538348 #
14. netbioserror ◴[] No.43537431[source]
There's a way I like to phrase this:

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.

replies(2): >>43537487 #>>43545348 #
15. rocqua ◴[] No.43537466[source]
> 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.

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.

replies(1): >>43537696 #
16. grandempire ◴[] No.43537487[source]
> but near-impossible to write incorrect code.

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…

replies(4): >>43537549 #>>43537582 #>>43537814 #>>43538545 #
17. grandempire ◴[] No.43537508[source]
I can only reply to so many UB comments in a day…
18. ◴[] No.43537549{3}[source]
19. pjmlp ◴[] No.43537560[source]
The author is a famous compiler writer, including C and C++ compilers as GCC contributor, regardless of how Go is designed, he does know what he is talking about.
replies(1): >>43538066 #
20. netbioserror ◴[] No.43537582{3}[source]
True, but I think errors in real-world modeling logic are part of our primary problem domain, while managing memory and resources are a secondary domain that obfuscates the primary one. Tools such as exceptions and contract programming go a long way towards handling the issues we run into while modeling our domains.
replies(1): >>43537653 #
21. 01HNNWZ0MV43FF ◴[] No.43537623[source]
Every day until my employer gives the juniors a blessing to learn something better
22. gavinhoward ◴[] No.43537632[source]
As a pure C programmer [1], let me post my full agreement: https://gavinhoward.com/2023/08/the-scourge-of-00ub/ .

[1]: https://gavinhoward.com/2023/02/why-i-use-c-when-i-believe-i...

replies(1): >>43539291 #
23. uecker ◴[] No.43537642[source]
You can implement C in completely different ways. For example, I like that signed overflow is UB because it is trivial to catch it, while unsigned wraparound - while defined - leads to extremely difficult to find bugs.
replies(4): >>43537908 #>>43538002 #>>43538056 #>>43538186 #
24. grandempire ◴[] No.43537653{4}[source]
Indeed, but you said something more extreme in the first comment.
25. bluGill ◴[] No.43537696{3}[source]
They did have implementation defined behavior, but a large part of undefined behavior was exactly that: never define anywhere and could have always been raised to implementation defined if they had thought to mention it.
replies(1): >>43538472 #
26. AlotOfReading ◴[] No.43537702[source]
There's half a dozen better ways that could have been addressed anytime in the past decade.

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.

replies(1): >>43537833 #
27. bluGill ◴[] No.43537710{3}[source]
No, and that has been happening over time. C++26 for example looked at uninitialized variables and defined them. The default is intentionally unreasonable for all cases where this would happen just forcing everyone to initialize (and also because the value is unreasonable makes it easy for runtime tools to detect the issue when the compiler cannot)
28. mananaysiempre ◴[] No.43537714[source]
That is more glib than insightful, I think: the programming equivalent of “as fast as you can” in this metaphor would likely be measured in lines of code, not CPU-seconds.
replies(2): >>43538129 #>>43539188 #
29. indigoabstract ◴[] No.43537734[source]
After perusing the article, I'm thinking that maybe Ferraris should be more like Volvos, because crashing at high speed can be dangerous.

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).

30. dcrazy ◴[] No.43537771[source]
The C language does not specify that `int` is 32-bits. That is a choice made by compiler developers to make compiling non-portable code written for 32-bit platforms easier, because most codebases wind up baking in assumptions about variable sizes.

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.

replies(1): >>43537843 #
31. nine_k ◴[] No.43537780[source]
/* I can't help but remember a joke on the topic. One guy says: "I can operate on big numbers with insane speed!" The other says: "Really? Compute me 97408743 times 738423". The first guy, immediately: "987958749583333". The second guy takes out a calculator, checks the answer, and says: "But it's incorrect!". The first guy objects: "Despite that, it was very fast!" */
replies(1): >>43546373 #
32. jayd16 ◴[] No.43537814{3}[source]
They should have said safe/unsafe. Correct implies it also hits the business need, among other baggage.
33. pcwalton ◴[] No.43537833{3}[source]
> Anything from making it implementation defined to unspecified behavior to just throwing a diagnostic warning or having a clang-tidy performance rule.

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.

replies(4): >>43538103 #>>43538139 #>>43538208 #>>43551655 #
34. pcwalton ◴[] No.43537843{3}[source]
> The C language does not specify that `int` is 32-bits. That is a choice made by compiler developers to make compiling non-portable code written for 32-bit platforms easier, because most codebases wind up baking in assumptions about variable sizes.

Making int 32-bit also results in not-insignificant memory savings.

replies(1): >>43538095 #
35. vacuity ◴[] No.43537845[source]
Empirically, then, we don't have enough programmers capable of mastering such control.
36. jayd16 ◴[] No.43537849[source]
But we write new code in C and C++ today. We make these tradeoffs today. So its not some historical oddity. That is the tradeoff we make.
37. dehrmann ◴[] No.43537908[source]
Some version of ints doing bad things plagues lots of other languages. Java, Kotlin, C#, etc. silently overflow, and Javascript numbers can look and act like ints until they don't. Python is the notable exception.
38. tmoravec ◴[] No.43537976[source]
size_t has been in the C standard since C89. "for (int i = 0..." might have it's uses so it doesn't make sense to disallow it. But I'd argue that it's not really a common textbook way to iterate over an array.
replies(1): >>43537997 #
39. pcwalton ◴[] No.43537997{3}[source]
The first example program that demonstrates arrays in The C Programming Language 2nd edition (page 22) uses signed integers for both the induction variable and the array length (the literal 10 becomes int).
replies(2): >>43538148 #>>43539066 #
40. AlotOfReading ◴[] No.43538002[source]
There's 3 reasonable choices for what to do with unsigned overflow: wraparound, saturation, and trapping. Of those, I find wrapping behavior by far the most intuitive and useful.

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.

replies(4): >>43538187 #>>43538933 #>>43539073 #>>43539198 #
41. Someone ◴[] No.43538026[source]
> Sadly, the C-like pattern persists.

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.

42. Calavar ◴[] No.43538038{5}[source]
UB can't be a compile time error though. And I don't mean it's too hard because it would require Turing complete/undecidable compile time analysis, I mean a compile time error for UB would be a violation of the language contract with the programmer. The programmer can say trust me, I don't need bounds checking in this function because the caller ensures that the index is in bounds. And this could actually be a safe assumption if, let's say, this function will only ever be called by machine generated code. You can't statically analyze the presence/absence of UB there, even if you magic a way around the decidability problem, because you don't know if the programmer was right that all inputs the function will ever see are guaranteed to be safe.

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.

replies(1): >>43546268 #
43. Strilanc ◴[] No.43538056[source]
Could you provide the code you use to trivially catch signed overflows? My impression is the opposite: unsigned is trivial (just test `a+b < b`) while signed is annoying (especially because evaluating a potentially-overflowing expression would cause the UB I'm trying to avoid).
replies(6): >>43538172 #>>43538229 #>>43538928 #>>43539940 #>>43539954 #>>43541133 #
44. ajross ◴[] No.43538066{3}[source]
It's still a bad headline. UB et. al. weren't added to the language for "performance" reasons, period. They were then and remain today compatibility features.
replies(2): >>43538405 #>>43541974 #
45. bobmcnamara ◴[] No.43538095{4}[source]
And even wastes cycles on 16bit size_t MCUs.
replies(2): >>43538244 #>>43538284 #
46. frumplestlatz ◴[] No.43538103{4}[source]
Even if it is the most common method in text books (I’m not sure that’s true), it’s also almost always wrong. The index must always be sized to fit what you’re indexing over.

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.

replies(1): >>43538414 #
47. z3phyr ◴[] No.43538129{3}[source]
Always think from the users perspective. For a lot of applications, when a user does something, it should happen with minimal latency.
48. AlotOfReading ◴[] No.43538139{4}[source]
No, I'm saying that there could be anything from a one word change to the standard that doesn't affect compilers at all to safety by default with a clang tidy performance warning.

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.

replies(1): >>43538290 #
49. frumplestlatz ◴[] No.43538148{4}[source]
The language has evolved significantly, and we’ve learned a lot about how to write safer C, since that was published in 1988.
50. uecker ◴[] No.43538172{3}[source]
Oh, I meant it is trivial to catch bugs caused by overflow by using a sanitizer and difficult to find bugs caused by wraparound.

But checking for signed overflow is also simply with C23: https://godbolt.org/z/ebKejW9ao

51. tsimionescu ◴[] No.43538186[source]
Signed overflow being UB does not make it easier to find in any way. Any detection for signed overflow you can write give that it's UB could be found if it were defined. There are plenty of sanitizers for behaviors that are not UB, at least for other languages, so it's not even an ecosystem advantage.
replies(1): >>43543580 #
52. uecker ◴[] No.43538187{3}[source]
I am fine with unsigned wraparound, I just think one should avoid using these types for indices and sizes, and use them only for the applications where modulo arithmetic makes sense mathematically.
53. agwa ◴[] No.43538208{4}[source]
> 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.

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".

replies(1): >>43538271 #
54. ndiddy ◴[] No.43538229{3}[source]
Compiling with -ftrapv will cause your program to trap on signed overflow/underflow, so when you run it in a debugger you can immediately see where and why the overflow/underflow occurred.
replies(1): >>43542268 #
55. sapiogram ◴[] No.43538237[source]
Thank you so much for this comment. I think Russ Cox (along with many others) is way too quick to declare that removing one source of UB is worth a (purportedly) minuscule performance reduction. While I'm sure that's sometimes true, he hasn't measured it, and even a 1% slowdown of all C/C++ would have huge costs globally.
56. moefh ◴[] No.43538244{5}[source]
Is there any MCU where `size_t` is 16 bits but `int` is 32 bits? I'm genuinely curious, I have never seen one.
replies(2): >>43538605 #>>43541701 #
57. pcwalton ◴[] No.43538271{5}[source]
> 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).

58. dcrazy ◴[] No.43538284{5}[source]
Now that you mention it, at least on Wintel compiler vendors did not preserve the definition of `int` during the transition from 16-bit to 32-bit. I started in the 386 era myself so I have no frame of reference for porting code from 286. But Windows famously retains a lot of 16-bit heritage, such as defining `DWORD` as 32 bits, making it now a double-anachronism. I wonder if the decision to model today’s popular 64-bit processors as LP64 is related to not wanting to go through that again.

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...

replies(1): >>43541674 #
59. pcwalton ◴[] No.43538290{5}[source]
Have you measured the performance cost? I highly suspect it is not negligible, given that it's the most common way to write inner loops, which are the most performance-sensitive type of code.
replies(1): >>43538438 #
60. agentultra ◴[] No.43538340[source]
If you don't write a specification then any program would suffice.

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...)

61. dooglius ◴[] No.43538348[source]
As mentioned in the linked post, the compiler could in fact prove the increment on i wont overflow, and in my testing, -fwrapv does produce identical assembly (YMMV). The post talks about hypothetical more complex cases where the compiler would not prove the loop bound. But if -fwrapv semantics were mandated by spec, then presumably compilers would at least hardcode a check for such a common optimization (if they are not doing so already).
replies(1): >>43538430 #
62. pjmlp ◴[] No.43538405{4}[source]
That is what implementation defined were supposed to be.
63. pcwalton ◴[] No.43538414{5}[source]
> Even if it is the most common method in text books (I’m not sure that’s true), it’s also almost always wrong. The index must always be sized to fit what you’re indexing over.

"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.

64. pcwalton ◴[] No.43538430{3}[source]
> But if -fwrapv semantics were mandated by spec, then presumably compilers would at least hardcode a check for such a common optimization (if they are not doing so already).

I don't know what this means. The optimization becomes invalid if fwrapv is mandated, so compilers can't do it anymore.

replies(1): >>43538931 #
65. AlotOfReading ◴[] No.43538438{6}[source]
There's a range of possible performance costs. Let's talk about the lowest: the standard changes "the behavior is undefined" to "the behavior is unspecified". Everything else is the same, except that suddenly your program still has semantic meaning if there's signed overflow.

Why would that be problematic?

replies(1): >>43538560 #
66. moefh ◴[] No.43538472{4}[source]
I don't doubt what you're saying is true, I have heard similar things many many times over the years. The problem is that it's always stated somewhat vaguely, never with concrete examples, and it doesn't match my (perhaps naive) reading of any of the standards.

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.

[1] https://port70.net/~nsz/c/c99/n1256.html

replies(2): >>43539071 #>>43539082 #
67. dang ◴[] No.43538508[source]
Discussed at the time:

C and C++ prioritize performance over correctness - https://news.ycombinator.com/item?id=37178009 - Aug 2023 (543 comments)

68. kstrauser ◴[] No.43538545{3}[source]
> Except most bugs are about unforeseen states

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.

replies(2): >>43539998 #>>43541660 #
69. pcwalton ◴[] No.43538560{7}[source]
What does "unspecified" mean in this context? If you mean "the value of a signed integer is unspecified if it overflows", then I can still construct cases in which you lose the optimization that widens a loop induction variable from 32-bit to 64-bit. For example, take this code:

    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.
replies(1): >>43538650 #
70. dcrazy ◴[] No.43538605{6}[source]
Me either, but it wouldn’t be unreasonable if the target has 32-bit ALUs but only 16 address lines and no MMU.
71. AlotOfReading ◴[] No.43538650{8}[source]
"unspecified" in the standards sense of "unspecified behavior", i.e. any chosen behavior is permissible. The compiler doesn't have to document the behavior and the behavior isn't constrained, it just isn't undefined. That's still better than where we are today.
replies(1): >>43539976 #
72. o11c ◴[] No.43538928{3}[source]
Avoiding UB for performing the signed addition/subtraction/multiplication is trivial - just cast to unsigned, do the operation, cast back. In standard C23 or GNU C11, you can write a `make_unsigned` and `make_signed` macro using `typeof` and `_Generic`.
73. dooglius ◴[] No.43538931{4}[source]
The optimization is still valid under -fwrapv semantics. To see this, observe the invariant (0 <= i && i < count) when entering the loop body, and (i==0 || (0 < i && i <= count)) when doing the loop test -- in particular, 0<=i<INT_MAX when entering the loop body (since count <= INT_MAX), so wraparound cannot occur.
74. jcranmer ◴[] No.43538933{3}[source]
> There's 3 reasonable choices for what to do with unsigned overflow: wraparound, saturation, and trapping.

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.)

replies(2): >>43539519 #>>43540958 #
75. Maxatar ◴[] No.43539066{4}[source]
From what I see, that book was published in 1988.
76. bluGill ◴[] No.43539071{5}[source]
What you are not seeing is times where the standard didn't say anything at all.
replies(1): >>43539341 #
77. cwzwarich ◴[] No.43539073{3}[source]
> it's the natural implementation in hardware

The natural implementation in hardware is that addition of two N-bit numbers produces an N+1-bit number. Most architectures even expose this extra bit as a carry bit.

replies(1): >>43539787 #
78. jcranmer ◴[] No.43539082{5}[source]
I think bluGill might be referring to cases of undefined behavior which are undefined because the specification literally never mentions the behavior, as opposed to explicitly saying the behavior is undefined.

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; }".

79. Maxatar ◴[] No.43539105{4}[source]
>... has always struck me as somewhat of an odd complaint.

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.

80. skavi ◴[] No.43539188{3}[source]
in certain situations, latency is an aspect of correctness. HFT and robotics come to mind.
81. itishappy ◴[] No.43539198{3}[source]
Does wrapping not break the successor relationship as well? I suppose it's a different problem than saturation, but the symptoms are the same: the relationship between a number and it's successor is no longer intuitive (not injective for saturation, not ordered for wrapping).
82. muldvarp ◴[] No.43539291[source]
To quote your article:

> 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?

replies(2): >>43540135 #>>43540589 #
83. gblargg ◴[] No.43539295[source]
The infinite loops example doesn't make sense. If count and count2 are volatile, I don't see how the compiler could legally merge the loops. If they aren't volatile, it can merge the loops because the program can't tell the difference (it doesn't even have to update count or count2 in memory during the loops). Only code executing after the loops could even see the values in those variables.
84. moefh ◴[] No.43539341{6}[source]
Are those instances of undefined behavior relevant to what's being discussed here? The vast majority undefined behavior people argue/warn/complain about, including the original article, is behavior that is explicitly defined to be undefined (I say that with the caveat that I almost never use C++ and I've never read any C++ standard closely, so my perception is biased towards C; things might be different for C++).

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.

85. AlotOfReading ◴[] No.43539519{4}[source]
You can get exactly the same "benefits" without the side effects by simply making signed overflow unspecified rather than undefined. There are better alternatives of course, but this is the one that has essentially no tradeoffs.

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.

replies(1): >>43539840 #
86. AlotOfReading ◴[] No.43539787{4}[source]
Addition of two 1-bit numbers produces a 1-bit number, which is simple and fundamental enough that we call it XOR. If you take that N-bit adder and drop the final carry (a.k.a use a couple XORs instead of the full adder), you get wrapping addition. It's a pretty natural implementation, especially for fixed width circuits where a carry flag to hold the "Nth+1" bit may not exist, like RISC-V.
87. jcranmer ◴[] No.43539840{5}[source]
> You can get exactly the same "benefits" without the side effects by simply making signed overflow unspecified rather than undefined.

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.

replies(2): >>43540139 #>>43543491 #
88. ◴[] No.43539940{3}[source]
89. steveklabnik ◴[] No.43539954{3}[source]
You're right that this test would be UB for signed integers.

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.

90. steveklabnik ◴[] No.43539976{9}[source]
I looked this up after you mentioned it upthread, I thought you meant implementation-defined, but I learned that yeah, this exists too. The difference is that implementation-defined requires documentation of the choice made, whereas unspecified does not. However,

> 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.

91. gpderetta ◴[] No.43539998{4}[source]
Parent said bugs, not security bugs.
replies(1): >>43550040 #
92. bsder ◴[] No.43540135{3}[source]
> 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?

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.).

replies(1): >>43547291 #
93. AlotOfReading ◴[] No.43540139{6}[source]
My understanding is that unspecified behavior is poison as the standard understands it. The standard pretty much days that implementation being free to do whatever it thinks is reasonable. I'm not deeply familiar with the nuances of LLVM's terminology here, but that would be frozen poison I think?
replies(1): >>43543516 #
94. gavinhoward ◴[] No.43540589{3}[source]
I was willing to, but the consensus was that people wouldn't use it.

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.

replies(1): >>43543146 #
95. amavect ◴[] No.43540958{4}[source]
I feel like this all motivates for a very expressive type system for integers. Add different types for wraparound, saturation, trapping, and undefined. Probably require theorem proving in the language to provably never overflow for undefined overflow integers.

>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.

replies(1): >>43554303 #
96. amavect ◴[] No.43541133{3}[source]
>unsigned is trivial (just test `a+b < b`)

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".

replies(1): >>43542957 #
97. grandempire ◴[] No.43541660{4}[source]
Imagine a backlog full of buffer overflows and nil pointers.
98. bobmcnamara ◴[] No.43541674{6}[source]
Some of the 8-bit MCUs I started with defaulted to standards noncompliant 8-bit int. 16-bit was an option, but slower and took much more code.
99. bobmcnamara ◴[] No.43541701{6}[source]
The original 32-bit machine, the Manchester Baby, would've likely had a 32-bit int, but with only 32 words of RAM, C would be rather limited, though static-stack implementations would work.
100. fooker ◴[] No.43541974{4}[source]
You are wrong. The formalized concept of UB was introduced exactly because of this.

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.

replies(1): >>43546319 #
101. AlotOfReading ◴[] No.43542268{4}[source]
It's worth mentioning that GCC's ftrapv has been unreliable and partially broken for 20+ years at this point. It's recommended that you use the fsanitize traps instead, and there's an open ticket to switch the ftrapv implementation over to using it under the hood:

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=101521

replies(1): >>43549143 #
102. lelanthran ◴[] No.43542957{4}[source]
> Nitpicking, the test itself should avoid overflowing.

Why? Overflowing is well defined for unsigned.

replies(1): >>43552153 #
103. muldvarp ◴[] No.43543146{4}[source]
Shouldn't your blog post then condemn the C community at large for failing to use a more "reasonable" C compiler instead of complaining about compiler authors that "despite holding the minority world view, [...] have managed to force it on us by fiat because we have to use their compilers"?

You don't have to use their compilers. Most people do, because they either share this "minority" world view or don't care.

replies(1): >>43549117 #
104. jurschreuder ◴[] No.43543378[source]
Already all fixed in C++.

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.

replies(1): >>43543418 #
105. jurschreuder ◴[] No.43543418[source]
Also in my experience, but you have to take my word for it, C++ "feels" more mathematically correct than Python.

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.

106. uecker ◴[] No.43543491{6}[source]
Clang now has "frozen poison" is because the original poison was essentially a flawed concept that lead to incorrect optimizations. I certainly do no think we should import this into the C standard.

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.

replies(1): >>43546491 #
107. uecker ◴[] No.43543516{7}[source]
Unspecified means you pick a value at a specific point in time (the standard defines this point in time) and then you treat is a regular value. It is not the same as poison. It might be similar to frozen poison, but I am not 100% sure about clang does there.
108. uecker ◴[] No.43543580{3}[source]
One can have sanitizers also for defined behavior. The issue is that a sanitizer that has no false positives is about 100x more useful than a sanitizer that has false positives. You can treat each case where a sanitizer detects signed overflow as an error, even in production. You can not do this same when the behavior is defined and not an error. (you can make it an error and still define it, but there is not much of a practical difference)
replies(1): >>43543907 #
109. tsimionescu ◴[] No.43543907{4}[source]
If you think signed overflow is a mistake, you could forbid it from your code base, even if it weren't UB, and then any instance of it that a sanitizer finds would not be a true positive, because your code style forbids signed integer overflow.
replies(1): >>43545471 #
110. ptsneves ◴[] No.43544408[source]
Most comments like this, fall into a form of scarecrow fallacy. They assume performance in C/C++ can only come at the cost of correctness and then go on to show examples of such failures to prove the point, and there are so many. On the other hand the event space also has sets of cases where you can be correct and get faster.

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.

111. spacechild1 ◴[] No.43545348[source]
> but near-impossible to write incorrect code.

Rust makes it near-impossible to make typos in strings or errors in math formulas? That's amazing! So excited to try this out!

112. uecker ◴[] No.43545471{5}[source]
If you only have little self-contained projects where you can control everything, then yes.
113. adgjlsfhk1 ◴[] No.43546268{6}[source]
I don't think this is true. the standard says that valid code does not run UB, do of the compiler can prove that code runs UB, it is invalid code
114. ajross ◴[] No.43546319{5}[source]
UB was not introduced to facilitate optimization, period. At the time the ANSI standard was being written, such optimizations didn't even exist yet. The edge case trickery around "assume behavior is always defined" didn't start showing up until the late 90's, a full decade and a half later.

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.

115. nindalf ◴[] No.43546373[source]
I love how he multiplied 2 numbers ending with 3 and confidently gave an answer ending in 3.
replies(1): >>43549760 #
116. jcranmer ◴[] No.43546491{7}[source]
It was undef that was broken, not poison.

(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).

replies(1): >>43548932 #
117. WalterGillman ◴[] No.43547291{4}[source]
I'm willing to write a C compiler that detects all undefined behavior but instead of doing something sane like reporting it or disallowing it just adds the code to open a telnet shell with root privileges. Can't wait to see the benchmarks.
replies(1): >>43547426 #
118. muldvarp ◴[] No.43547426{5}[source]
> doing something sane like reporting it or disallowing it

This is only possible if you check for it at runtime and that's a tradeoff most C programmers don't like.

119. uecker ◴[] No.43548932{8}[source]
Ah right, thanks! undef is what I meant.
120. gavinhoward ◴[] No.43549117{5}[source]
My blog post doesn't condemn the C community at large because it was the HN comments on that post where I found out that the C community is a bunch of hypocrites.
121. ndiddy ◴[] No.43549143{5}[source]
Thanks, I hadn't heard of that.
122. nine_k ◴[] No.43549760{3}[source]
Consider it an inadvertent rhyme.
123. kstrauser ◴[] No.43550040{5}[source]
There's a fine line between a crash and a vulnerability.
replies(1): >>43550815 #
124. gpderetta ◴[] No.43550815{6}[source]
not all bugs are crashing bugs, and not all crashing bugs are vulnerabilities (and not all vulnerabilities are crashing bugs, although they are all bugs).
125. UncleMeat ◴[] No.43551655{4}[source]
I know the team at Google that is doing exactly this. They've very explicitly accepted a small but significant performance overhead in order to improve safety.
126. amavect ◴[] No.43552153{5}[source]
Personal preference, hence nitpicking. It forms a special case of the signed integer algorithm, which feels nice.
127. uecker ◴[] No.43554303{5}[source]
I am not sure more types are the solution. I like types, but I do not like complicated things.

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.