UB means the compiler is free to perform whatever optimizations it wants. Are you really saying that no optimizations are allowed once the compiler notices UB?
Michael Chastain (GDB developer, one of the few people with commit rights to the root of Google's monorepo) told me about the worst bug he ever helped a Googler debug, eventually boiling it down to:
static const double y = 1.0;
static const double x = 2 * y;
printf("Before\n");
if (x > 0.0) {
printf("x\n");
} else {
printf("Not x\n"):
}
printf("After\n");
It was printing "Before\nAfter\n", with nothing in-between.It turns out that the value of x depended upon the initialization order of statically-scoped doubles. There was no malicious optimization in GCC that got rid of both branches, but both branches got optimized away through a series of intermediate steps where the compiler basically split the control flow graph into something equivalent to:
static const double y = 1.0;
static const double x = 2 * y;
printf("Before\n");
if (x > 0.0) {
printf("x\n");
}
if (!(x > 0.0)) {
printf("Not x\n"):
}
printf("After\n");
GCC determined that x was statically determinied and UB. In both places an optimization pass determined that the fastest choice for if(UB) is to branch over the code in question. Since it's UB, the compiler wasn't required to pick some self-consistent value for x. It's free to optimize away all if(UB) blocks to if(false) blocks.(Note that this particular case of UB is specific to floating point types and would not occur with integer types. I believe the intention of the spec is to avoid the requirement for floating point hardware to behave identically, including the state bits that affect rounding etc., to behave identically on the machine where the compiler is run and on the machine where the program is run. My naive hot take would be that this should be ID, not UB, but I'm sure the standards committee put a lot more thought into this than I have.)
It wasn't some malicious or two-clever optimization. It was the emergent behavior of a series of optimizations, each one correct.
Anyway, long story short, once the compiler notices UB, all optimizations are on the table. You seem to be implying that compilers need to be conservative when they detect UB. Compilers are generally recklessly aggressive in optimizing when they detect UB.
Static initialization is such a bug magnet in general. Can make builds ”work” randomly, when static object constructors are ran in more or less random order, decided by the compiler. Change something unrelated and kaboom.
(Needless to say, that statically initializing anything with a complex constructor is a bad idea, especially if it refers to another statically initialized object.)
ETA:
from https://en.cppreference.com/w/cpp/language/siof
"The static initialization order fiasco refers to the ambiguity in the order that objects with static storage duration in different translation units are initialized in."
"Within a single translation unit, the fiasco does not apply because the objects are initialized from top to bottom."
At least in C89, floating point literals aren't constant expressions, and static initialization order is only defined for constant expressions.
I distinctly remember getting burned by this in 2003. GCC was acting sanely, but Sun Studio was initializing two static const floats in successive lines in the same file in reverse order they were defined, resulting in 0.0 for the product of two nonzero constants. Tracking that down was rather painful. Switching the file to C++ solved the issue, but this was radio network simulation code that took several days to run on my client's $250,000 Sunfire V1280 with 96 GB of RAM (quite a large amount of RAM for 2003), and Sun's C++ compiler generated slower code for this same file. I presume some amount of monkeying with compiler flags would have brought the C++ performance back in line with the C performance, but I had more important things to do.
So, I changed the static const doubles to #defines and that also fixed the bug.
In C89, floating point literals aren't constant expressions, and static initialization order within a compilation unit is only defined for constant expressions. I believe this is also the case for C99, and things get much more fuzzy for me for C standards after C99.
As stated in another comment, I got burned by this in 2003 in porting some very large network simulation from Linux on x86 to run on Solaris on my client's $250,000 SunFire V1280. GCC was initializing two const doubles in the file in top-to-bottom order, but Sun Studio was doing the opposite. Switching the file in question to C++ solved the bug, but Sun's C++ compiler generated slower code in this case. So, I changed the static const doubles to #defines to fix the bug.
My statement is specific to how data is shared across multiple threads and does not generalize to any other situation, such as the one you presented. When threads are in play a compiler typically cannot know if there is UB or not (since the access is often done across different translations units).