UB is always about optimisations and performance. Incidentally, this is why I don't think talking about "nasal demons" is productive. The compiler mostly just uses UB to assume: ah, this can't happen, so I can optimise it away. Often that means valid programs go faster. We wanted it: we got it.
From my limited experience (and it's been a while), -O0 (no optimisations) is really quite reliable, even if you do all kinds of UB shenanigans.
Really? I've seen people switch between competing compilers for licensing reasons, platform support, features---but benchmark performance? Maybe blog posts suggesting that a new compiler wasn't ready.
Compiler writers judge themselves on benchmarks.
No idea whether ICC is still worth paying for
From my experience, ICC is far more reluctant to exploit UB, yet still generates very good code.
So basically ICC has -ffast-math (or -funsafe-math-optimizations) on by default, and you can turn it off with an explicit flag?
I see this as more of a philosophical difference than a material one since you can just add or remove the flag on either one...
Ironically, strict aliasing rule (which is one of the most common causes of UB) makes writing fast programs much harder, because it forbids type punning (except via memcpy or unions).
BTW, according to WG14 mailings and minutes, the C committee is considering either relaxing it or creating a standard way to suppress it in C2X. I can't wait for it.
Brian Kernighan didn't invent C, he co-authored a book about it.
> Whenever discussions like this come up, I like to quote the C standard itself on its definition: "NOTE: Possible undefined behavior ranges from [...] to behaving during translation or program execution in a documented manner characteristic of the environment"
The actual quote is: "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)." - but you snipped out the part that contradicted your point.
> The whole point of C as K&R and ISO is to let you do "what the hardware does". They left parts of the standard purposefully undefined, so it could remain applicable to a wide variety of implementations. The intention was definitely NOT "screw the programmer" as a lot of the compiler writers seem to have interpreted it, but to allow them to do something that makes sense for the environment.
This seems to be contradicted by the full quote above: "ignoring the situation completely with unpredictable results" does sound like it's going to "make sense for the environment". It's also pretty inflammatory to say that the intention of a compiler author is to "screw the programmer".
> Now we have, mostly academics from what I've noticed, that are taking the language farther and farther away from reality; focusing only on something uselessly abstract, completely ignoring the practical consequences of what they're doing. Compilers are becoming increasingly hostile to programmers. A programming language that had humble and very practical uses, with straightforward and easily understood behaviour, has been perverted into theoretical quagmire of uselessness.
So, the "academics" are making compilers "hostile to programmers" and perverting the language into a "theoretical quagmire of uselessness"? That's nonsensical anti-academic garbage unsubstantiated by fact.
I don't think it's the academics, it's the compiler implementers - driven by wanting to win at benchmarks. Our industry is absurdly focused on "performance" over all else (even correctness). But then again, those who care about other things moved on to non-C languages years or decades ago.
However, the performance impact of optimizations that take advantage of UB is not known, and is potentially very large. It would be a very interesting experiment to modify a C/C++ compiler so that every C/C++ program has defined semantics in terms of a simple array-of-bytes abstract machine, and see how much slower the generated code is compared to the regular compiler.
Alternatively, look at compilers like Intel's ICC --- it has historically been one of the best at code generation, yet it's not known for having anywhere near the same level of UB-craziness as Clang (or GCC, to a lesser extent). The same has been my experience with MSVC, at least the earlier versions.
[1]: https://hal.archives-ouvertes.fr/hal-00128124v5/document
So in some respects ICC is very aggressive.
Us academics do have a lot of "fun" figuring out a way to put what the compiler developers do on solid footing [1]. But this is a game of whack-a-mole that will never end: each time we find a way to formally approach some new crazy optimization they came up with, and help them weed out all the bugs that were caused by not carefully thinking through all the implications [2], the next crazy optimization comes up [3].
[1]: https://people.mpi-sws.org/~jung/twinsem/twinsem.pdf
[2]: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65752, https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82282, https://bugs.llvm.org/show_bug.cgi?id=35229, https://bugs.llvm.org/show_bug.cgi?id=34548
[3]: https://bugs.llvm.org/show_bug.cgi?id=21725, https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57359
Safe Rust does have some implementation-specific behavior - for instance, panics can either unwind or abort.
In Unsafe Rust, all bets are off, and the programmer may trigger Undefined Behavior if they're not careful. The upshot is that it's easier to maintain the compiler's invariants because you only need to be careful in a single place about making sure everything is in order. By clearly marking places where the programmer is in charge of maintaining the invariants instead of the compiler, the hope is that mistakes will be minimized (thanks to careful code review and making the programmer think a lot about what they're writing).
Further helping with that, the goal is to have tools to help catch UB at runtime. For instance, miri is an interpreter for MIR (an intermediate language between rust and the LLVM IR) that checks for UB.
I agree that in C, UB has probably be stretched way beyond its intended purpose. But in Rust, this new definition of UB is taken advantage of in much less WTF-inducing ways, allowing us to enjoy those performance guarantees without losing our minds.
And when they're talking about doing something hardware-specific I would take that to be about the MMU on certain platforms. Platforms with an MMU and where the processor is executing code in a virtual address space can generate an access violation if the program code reads data at a virtual address that is not mapped to a specific segment of physical memory.
Even when a program runs in a physical address space, the processor may be configured with an address space that is significantly larger than the actual block of memory and registers on the memory bus. And this matters because if you have a 32-bit processor but the upper 3 gigabytes of the address space don't map to a register file or RAM then it's hardware-dependent what happens when you read from those addresses.
Some hardware will roll the map over and read from somewhere in the available section of the address space based on the offset, and some hardware will treat that as an error which triggers an access violation. Some hardware will bug out and do something completely unexpected. It's undefined behavior what happens in this case. It has everything to do with how your specific chip, mainframe, or minicomputer is wired. And that's what they're referring to.
Undefined behavior does not mean "do whatever you want," it means "do what the hardware would probably do in this situation, or do whatever can be reasonably expected." And that's important here because that means that UB is not carte blanche to violate the programmer's expectations here.
But let's ignore the standard... who cares what it says...
The thing is, what you're asking for inhibits optimizations that many people would very much like to see from their compilers. Like for example if you have an uninitialized function pointer that you only assign to in 1 or 2 locations -- the compiler should be able to just replace the indirect function calls with direct function calls. You're demanding that it doesn't do that, and that it simply call whatever function or non-function that pointer happened to point to. I mean -- you're welcome to ask for that, and maybe your compiler should have a flag to make it behave that way (or maybe it does already? do you use it if so?), but to me and many other people, the compiler should obviously be permitted to see right through that.
Rather than telling someone what they are demanding, it's often better to ask them --- especially if you are sure that what they are demanding is stupid. Personally, I'd guess that 'jschwartzi' might be fine with a compiler that makes the optimization you refer to, and is instead objecting to a compiler that deletes essential safety checks in other parts of the program on the assumption that all bets are off once "undefined behavior" can be proven to occur. If he's like me, he'd probably also prefer that the compiler issue a warning about the undefined behavior rather than silently making changes to the program. But better to ask him than to guess.
In the case of your safety check example, it'd be nice if you could mention something concrete so we know exactly what situation you're talking about. But I mean, I can't rule out that maybe you'll find a couple situations here and there where the standard shouldn't leave things undefined. But the argument I'm rebutting here is that all instances of UB must behave "like the hardware", not that this particular instance is good but another one is bad, so I'm not sure you two would agree. I agree warnings would be nice too (some of which already exist), and I think despite their current efforts compiler still have some ways to go (e.g. a macro expanding to 0 should probably not behave the same as the literal 0 when you multiply, say, by a constant), but again, that's already assuming you're fine with UB...
There are some well-publicised cases of compilers removing NULL-checks[0] on the assumption that the NULL value can't occur as it would be UB.
As another example, the Linux kernel assumes in several places that signed integer overflow wraps, so it is compiling with -f-no-strict-overflow/-fwrapv ever since GCC started optimizing based on this piece of UB (they noticed the compiler behavior change and added the flags before releasing any faulty kernels though, apparently).
People want these optimizations individually. They don't want to keep dead code taking up cycles and they also don't want dead variables taking up registers. So you can't really find much support arguing that those optimizations should be removed entirely. The only real possibilities you can propose here are that the compiler should have magically re-inserted the pruned check during the second optimization, or that it should have performed them in the opposite order. But are you sure these are actually possible and if so, practical? I mean, maybe they are, but they are far from obvious to me. I can easily see the compiler thrashing and failing to reach a fixed point if it re-inserts code that a previous optimization pass pruned. Similarly, I don't see how the compiler can just magically detect an optimization order that ensures "surprising" situations like this don't occur. My guesstimation is that it would carry severe downsides people wouldn't want. Now maybe I'm just not smart enough to see a good solution to this that doesn't carry significant downsides, and there's already one out there. If there is, I'm curious to hear about it, and I hope someone implements it under some flag, but I have yet to hear of one.
For signed integer overflow -- that might be one place where I think it would make sense to just define it to either wrap with 2's complement just like unsigned integers do, or to be unspecified behavior that falls back to the implementation's representation. Though in the latter case... you already have an implementation-specific solution: your compiler flags. But again, we might agree on a couple optimizations here and there, but that's a far cry from saying UB should just fall back to hardware behavior. And honestly, I'm not even here supporting C; I hate it. If you want wrapping, I would suggest it's a sign you might want to use C++ already. Then you can define an integer type that will play Beethoven when you overflow, and people who want their UB on overflow can have that too.
[1] I'm ignoring the validity of address 0 in kernel-mode here; there's also some subtleties on what's a null pointer and what's address zero that are rather beside my point.
That's the sort of thing I would suggest - don't work back from UB (and I agree, I wouldn't expect the optimizer to backtrack optimizations as new facts come up), work forward from actually known facts.
NULL checks are probably a pretty bad example, since the NULL access would surely SEGFAULT if allowed to execute (though the particular case of address 0 vs NULL, and of code catching segfaults like on Windows, throw a wrench in this assumption eve here), but other types of UB are much worse.
If you accidentally issue a read from a point after the end of an array, but only later check that the index was within bounds (e.g. `int x = a[i]; if i < len(a) return x; else return NULL`), the compiler eliding the bounds check by the same logic will take a program that might have been safe in practice to a program that is certainly not safe. Note that I don't know if compilers perform this type of optimization, so this may be a hypothetical.
In general though, I think that the tension here comes from C being used in 2 very different use cases: 1 is C used as portable assembly, where you expect the compiler to keep a pretty 1:1 mapping with your code; and the second one is C used as the ultimate performance language, where you drop to C when you can't optimize further in anything else. I think most of the complaints about "exploiting UB" come from the first camp, whereas the second camp is pretty happy with the current status quo.
Usually these are not reachable in practice so warnings that these exist would cause a tidal wave of pointless warnings. Instead, the compiler simply prunes those paths which can lead to better code generation for the paths that are taken.
What current compilers do is about as far away from “ignoring the situation completely” as it is possible to get.
(b) What compilers are doing does not match "ignore at execution", because they also use it to remove completely separate code.
It isn't.
Rather than rehashing the same arguments I've made over and over, I'll just link to parts of a Twitter thread where Daniel Micay argues eloquently that keeping the sources of UB that we have today as they are is important:
You could always write “WrapAroundSan”.
That also means it's possible to write overflow checks easily, and ones that the compiler won't optimise out. Before compilers became UB-crazy, you could write such checks in the most straightforward way, and get exactly what you expected. I'd consider that a far bigger advantage for security than arguing for the existence of a tool whose sole reason for existence seems to be due to the presence of UB in the first place.
This is effectively what Rust does (replace "SIGILL" by "panic").
no, because unsigned overflow is valid and yet ubsan is able to check for it with -funsigned-integer-overflow (and I caught so many bugs like this)
[1]: https://raphlinus.github.io/programming/rust/2018/08/17/unde...
[2]: https://www.ralfj.de/blog/2017/07/14/undefined-behavior.html
Side note, Kernighan (of K&R) did not have any part in the invention of C, it was all Dennis Ritchie. Kernighan famously wrote the book on it with Ritchie but that was it.
In his own words[1]:
> remember, C is entirely the work of Dennis Ritchie, I am but a popularizer
I agree though, the moment abstract machines distract from actually creating something is the moment one has gone too far.
> "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, ... FITNESS FOR A PARTICULAR PURPOSE.
which would permit the GCC steering committee to decide tomorrow that every other statement must be "HAIL SATAN;" or your code won't compile.
Of course, "can" doesn't mean "should," and if they actually did it, it wouldn't say anything particularly deep about the GPL being a bad license, it would merely indicate that the GCC steering committee had become untrustworthy, and that the community should act accordingly.
Undefined behavior is exactly analogous. It was a degree of freedom designed to allow compiler writers to smooth over CPU architecture friction back in a time when the CPU architecture scene was far less settled than it is now. Trust and responsibility were always understood to be necessary components to make it work. The fact that trust can be broken isn't deep, isn't surprising, and isn't a failure of C.
If C compiler writers want to use UB as an excuse to abuse the trust they have been given, that's on them. I am moving away from C, for many reasons, but the UB-related reason has very little to do with UB being inherently evil and very much to do with the fact that it seems to be placed in increasingly irresponsible hands.
"NOTE: Possible undefined behavior ranges from [...] to behaving during translation or program execution in a documented manner characteristic of the environment"
The whole point of C as K&R and ISO is to let you do "what the hardware does". They left parts of the standard purposefully undefined, so it could remain applicable to a wide variety of implementations. The intention was definitely NOT "screw the programmer" as a lot of the compiler writers seem to have interpreted it, but to allow them to do something that makes sense for the environment.
Now we have, mostly academics from what I've noticed, that are taking the language farther and farther away from reality; focusing only on something uselessly abstract, completely ignoring the practical consequences of what they're doing. Compilers are becoming increasingly hostile to programmers. A programming language that had humble and very practical uses, with straightforward and easily understood behaviour, has been perverted into theoretical quagmire of uselessness.
Something very very odd is going on, and I don't like it one bit. It's very WTF-inducing.
(I don't know much about Rust, hence why I didn't say anything about it. But I've been using C since the late 80s.)