Originally posted by coder
View Post
Code:
static volatile int x; int a = 42; init_hw( &x ); for (int i = 0; i < 10; ++i) { a *= x; } release_hw( &x ); return n;
Writing a decent optimizing compiler is hard enough as it is, but it becomes infinitely harder to do so correctly when the compiler (a) ignores the directives that it's given because this month's rewrite of the optimization pass failed to consider them properly, and (b) the constant churn *of* that optimization pass makes the reintroduction of such bugs a seemingly frequent event.
C is a pretty stable language, and even C++ "mostly" is. Nearly all compiler work is nothing *but* rewriting the O pass, and it's fairly evident from GCC's changelogs and errata that well over half the bugs in any given release are directly related to that rather than the basic codegen. Honestly, I think it's more realistic to be impressed that even "just" -O2 generally works much much more often than not - but ultimately you're playing a game of probabilities. To bring this slightly back on track, -O3 is really just saying you're okay with going from e.g. a 99.9% chance of the compiler not messing up to a 99% one. As with gambling, viruses, contraception, and everything else though, the more you keep rolling the dice the more likely it is that at some point your luck will run out.
When a given project is not just multi-platform, but also spans multiple GNU/Linux stacks because it's embedded across a whole bunch of different SKUs, you will invariably end up having to build with what is really at least half a dozen different compilers. Every single one of them needs to "work", and IME for any nontrivial project the chances of at least *one* of them having a bug in the optimization pass is very close to 1. (IME it's actually exactly 1, but maybe I'm just cursed. :P). You can have 15 dependencies plus all of your own code that all works on 6 platforms, but the 7th one is an ARM box with no fp support, and the compiler there generates bad code for a specific function in one of the libs you're using if it's built at the library author's default -O2.
Again, this is "things that actually happened" rather than theoretical scenarios. Validating the toolchain for that device has been the work of at best a different department and often a different company entirely (generally in China, to add language barrier and timezone headaches into the mix) for several months, and changing it is simply not possible.
> I'm not arguing what the compiler should do - just trying to imagine a case where it could safely ignore volatile.
I understand *why* you're thinking down that road, because yes, it's a fun puzzle in the abstract - but the answer, *really*, honestly, truly, is "there aren't any". Just, never.
If you go back to simpler times, say, the DOS era, long before ASLR etc, you (that is, "the developer") could legitimately know that a program always loads at address x, and its DATA segment is always at address y, and you would be justified in (ab :P)using that knowledge to have e.g. a parent process reach in and modify some region of the child (say, the first static in the unit containing main()) as an IPC mechanism. No matter what kind of batshit-crazy scenario you can imagine, someone will absolutely have done it somewhere at some point.
> That's weird.
Yes, but also no. Ultimately, it's "just" a "simple" bug: something that "shouldn't have" happened, but did. The guy responsible for the code that broke it, who acked the bug (and did eventually fix it, two point releases later) couldn't provide any way to work around it other than the -O0 I'd already changed it to use - because the whole point of the bug was that the compiler thought it was "helping", when it shouldn't have been trying to.
(Sidenote: Remember that the code is from memory and is an oversimplification of the actual bug that is unquestionably not the exact construct that caused the compiler to trip over its own feet. It is, however, "close enough" to emphasize the real point, which is that the compiler's failure to honor the volatile directive in the first place is what then gave the compiler the *opportunity* to screw up).
> I wrote some moderate amount of C++ with inline asm in GCC 4.3 or thereabouts. Mostly MMX and SSE stuff, when I wasn't satisfied with the code generated by the intrinsics. They were all inside inline functions and compiled with -O3. They're included in regression tests which we've run multiple times per upgrade, even to this very day (currently on GCC 10.2). I don't recall ever having any problems like yours.
Sure. This was *far* from the only asm in the project, and all of the rest of it worked fine. And all of it, including the piece that GCC broke, had worked fine for years before then, and I'm sure would have continued to work for years afterwards once the compiler was fixed. Some bugs are like that, and I think that's especially true for a compiler: c.f. the piece about probabilities earlier on.
> A bug is a bug. But, where the compiler can deliver the same behavior by doing something equivalent to the spec... that's what people actually want. I get that volatile is supposed to be no-go, but otherwise an optimizer's life is spent trying to do something equivalent to what you said.
Absolutely. Instruction reordering happens all the time, nearly always works, and a substantial fraction of developers have no idea it even exists. The thing is, while the compiler is absolutely free to disregard "register" etc (to the point where most modern compilers simply discard the keyword entirely), going in the opposite direction is absolutely forbidden, and the rather crucial "but otherwise" part of your sentence simply doesn't apply. The italicized parts are, by definition, not permitted.
Comment