Impact of Undefined Behavior on Performance
softwarebits.substack.com
softwarebits.substack.com
Not discussed (that I can see), the real problem is the loop variable -- because the loop ends with ' <= n', which means if n is the largest unsigned integer, this is an infinite loop. One easy thing UB is useful for is proving these types of loop terminate (as overflowing would be UB).
gcc doesn't seem to implement this optimization at all.
Technically it should be possible for the <=n case, too: An infinite loop is undefined behavior, so the compiler is allowed to assume it doesn't happen. But I guess (for good reasons) compiler writers are reluctant to exploit that too heavily (or it's just about ordering of optimization passes).
EDIT: I had a quick look to see why the obvious equation ( n (n+1))/2 ) didn't just work. The only problem for large n is you need to do the 'n (n+1)' in a bigger type (it doesn't technically matter if the n*(n+1) overflows, you just need a bigger type), so when you divide by 2 you get the right value in the top-most byte of 'unsigned int'.
If you have defined wrapping/saturating semantics then you don't get this option, even if overflow is a bug in your program.
AFAIK rust also panics on overflow in debug builds, so it's already decided that overflow is semantically a bug by default. So in release mode as it is already a bug then you potentially can't reason about the further behavior of the program. The compiler to assume that overflow doesn't happen and using that to optimize your program does not worsen the situation too much.
You can use this fact to insert assertions that check if that happened and you won't be surprised that the compiler removes the check code you just wrote to check whether something happened!
> The compiler to assume that overflow doesn't happen and using that to optimize your program does not worsen the situation too much.
quite the contrary, the situation is much much worse! The C compiler silently removes code you may have written with the explicit intention of checking the effects of something.
Yes sure you have bug; now with underfined behaviour you have an even harder time figuring out you have a bug in the first place!
I'd be slightly less mad at C (copmilers) if it provided me a way of annotation a statement with "please don't optimize this out".
But seriously, just don't exploit undefined behaviour. Programs have bugs; yes when a program hits a bug and continues you can say that the behaviour of the program is no longer defined according the the original intentions of the author. But wouldn't it be much much better if at least you can reason about what the program does according to how the (buggy) program is actually written?
How mad would you be at a debugger that when asked to inspect the state of a variable that has been subjected to a signed overflow, would just lie to you and tell you it has a value it had some time in the past (or worse)?
Well, unsigned overflow is also a potential bug, and it's defined behavior. So if you want useful integer overflow diagnostics, you already need to break the C/C++ spec.
Of course, the correct way to model this in a programming language is to distinguish between bitvectors with defined twos-complement operations and fixed-range integers.
https://gist.github.com/rygorous/e0f055bfb74e3d5f0af20690759...
I think this is the key point a lot of responses are missing. It's not that undefined behavior is innately good or bad. It's that it leaves wiggle room that can potentially be exploited for good while being too rigid may not always be the best long-term strategy.
I used to think of code as something that should live forever and compile/run for all time without change. Over time, I've come to realize that just as spoken languages evolve, so do our programming languages. To run/understand code written 2 decades ago, we often have to use compilers/interpreters from that era. I doubt that will change even with extremely rigid language specifications.
This summation thing seems clever but in a large code base you probably don't want to have a bunch of clever UBs.
Behavior of the code is not supposed to ever change, unless you let the program to cause UB (e.g. give it numbers that do overflow). But you're not allowed to let the program cause UB under any circumstances, ever. So as far as "correct" programs go, it's guaranteed not to change.
UB is about giving freedom to the compiler to assume that certain cases never ever happen, so it doesn't need to worry about them. In C, C++, and unsafe Rust the programmer has responsibility to ensure that UB situations can't happen, so the effect of the assumption can't be observed.
Specifically with this example, if the rust community cared, it could implement pattern detection for this operation and reduce it to a constant-time operation too; the reality is just that nobody cares and this optimization was probably only added to make Clang look better on a specific benchmark.
The other problem with UB is how capricious the specification is. There is no contemporary reason for why signed integer arithmetic is UB and unsigned integer arithmetic isn't. It's just a wrinkle from the past that somehow, some people prefer to praise than to fix.
... on hardware where the native instruction happens to trap.
Otherwise it needs to generate a conditional branch and explicit trap instruction, which is as unoptimal as any other imposed behavior.
Removing code is both among the most sensitive and the most effective optimizations that compilers do, and they use UB as another signal for “unreachable”. The current model of “assume things never happen and don’t plan anything in particular if it does happen” does no one a service.
Conversely, if the compiler can reason that the condition is impossible, it can remove code regardless of what behavior is imposed for the condition that it knows can't happen.
That's not true. Suddenly, almost every single signed integer operation on a machine with different hardware integer widths than the language implementation requires branching to check for overflow.
1. Basically no developer actually wants it to wrap. This is almost certainly a logical bug that just pushes the problem a few statements further into the program.
2. Defining it to wrap means that you need software checks for architectures that don't support hardware wrapping on the same width as the integer type. This means that all programs get slower when targeting these architectures. This isn't a "sliver of performance". It is a software branch at almost every integer operation. This approach is not especially well loved in the C/C++ communities.
Although you can find these kind of examples for a few lines of code snippet, the question is what is the impact on overall program? Nowadays, I guess it has no impact for almost all programs. Because memory access patterns, system call overhead, operating system interaction etc. have much more impact on overall performance compared to optimizations enabled by undefined behaviors.
Optimizations directly enabled by Undefined behavior are only a very negligible part of the performance benefits of the existence of UB.
For example, consider the fact that array access out of bounds is UB. Because of this a compiler can assume (without proof) that all accesses are actually going to be in range. This enables a boatload of loop optimizations.
All non trivial optimizations done by a compiler usually assume such facts.
It's a complete disaster on all sides.
In practice, weird miscompilations due to UB are just slightly more difficult to debug than your usual segfault. You can generally keep reducing your problem to localize the issue in the code.
Also, such issues are not very common because the value obtained from an UB operation is usually nonsense (shifting past bitwidth, out of bounds array element, etc) so a compiler switching things around is just garbage-in-garbage-out. It is of course a serious issue if a program is actually depending on such a value for a crucial operation. That's how you get exploits, with or without the compiler doing something clever.
They're not harder to debug. Debugging them isn't the issue. The problem is knowing that the code in the editor doesn't correspond to the code under execution.
This is probably because they’ve seen the effects of the foot gun first hand. They also recall what C/C++ looked like before the standards bodies made exploiting UB in compilers open season. There’s also a reason why Rust doesn’t have any UB in its safe dialect even though it sits on infrastructure capable of most/all the same possible optimizations via LLVM.
There was in fact a direct kernel security issue UB exploitation caused in the Linux kernel whereas without that optimization there is no security issue. if I recall correctly the code looked something like:
Some_value = ptr->value
If (!ptr) { return; }
This worked correctly in all cases before the compiler added the optimization. It worked less well after because the !ptr check was UB since it followed a dereference. It also wasn’t immediately obvious this code was broken because there was no diagnostic nor any indication that upgrading a compiler would suddenly elide the check. The value-add of such optimizations at scale vs the correctness issues exploiting it causes is questionable.The problem wasn’t that it’s not fixable. It’s that it takes time to find and you may not even find it until after it’s being exploited.
Linus is an important figure, but there are plenty of world experts on the other side of this argument. And there is even a third side that states that not only should the compiler avoid optimizations based on UB assumptions, C implementations should instead function more like a virtual machine for a PDP-11 and have everything under the sun be defined (at great cost of performance).
If you take an especially pessimistic view from the compiler, then really really basic stuff is almost impossible. A write to a dereferenced pointer is almost equivalent to a full program havoc. This invalidates almost all facts that the compiler knows and prevents virtually every optimization near that write. Heck, technically this can interfere with things like vtables and invalidate any sort of devirtualization, which is hugely valuable for performance.
Sure. I agree with you in principle it's cleaner to have the nullptr check before. However, it's also important to remember that UB & the compiler optimizing around it is a relatively "new" phenomenon & the compiler going out of its way to punish you for it (for a perf improvement that likely doesn't matter in many/most cases) seems punative & not helpful to most C/C++ codebases (with the exception of some heavy math or HFT applications that could probably be better suited with their own language/dialect). IIRC the standard relaxed to allow this in C99 & the consequences weren't well understood until about 10-15 years later once compiler authors realized what the standard was allowing them to do. This is also the most innocuous case - there's plenty of even more subtle issues that UB can cause.
While I agree there can be nuance in viewpoints, I'm not sure what the 3 you are talking about are. Generally there's the pro-UB & optimizing assumptions around it which is largely populated by compiler authors & "performance at all costs". I'd say Chris Lattner, Linus & DJ Bernstein hold a largely similar point of view of UB = bad decision by the C/C++ standards bodies & probably only differ on the solution (Chris = "switch to a different language", Linus = "give me back the behavior before UB was introduced", DJB = "define all current UB").
Personally, while I'm a fan of performance, I'm not convinced that UB has proven enough of a performance gain that it's not better as sitting behind a flag like `ffast-math` - most software would benefit from removing most of the UB optimizations & it's not clear to me that they performance impact would be all that significant.
In most media apps, the actually processing intensive part definitely does not do syscalls or OS interaction. It's pure computations for as long as possible (and often non-parallelizable, e.g. x[i] *= x[i-1] sort of things). Disabling those optimisations is a killer.
Pretending that integer overflows don't happen because of undef behaviour is... Funny. Integer overflow do happen a lot, both with signed and unsigned ints. Compilers should only apply radical optimisations in this situation if they can prove an overflow will never happen. Otherwise, they should not touch the loop.
EDIT: typos, rephrasing for clarity
If Rust is too slow (as measured by the profiler… premature optimisation etc.), you can always take another look at the algorithm and optimise it manually, and correctly.
On the other hand, if C is wrong… let’s hope you manage to isolate the fault to this function (a big if!)… start up the debugger… the assembly will likely look correct, because the compiler won’t optimise so aggressively in a debug build! Good luck debugging <3
a.c
int foo(int* ptr);
int main() { return foo(0); }
b.c int foo(int* ptr) { return *ptr; }
c.c int foo(int* ptr) { return 5; }
All three translation units do not have undefined behavior, so no compiler should report it.At the time of compilation of a.c, foo does not have a definition. If it is linked with b.c's object file, the program would have undefined behavior. But if it is linked with c.c's object file, there is no undefined behavior. Object files can't tell who they're going to be linked with.
And when b.c is compiled, foo only exhibits undefined behavior if somebody else calls it with an invalid pointer. It's not b.c's problem.
Further, a.c and b.c might be compiled with different C compilers(very common), so you can't rely on any kind of caching of type information or state-tracking across translation units.
When the linker is run, the undefined behavior is instantiated to create an incorrect binary, but linkers aren't regulated by the C standard so it wouldn't be appropriate to include a UB checker in one.
To do what you're saying, you'd need a language designed for whole-program analysis. Rust and Haskell do this, and they both require you to have source code for all your dependencies so they can recompile them each time. It'd be quite the design change for C, and not something compiler writers can "fix" by themselves.
The idea that you can just stop writing code with undefined behavior is absurd.
If this is true, then this should also apply to Rust or other proven languages: your program will still run in a dynamic environment, and probably calling into all kind of OS and third party libraries. Unless... you write your own (unsafe) wrappers around libraries, trying to catch, as much as possible, errors that can filter into the Rust (or whatever) code. But still, someone has to write these wrappers. If it's not you, you have to trust on somebody else's work, and this is a runtime check that will slow your program down.
Windows has different requirements for invoking foreign code vs. Linux for example.
The foreign code may have requirements on how it should be called, but it is not C's undefined behavior to invoke an OS system call incorrectly because the OS is not regulated by the C standard.
You can formally prove that a program has no undefined behavior, and it can still have lots of errors when actually run.
Dynamic linking vs. static linking is irrelevant.
The normal rules the optimizer works from assume there is no UB. It's just always the normal rules, and they work the same way for all programs.
MichaelBurge's comment has illustrated this better than I could.
So I'd happily take more performance for code without UB trading off slight inconveniences for debugging UB issues.