More specifically, the problem is that processing this operation is not producing error; it instead propagates the problem and produces invalid code. That's a situation where nobody wins.
I think we'd be better off if many undefined behaviours were instead implementation defined. If, instead, LLVM had converted the double to some integer and then chosen the lowest 8 bits from that integer, we'd have gotten some random value - but at least the comparison wouldn't have been ignored. The comparison should only be ignored if the compiler can prove that the value definitely lies within the range; if it isn't sure, it can't remove the comparison. The programmer wrote it for a reason; ignoring the intent of the programmer is a bug.
On strict aliasing, I'm against it without explicit opt-in over a delimited subset of source code. I understand that using & is going to harm the performance of my code; I think that's an acceptable tradeoff for more predictable behaviour. If it harms the performance of C++ vector iterators, for example, I don't care: strict aliasing rules are a worse cure than the disease of C++'s poorly thought out abstraction tools. Let a subset of the program follow Fortran rules if it's required.
I believe that you and others think that's an acceptable tradeoff. At the end of the day, though, most people want C compilers to produce the fastest code possible. Compiler authors are just responding to what users want. The example below is actually an excellent example of this.
> If it harms the performance of C++ vector iterators, for example, I don't care: strict aliasing rules are a worse cure than the disease of C++'s poorly thought out abstraction tools.
It's not just C++. Consider (from Chris Lattner's blog post):
float *array;
void zero_out() {
for (int i = 0; i < 100; i++)
array[i] = 0.0;
}
People saw compilers missing the optimization to compile this to a memset, and filed bugs against them. As a matter of fact, though, that is an illegal optimization to make unless the compiler is able to take advantage of strict aliasing. That's because, absent strict aliasing, array[0] could legally be a type-casted pointer back to "array" itself, even though almost nobody would actually write that code.The reason why compilers implemented strict aliasing rules is that their users demanded that they fix "optimizer bugs" like the one above. And, honestly, I can't blame either the compiler developers or the users. Strict aliasing ends up being one of the things that's often important for performance. The problem, if anything, lies with the C language.
I feel like a new systems language needs to come and make aliasing explicit in a way, so that it doesn't surprise programmers but still allows them to get the optimizations that they want. Tracking uniqueness like Rust is a good first step, but it doesn't work for unsafe code or anything with more subtle aliasing relationships.
I think it could be added to Rust. :)
Uniqueness can be viewed as just a starting point. It's a conservative subset of more sophisticated systems like PALE. Since it's a subset, it could be expanded to the full system eventually.
I think the biggest problem is not implementing it or finding a way to slot it into the language design but rather making it easy enough to use to achieve widespread adoption. Rust already stretches the boundaries of programmers' willingness to learn new type systems/static analyses to control aliasing, and it has, as you point out, a very simple system. In fact, I think it's unproven that programmers will be willing to adopt such a system en masse at all†, since Rust is still niche. If and when Rust really takes off in widespread production, we'll have proof that programmers are willing to adopt aliasing information in the type system.
† It'll be interesting to see how the ISO Core C++ lifetime checker fares here. I'm not optimistic about how much use it will get due to complexity and effectively being a different language from C++, but I'd love to be proven wrong.
Separate translation units. For this to work, you'd need to postpone all optimizations to the link stage. Even then you're not safe, because you may be producing a dynamic library and the program loading it can take the address of an object by dlsym().
- array pointer points to pointer itself, then the pointer is set to NULL pointer (assuming architecture where float 0 and NULL are represented in memory by zero), and derefencing null pointer is an undefined behaviour
- array points to misaligned itself (possible on x86), then there is a write outside of variable bounds, and undefined behavior
- array points to part of zero_out or memset function code, however the function is constant, and cannot be changed, and this causes undefined behavior
In each of these cases, there is an undefined behavior not depending on strict aliasing.
It could. That is indeed how Rust, for example, defines it.
But in C "unspecified values" have a way of turning into undefined behavior really quickly.
Here's an actual bug we have in Rust [1]:
Why does that segfault instead of emitting a safe panic? Because the IR looks something roughly like this (in C notation): The optimizer correctly determines that the cast from double to uint8_t results in an undefined value. In LLVM terminology, undefined values can arbitrarily change value over their live range, and this is allowed per the C specification. So LLVM is allowed to replace the conditional "index >= array->length" with "false", and this is indeed what it does, leading to this code: And with that, LLVM optimizes out our bounds check, and safe code is able to perform arbitrary memory reads and writes. Note that all we needed was an unspecified value resulting from a floating point cast!The fix is probably going to be to stop using LLVM's floating point conversion instructions and to instead use some "safe conversion" intrinsics that we'll define.
> What compilers do today should be the recourse of absolute last resort, and the sort of thing that makes its authors feel bad. But it seems to be treated as a matter of course. I don't know what to say.
You might think that I would think this way too after fighting bugs like the above, but I actually don't. I appreciate the performance that undefined behavior brings to C, and I think we'd have much slower programs without it. Undefined behavior resulting from strict aliasing is the reason why a for loop setting an array to zero can optimize to a 10x faster memset, for example. And heavily inlined series of functions, as well as generic libraries like the C++ STL, frequently require these optimizations to get reasonable performance. I think it's not an exaggeration to say that much of the software performance we take for granted—stuff you care about, like codecs, games, browser implementations—is the result of optimizations that are enabled by undefined behavior.
[1]: https://github.com/rust-lang/rust/issues/10184