3 ms·
People complaining about UB frame it as if it was some intentional malice from compiler authors, a punishment for writing non-standard code. The reality is muc
by pornel 4y ago
People complaining about UB frame it as if it was some intentional malice from compiler authors, a punishment for writing non-standard code.
The reality is much more boring: writing fast, well-optimizing, spec-compliant compiler is hard, and the way current compilers deal with this makes it nearly impossible to know when UB is exploited for good things that users demand, and when it creates nonsensical behaviors.
To tame this complexity, optimizations are typically decoupled into multiple analysis and transformation passes, with each pass doing only its own thing without a context. But this architecture also makes it nearly impossible to see the full picture. The pass that marks "this can't happen" cases doesn't know if it's propagating spec-compliant rules and useful checks done earlier in the function that may be useful for later optimization passes, or creating a weird action at a distance from UB code. The later "delete code that can't happen" pass doesn't know if it's cleverly removing redundant functions hoisted out of loops, remains of `if (0)` and inlined functions, or if it's deleting your slightly-wrong overflow check.
- kazinator 4y ago> with each pass doing only its own thing without a context That's exactly how people working in organizations deny any wrongdoing. "I'm just following orders in my cubbyhole and that's it; I have no idea where this stuff comes from or what happens with it after I'm done." C89's "permissible" word does appear to prohibit situations in which some optimization is performed on the assumption of "no UB", where that assumption is obviously unreasonable. It is permissible to allow unpredictable behavior to take place if the situation is "ignored completely". If it's obvious that UB must be happening, but we ignore that, then we are perhaps "ignoring completely". Turning a blind eye is ignorance, but it's not complete ignorance. For instance, consider an optimization which turns for (int i = 0; i >= 0; i++); into an infinite loop. This is based on 1) knowing that i is initialized to a nonnegative value and 2) examining every update of i in the loop and seeing that i is incremented. But if i is only incremented in the loop, it is inescapable to know that i overflows. Ignoring that situation looks a lot more like "blind eye" ignorance and not passive, complete ignorance.
- pornel 4y agoIt's not an evil scheme to do whatever. It's a bunch of general-purpose optimizations that add up to this. There's a pass that looks at the condition, and checks if "for all cases that C cares about, is this always true?" replaces it with true. This pass ruins your loop, but also the very same pass cleverly removes redundant instructions in many other cases. Not doing this to the loop would require special-casing this particular code, and it's a burden to special-case such code (and many other pathological cases) on top of all the other complexity there is already in optimizers. C gets "lowered" into blocks of assembly-like instructions, so there's no `for` loop any more even before the optimizer touches it. This format is used, because it's easier to correctly optimize than anything which looks like C (partly because C requires supporting wicked things like `case` in places you wouldn't believe are legal). For the most part, C optimizers simulate running code on a Low-Level Virtual Machine described in the C spec, not a specific target CPU. This isn't to screw you for looking at this from perspective of what x86 would do. It's to support lots of different CPUs in one codebase, with the best possible optimizations, in a spec-compliant way. But it means your code will first "run" on an imaginary CPU that really can't overflow an int. Just as a bunch of optimizations that are sensible in isolation can add up to surprising UB bomb, a bunch of "just do the simple thing that the code says" rules can add up to surprisingly inefficient code. If `i++` was simply implemented as `add 1 with overflow`, you couldn't get some strength reduction optimizations in the loop, or index arrays with CPU's native addressing modes (they overflow differently than an int).