8 ms·
Sure. Compile at -O0. A huge number of seemingly-trivial optimizations depend on assuming that undefined behavior will never happen. The number of optimization
by pcwalton 8y ago
Sure. Compile at -O0.
A huge number of seemingly-trivial optimizations depend on assuming that undefined behavior will never happen. The number of optimizations that don't depend on UB in any way is quite small. For example, if you want to get pedantic about it, even automatic promotion of local variables to registers is exploiting undefined behavior—who's to say you didn't have a pointer that just happened to point to one of them?
- kazinator 8y agoOf course you can have a pointer to a local variable, and that variable can still spend portions of its lifetime in a register. What won't necessarily work as expected is a shadily obtained pointer to a local variable, like by displacing &i to try to point to the same location as &j. If &j doesn't occur anywhere (or does occur, but in a way that is optimized away), j may not even have a location.
- jcranmer 8y agoHow do you reflect the fact that pulling a pointer to a value "out of thin air" may not make any sense without either banning it entirely or defining some step of the process to be undefined behavior?
- esrauch 8y agoMost languages have no concept of undefined behavior and they still have compilers and runtimes that can do a huge amount of optimizations.
- eddyb 8y agoThe languages that you describe must look nothing like C (other than, ironically, syntax), and must have no untyped direct memory access pointer feature at all, which usually means they rely on a GC instead for memory safety.
- zokier 8y ago> and must have no untyped direct memory access pointer feature at all Can you expand on why you feel this must be the case? I'm thinking for example deference of pointer could be defined to be equivalent of platform-specific memory load instruction. It wouldn't be memory safe and would segfault like C, but it still wouldn't bring up nasal demons like C UB.
- pcwalton 8y agoIf every source-language load must result in a memory load, then you at the very least killed scalar replacement of aggregates (a critical optimization), and, depending on how you define it, you might have killed register allocation too.
- eddyb 8y agoYou get register allocation back if you deny taking the address of variables declared with the `register` keyword - and we've gone full circle!
- kazinator 8y agoTaking the addresses of variables defined register is denied; that's the only modern meaning of the specifier.
- kazinator 8y agoGive me the rule "every source language load of a structure member, array or global variable, or anything loaded indirectly through a pointer results in an actual load" and I will still crank out fast code, by explicitly using local variables for caching the values coming from those places.
- pcwalton 8y agoYou'll also write unmaintainable code. To a large degree, optimizations exist to allow programmers to write maintainable code without sacrificing performance.
- pcwalton 8y agoRight, because those languages are type- and memory-safe. C is not.
- xenadu02 8y agoBecause they define-away the scenarios where UB is an issue for most code. Sometimes they do this by having a GC, but Rust and Swift prove that isn't necessary. Such languages typically do provide some form of "no promises, here there be dragons" in the form of unsafe blocks or functions. Restricting this region of unsafety is a huge benefit for programmers and compilers. Programmers because relatively few pieces of code need to be carefully reviewed. Compilers because most of the program contains little or no UB. As a typical example, in C you can alias and type pun. Therefore to avoid all UB the compiler would need to be extremely careful in any function containing a pointer. You could return a reference to a local through a separate opaque function call, or receive multiple pointers all aliasing the same memory. To completely avoid all UB means inserting checks after every potentially mutable machine instruction, or emitting duplicate function bodies that take different paths when the pointers alias; that's assuming the compiler even has enough type information to answer the question! With typical C#, Rust, Swift, etc you just can't cause those kinds of problems without deliberate subterfuge and use of Unsafe types or blocks.
- pjmlp 8y agoLanguages like Ada and Modula-2 were already proving it long time ago. Or to go even further in the past, NEWP, still alive on Unisys ClearPath mainframes.
- eddyb 8y agoNote that `unsafe` code blocks (or having some unsafe primitives) fundamentally results in some kind of UB in the language as a whole, and you totally can create tons of problems from it, since the rest of the language has invariants it can't itself violate, but the unsafe code can (potentially much easier than C, if there are more invariants) - that is the "UB", and the invariants that can be violated are what the compiler optimizes based on. Lest we forget, any language with a C FFI capability must have some notion of UB, because the FFI effectively includes the C code in its own semantics (unless fully sandboxed, which may be too expensive to be done).
- pjmlp 8y agoTrue, but they are quite easy to track down. In systems like Unisys ClearPath, you can even configure the system such that only admins can execute applications with unsafe blocks. In C any line of code, if care is not taken to use the correct compiler flags, can be a possible source of UB.
- vyodaiken 8y agoI'd like to see some actual evidence of some significant optimization that depends on UB.
- eddyb 8y agoSee this sibling thread https://news.ycombinator.com/item?id=17189666 https://news.ycombinator.com/item?id=17189666 - typically anything touching memory needs invariants to be optimized, which in languages that can directly manipulate memory means there's also UB (code that can't be statically proven not to break those invariants).
- vyodaiken 8y agoyou don't need UB for invariant optimization. What UB permits the compiler to do is INCORRECTLY assume invariance. This permits the compiler to "optimize" badly written C code in a way that silently changes its function - which I don't call an optimization.
- xenadu02 8y agoI’m not sure you’ve though this through. Any C function that manipulates pointers (or arrays) can alias those pointers, type pun, and otherwise touch the same block of memory through different paths and/or treating the bits as different types. Vast areas of optimization are completely closed to you if you want code to be resilient in the face of aliasing. You should sit down with pen and paper to figure out how to optimize a simple function while preserving invariants but without UB. It would be very illuminating.
- vyodaiken 8y agoIt's really irritating that people take this condescending approach. C is harder to optimize than language that e.g. do not have pointers. That is a tradeoff the language designers chose because they wanted C programmers to be able to type pun, for example. This is why C89's only limit on type punning was memory alignment. The intent of "restrict" was to enable some aliasing optimizations via opt-in. So, sure, things that are not invariant in C are not available for invariant optimization. C permits aliasing. So it calls for smarter compiler techniques. If you want a language that forbids aliasing, use one that does and then call out to C libraries. But breaking C to allow CS220 "clever optimizing tricks" to be easily used is bad engineering. Essentially, what you are saying is, "elimination of invariants is a good optimization, so pretending C has more invariants than it does is a good thing. " So maybe come up with a real example and stop hand waving.
- loup-vaillant 8y ago> Sure. Compile at -O0. Hmm, nope. Sure, it will save you from much UB. But GCC and Clang have idiotic front ends. On purpose. They subscribe to the idea that the optimizer phase should clean up the front end mess. They're not entirely wrong, since it does simplify the front end, and does not really make the rest any more complex. It does make it slower, though. Long story short, those compilers don't generate reasonable code by default. You need at least -O1.
- glandium 8y agoI was recently surprised to find that GCC, at -O0, does some level of optimization. At least it does for multiplications. https://godbolt.org/g/ekTkBm https://godbolt.org/g/ekTkBm Edit: the irony is that if you do compile with optimizations, it does a different optimization.
- umanwizard 8y agoWhat do you mean by "reasonable" ? Can you give an example of code that is compiled in a more obvious, straightforward way with O1 than O0 ?
- loup-vaillant 8y agoMy knowledge of this is only second hand, so I don't have any specific example. I believe Chandler Carruth explains this well on one of his LLVM talks (that is, how and why the front end emits crappy intermediate code, before the optmiser kicks in).
- xorblurb 8y agoThat's, given the question, is a really moronic answer. There are means to apply the modern approach of optimizations beyond O0 on C without importing all kind of UB from the language level. You just have to actually proove the properties you want to rely on, instead of relying on wishful "UB => authorized by the standard => programmer fault if anything goes bad" thinking. And promoting local variable to registers CERTAINLY NOT depends on language level UB. It would be permitted by the as-if general rule even if anything prevented it to happen in the first place, which is not the case. You don't have to have an address if nobody wants it and random pointers haver never been required to allow access to all objects, especially those who might never have an address at all. Plus nobody ever expected that anyway. People expect 2s complement. Or at least something that can not result in nasal daemons, and given C history, something that matches what the processor does. So 2s complements is at least not utterly stupid. So conflating the two is dishonest to the highest point -- except maybe if the only intended audience of the C language is now experts who e.g. write compilers. What a bright future this would be. Hell, we dropped the hypothetical flat memory model even without strict aliasing for maybe 20 years (and probably 30, to be honest), and this had NEVER caused the kind of issues we are talking about. So don't pretend it did, just to dismiss the real issues. So ok even then it was actually probably informal as hell and in some ways worse for experts, but the amount of exploited UB was also WAY smaller. Quantity matters in this area. And context too. Do you want secure OR fast embedded systems? I would prefer reasonably secure and reasonably fast. Certainly NOT fast to execute and exploit, or more probably fast to crash pathetically. You know very well that compiling in O0 is not going to happen in prod on tons on projects. Don't dismiss real concerns with false "solutions", especially when mixed with proofs of your misunderstanding of the situation.
- comex 8y ago> You don't have to have an address if nobody wants it The spec says otherwise: > An object exists, has a constant address, and retains its last-stored value throughout its lifetime. [C11 6.2.4.2] But that's more of a pedantic detail. More pragmatically, you do have a point. It is indeed possible to build a C compiler that has no "undefined behavior", but only unspecified behavior, as long as the program doesn't actually violate memory safety by, say, writing to some random address it can't prove it has permission to write. For example, guessing the stack slot used for a variable and overwriting it pretty much has to be undefined behavior – it's hard to optimize anything if variables can randomly change their values without being referenced. But that's okay, because overwriting random memory is inherently unsafe without obtaining a guarantee of what that memory will be used for. On the other hand, reading from random stack memory could be unspecified. A particular stack slot might be used for a variable, a temporary expression, or nothing at all, so it's unspecified what you might find there. But the compiler will always generate a single, real load instruction, without making any assumptions about aliasing that it can't prove; thus, you'll never get logical impossibilities like "x + 1 > x". And such a compiler could definitely produce code that's better optimized than -O0 – because -O0 is a very, very low bar (it doesn't even do register allocation, in the compilers I've seen). But I expect it would do substantially worse than a modern compiler's -O2 even on average code, with a lot of little missed optimizations that add up. (Though if you're using -fno-strict-aliasing, you're probably already eating a decent percentage of that penalty.) And in the worst cases, like tight loops that can be autovectorized only by taking advantage of undefined behavior, it might be only a fraction of the speed of the better-optimized version. Still, it might be an interesting project, especially if you could formalize the "no undefined behavior" guarantee.