13 ms·
I’m amused by the people who ask for optimization and then complain about it. Or the “it did what I said, not what I meant” crowd. But, officially, undefined b
by maxlybbert 5y ago
I’m amused by the people who ask for optimization and then complain about it. Or the “it did what I said, not what I meant” crowd.
But, officially, undefined behavior is always undefined, not just at higher optimization levels.
- vgatherps 5y agoThis is a pretty dismissive response to something that's a real problem. Sure, the "Why are you deleting my checks for if *this is null" is a little silly - but there are definitely sharp edges where UB conflicts with actually useful things you might want to do. Did you know seqlocks are undefined (benign race conditions)? Ever ran into padding concerns playing poorly with trying to do atomic CAS? It's not unreasonable for the standard to say 'padding is undefined', 'data-races are undefined' - but having no way to say "hey, trust me, please un-poison this thing you don't like" is pretty unfortunate.
- jcranmer 5y ago> Did you know seqlocks are undefined (benign race conditions)? It wouldn't be undefined behavior if you used atomic variables--data races involving atomics aren't undefined behavior.
- vgatherps 5y agoThat is true in theory, but in practice it's not feasible. Even assuming you restrict to trivially copyable types, you either have to have a purely-atomic duplicate of anything that you want to store, or play games with trying to get a fast atomic memcpy. Unless things have changed a lot in the past two years neither LLVM or GCC do that much optimisation around atomics so this comes with disastrous performance implications as well as overhead battling the standard.
- gpderetta 5y agoYes, a fast and safe seqlock is still elusive. We will hopefully get the misleadingly named std::atomic_memccpy at some point in the future.
- klodolph 5y agoI know this is not relevant to your point... but padding in C is not "undefined". The amount of padding is implementation-defined, but it can only occur in specific places. The value of padding bits is unspecified. "Unspecified" in the standard means that it can be various different things, but it has to actually be one of those things. As opposed to undefined behavior, which is more of a "poison value" that trims paths from your CFG.
- vgatherps 5y agoAh yes, that's correct - I believe that reading and comparing the padding bytes results in undefined behavior - the bytes might not actually be initialized depending on the object class. I also don't know if you can actually use the result of comparing an unspecified/implementation defined/indeterminate value - that's where poison is generated. In practice (UB aside), this is basically fine in the context of a read, compute, CAS loop. Those bytes do have some value in the machine and if that memory isn't written they won't mysteriously change. It's playing games with the optimisers and UB however. You might be able to get around this by first initialising the bytes to zero, then in-place copy constructing whatever you want? I wouldn't bet anything serious on that being defined though.
- klodolph 5y agoThe poison is only generated with indeterminate values in objects with automatic storage duration (local variables on the stack). Padding bytes in other circumstances will have specific, valid values and can be compared. (An "indeterminate value" is an unspecified value or a trap representation, unsigned char does not permit trap representations, and unspecified values are "valid values".) Use of other unspecified values results in "unspecified behavior" which does not result in trimming the CFG. I think the spec is written this way to codify how compilers are allowed to do liveness analysis... a local variable is marked as live when it is initialized, and if a variable is not live, its storage (registers or stack) may be used for other variables or temporary values. You then get UB because, for example, two comparisons may result in contradictory outcomes, because the value has been overwritten with garbage between the two comparisons. Or in a more modern compiler, you read from an uninitialized local variable, and the compiler trims the CFG. Here's an example of what I'm thinking of: int x, y; // point A if (condition) { x = 5; } else { y = 10; } // point B A compiler can realize that x and y can't both be live at the same time, and assign them to the same register or stack location. However, this means that if you read the value of x at point A and point B, but x is uninitialized, it will naturally have a different value each time you use it... because the value of Y is overwriting it!
- zaphar 5y agoAlternatively if my compiler has a "run faster" flag I would not expect it to change the "semantics" of my code in the process. Additionally in C UB is often not intentional nor trivial to detect in your codebase since it may be the interaction of two pieces of code that are not anywhere obviously close to each other. There comes a point where faster but broken code isn't better it's just broken.
- maxlybbert 5y agoThe standard committee’s position is that if undefined behavior isn’t easy for the programmer to detect, why would it be easy for the compiler to detect? I’m a little more familiar with the C++ committee than the C committee. The C++ committee prefers to declare something an error than to declare it undefined behavior. They only declare something undefined when they believe it would be unreasonably difficult for the compiler to detect the problem (e.g., using multiple, incompatible, definitions for an inline function; which can happen if you have a macro expanding differently in different parts of the code, or you have multiple definitions for the function, but only one definition is ever visible to the compiler at any moment in time). I’m pretty sure the “signed overflow is undefined” rule is something of a special case: it should be easy to detect when source code doesn’t have a hard coded upper bound, but giving an error or warning in all cases will create too many false positives, and declaring that it wraps on overflow has been deemed unacceptable by the committee.
- GoblinSlayer 5y agoUB problems happen only when compilers detect it, understand it is an opportunity for optimization, because they are allowed to do anything, then do that optimization. When compiler can't detect UB, it actually works as expected: when integers overflow they do exactly that and nothing else, when null pointers are dereferenced they do exactly that and nothing else, when uninitialized memory is read it does exactly that and nothing else.
- foxfluff 5y agoThat's not true at all. Compilers can detect some instances of UB, and they will happily warn when they do. Most instances of UB are ones that the compiler could not detect. Your compiler isn't going to detect that your integer overflows, it assumes that it won't. Shit blows up if that assumption was wrong. If you want to detect UB, you should run ubsan and the like. The compiler is not running it.
- jvanderbot 5y agoNothing in this article convinces me the problem is contained to optimized C. The strict type aliasing is a general problem, right?
- gpderetta 5y agoIn practice GCC won't enable any TBAA optimizations at -O0. There is always -fno-strict-aliasing anyway.
- int_19h 5y agoUB is only UB when it comes to ISO C++ guarantees. The implementations can always make their own guarantees above and beyond that; they don't have to restrict themselves to those parts that are explicitly "unspecified" or "implementation-defined" in the Standard.