10 ms·
First: I do dislike how hard it is to avoid some UB / how impractical some of the rules are. But I also think a lot of discussions of this topic caricaturize c
by anarazel 5y ago
First: I do dislike how hard it is to avoid some UB / how impractical some of the rules are.
But I also think a lot of discussions of this topic caricaturize compiler writers to a ridiculous degree. Almost describing them to write optimization passes looking for UB so they can over-optimize something, while cackling loudly in glee about all the programs they can break.
The set of people doing so overlaps with the set of people complaining that the compiler doesn't optimize their code sufficiently to a significant degree.
Lots of compiler optimizations need to know the ranges of values, hence logic to infer ranges. One of the sources for that is "can't happen" style logic - which nearly all of the time are things the code author would agree with if they thought long and hard. Not just about the code as written, but also good the code looks like after inlining (across TUs with LTO).
- rectang 5y ago> But I also think a lot of discussions of this topic caricaturize compiler writers to a ridiculous degree. The inflamed backlash should tell you just how damaging it is to impose silent failure on meticulously written, previously fine programs.
- jcranmer 5y ago> meticulously written, previously fine programs With relatively few exceptions, if your program hits undefined behavior, then your program was already doing something pretty wrong to begin with. Signed overflow is a poignant example: in how many contexts is INT_MAX + 1 overflowing to INT_MIN actually sane semantics? Unless you're immediately attempting to check the result to see if it overflowed (which is extremely rare in the code I see), this overflow is almost certain to be unexpected, and a program which would have permitted this was not "previously fine" nor "meticulously written." I feel compelled right now to point out that software development is a field where it is routine to tell users that it's their fault for expecting our products to work (that's what the big all-caps block of every software license and EULA says, translated into simple English).
- anarazel 5y ago> in how many contexts is INT_MAX + 1 overflowing to INT_MIN actually sane semantics? Unless you're immediately attempting to check the result to see if it overflowed (which is extremely rare in the code I see) It's not that rare - I know that postgres got bit by particularly that issue, and several other other projects as well. Particularly painful because that obviously can cause security issues.
- rocqua 5y agoI recall a bug-report discussion that I sadly have never been able to find. It contains a pretty bad side-effect of this. It had code like: int *p; // lots of code if (p != NULL) return 1; // use p Then a later refactor wrongly added a single line before the if statement: int *p; // lots of code int a = *p; if (p != NULL) return 1; // use p This meant the null check was optimized away, since de referencing a null pointer is undefined behavior, so the if-statement can be assumed to be always false. This then lead to actual errors (perhaps even an exploit, I do not recall) arising from the removed null-check. I think in general the "sanity check" cases are the worst. It is hard to determine whether an expression causes undefined behavior if you cannot try and evaluate it. Perhaps a compiler intrinsic that checks (at runtime) whether an expression causes undefined behavior could be useful here. Though I can imagine such an intrinsic being essentially impossible to implement.
- tom_mellior 5y agoThis was in the Linux kernel, which is compiled with special kernel flags which make dereferencing null pointers legal. In the context of that code, dereferencing a pointer and later checking it for null was absoutely meaningful. Optimizing the later null check was a compiler bug that was acknowledged and fixed. The compiler here didn't respect the semantics it had promised to kernel code. This was entirely uncontroversial; it was not a case of "unwanted optimization based on undefined behavior", it was a case of "compiler bug breaking well-defined code". Again, all this in a kernel context. In user code GCC will still happily remove the null check because in user code this is an actual bug in the user's code.
- 5y ago
- vinkelhake 5y agoIf the program was previously "fine" on version x.y.z of some compiler, then it is most likely still fine on it. That's the target that the program was written for. There's some disagreement on whether you can call a program "fine" that breaks after switching to a newer version, or a different compiler. I see a lot of programmers out there that unfortunately use the behavior of their code on whatever compiler they're using at the moment as a proxy for what the language actually guarantees.
- IcePic 5y agoWell, I can imagine a program having something where new C comments ( // ) makes it not divide, like: a = 5 //* junk here */ 2 ; I'm sure there are ioccc or underhanded C contest entries doing this to make code work differently on compilers based on if // is starting a comment line or not. Sure it is an ugly way of writing stuff and you'd be hard pressed to find lots of real world traps like this, but when/if you did have code that "suddenly" miscompiles you might actually think your old code with an old compiler did work, and a new compiler for "the same" language breaks your program. I don't think everyone code base should need full rewrites ever time a new compiler comes out.
- mjw1007 5y agoI agree. I don't have much sympathy for people who were doing things like writing multithreaded programs in the days before C documented its memory model and then becoming unhappy because new optimisations that legitimately help single-threaded code broke their programs. In my experience C compiler maintainers have generally been open to the idea of offering guarantees beyond a narrow reading of the standard, but they want to be able to clearly state what it is that they're guaranteeing. "Keep old programs running" isn't enough. I think the "Prevailing interpretation" that Yodaiken complains about is coming from the same place as suspicion of the "be lenient in what you accept" IETF principle: that sort of thing doesn't lead to robustness in the long run. The way forward at this point is surely to define more things that are currently undefined (whether in the standard or by widely-implemented extensions).
- a1369209993 5y ago> the same place as suspicion of the "be lenient in what you accept" IETF principle No. It's fine (arguably desireable, but reasonable people might disagree) for implementations that encounter undefined behaviour to terminate execution immediately, especially if it's with a error message. The problem is when implementations silently willfully misinterpret what they accept, particularly in ways that cause (not "expose"; reading from a null pointer and discarding the value (for example) isn't) security vulnerablities.
- dooglius 5y agoThe caricatures are somewhat accurate though, optimizations that look at UB adversarially are never anywhere close to justified. > The set of people doing so overlaps with the set of people complaining that the compiler doesn't optimize their code sufficiently to a significant degree. There's no contradiction here, and the overlap is generally just "people who care". The optimizations that are not safe shouldn't exist, and the optimizations that are safe should be good. > nearly all of the time are things the code author would agree with if they thought long and hard I highly doubt this is the case for even one situation.
- pornel 5y ago> optimizations that look at UB adversarially The whole point is that there isn't such adversarial thing like "we're going to find the UB right there, won't even print a warning about it, and mess up your crappy code, haha!" Optimizers aren't reasoning about code like people do (start to finish with high-level understanding of the whole function), but rather as series of mostly dumb, mostly isolated small passes, each pass changing one little thing about the code. It just happens that one pass marks certain instructions as "can't happen" (like the spec says), then another pass simplifies expressions, and then another pass that removes code that doesn't do anything, usually left over from the previous steps. They sometimes combine in an "adversarial" way, but individually each pass is justified and necessary. Compilers already have lots of different passes. Splitting optimizations into passes is a way to keep complexity closer to O(n) rather than O(n^2), but this architecture makes interactions between passes very delicate and difficult to coordinate, so it's difficult to instrument the data to avoid only cases of annoying UB without pessimizing cases that users want optimized.
- MauranKilom 5y agoName one such optimization. We'll be happy to refute your points for that one.
- bigcheesegs 5y agoCompiler dev here. Do you really think we just come up with code transformations and add them to the compiler just because? All code transformations have a compile time cost and runtime perf impact. We don't add transformations unless the runtime perf impact greatly outweighs the compile time cost. These optimizations are added because they measurably improve the performance of real code. This comes up in every review for new or updated optimization passes. This claim that they aren't justified is actually rather insulting to the effort put in to improve perf without taking days to compile.
- vyodaiken 5y agoThe current situation is not good for compiler writers either. But nobody has ever shown that either C programmers want to sacrifice safety for "optimizations", or that these UB optimizations actually improve performance of anything.
- ynik 5y agoWhat do you mean by "these UB optimizations"? C is a low-level language; it's basically impossible for a compiler to reason about the code unless it makes certain assumptions. It needs to assume the code is not self-modifying to do pretty much any code-generation more intelligent than a macro assembler. It needs to assume the code isn't messing with the stack frames/return addresses in order to inline functions. It needs to assume the code isn't using an out-of-bounds pointer to access a neighboring local variable, so that it can move local variables into registers. "gcc -O0" is a good approximation for the performance you get if the compiler isn't allowed to optimize based on UB. Yes, that means C without optimizing without UB is slower than Java. Optimizations need some form of reasoning about what's happening. For Java it's optimizing based on guarantees provided by the language (there's no raw pointers that could mess with the things listed above). But C doesn't provide any hard guarantees, so instead it needs to blindly assume that the code will behave sanely. Also note that for many of the more manageable sources of UB, most compilers provide a choice (-fwrapv, -fno-strict-aliasing, ...). Yet few projects use these options, even when they use other gcc/clang-specific features. Doesn't that indicate that C programmers indeed want to sacrifice safety for optimizations?
- gpderetta 5y agoExactly. For example there were programs 30-40 years ago that relied on exact stack layouts. These days everybody would agree they are completely broken. The issue of course is that it is extremely hard to write programs that have no UB. It would be nice for compilers to have an option to automatically introduce assetions whenever they rely on some UB-derived axiom, basically as a sort of lightweight sanitizer. In fact if we had sanitizers 30-40 years ago probably things would be better today.