3 ms·
(out of order quoting in hopes of a better narrative) https://gist.github.com/rygorous/e0f055bfb74e3d5f0af20690759de5a7 https://gist.github.com/rygorous/e0f055
by nkurz 8y ago
(out of order quoting in hopes of a better narrative)
https://gist.github.com/rygorous/e0f055bfb74e3d5f0af20690759de5a7 https://gist.github.com/rygorous/e0f055bfb74e3d5f0af20690759...
A fine link, but oddly one that the two of us have already referenced in a different subthread. As stated there, I think there is room for a non-fwrapv approach that works just as well.
I try to use signed for all _numeric_ values (leaving unsigned for "bit strings"), because I'm quite sure that the number of bugs caused by the discontinuity at zero (for unsigned values) is much larger than the number caused by the discontinuity or UB at INT_MIN/INT_MAX.
I was probably unclear. I don't prefer unsigned to avoid the UB, but to get better generated assembly. While the unnecessary sign extension moves frequently come for free, occasionally they don't. I think the bugs are as likely to bite (or not bite) in either case, and need to be avoided by other means.
To see that with pretty much any program, just compare compiling with and without optimizations, or perhaps compare against a 20 or 30 year old C compiler if you can find one.
I'm less sure of that. You saw, for example, the current thread on RWT: https://www.realworldtech.com/forum/?threadid=182093&curpostid=182093 https://www.realworldtech.com/forum/?threadid=182093&curpost...? I think the situation is rather that recent compilers do a better job with naive code, but are more likely to pessimize consciously constructed code. One major example of this is the most important micro-performance issue I've seen, which is the failure of GCC and Clang to reliably get µop macro-fusion bottom of loop branches. ICC does a much better job of keeping a decrement to zero tight to a fused jz, making the loop body one µop smaller.
These people are smart enough to get us into this
situation, so one would imagine they'd be smart enough
to least discuss this better way if it exists.
I lack that belief, but I'm not sure if that makes me more or less cynical than you. I think the issue is that almost everyone involved in standards process is likely to share a set of core philosophical beliefs about programming that may be self-consistent but not cover the full range of possibility. Consider as not quite parallels who becomes a US Supreme Court Justice or military commander, and how the selection process of being appointable affects the range of ideas espoused.
So what is this better way?
It might not be the solution, but I think that what you are not seeing is that there are other interpretations of "implementation defined" besides "always wrap" or saturate. You (and I think the standards committees) seem to want all non-UB programs to be fully deterministic, such that an emulator can always be written that will produce the same results as a real processor. I think there's room for an approach that says "if you stray beyond these rules, the real processor will decide your fate" without the "electric fence of death" approach currently taken by UB. I might be wrong to call this "implementation defined", but I think that's where it best fits in.
Consider a simple example: deallocating (e.g., free() in C, delete in C++) the same memory twice is UB, as is deallocating memory that was never allocated.
Let's use that an as example. What I want is for a standards compliant compiler to adopt this attitude: "It is unpredictable what happens if you call free twice in succession on the same pointer. Our compiler will try to issue a warning if it sees this happening, but the generated assembly is guaranteed to call free() twice. What happens after that is between you and your god."
I don't want is for it to be a guaranteed no-op. Nor do I want it the compiler to eliminate code based on the assumption that the second call never happens. I want it to warn if it can (which can be a fatal warning if desired) but to always generate assembly that makes the potentially crashing call. My contract with the compiler is limited to determinism of the assembly it generates on my behalf, and does not include any guarantees or restrictions on the runtime behavior.
My worry is that you (and others) don't consider this to be a standards compliant interpretation of "implementation defined", nor can it be "undefined behavior" if the compiler also reasons that UB can be assumed to never occur. Despite this, I think it would make the frequently make the compiler into a more useful tool.
- BeeOnRope 8y ago> As stated there, I think there is room for a non-fwrapv approach that works just as well. Maybe I am missing something, but I feel the compiler is doing exactly what you are suggesting in that subthread, and I replied in that vein. Perhaps I am misunderstanding what you are suggesting. > I was probably unclear. I don't prefer unsigned to avoid the UB, but to get better generated assembly. While the unnecessary sign extension moves frequently come for free, occasionally they don't. That seems backwards. Due to this "issue" the suggestion is that signed often generates better assembly, because the compiler can reason that overflow doesn't occur. With unsigned, the compiler must generate code that accommodates wraparound, since it is allowed to occur. For example, I would have expected the example code in the link to compile to worse code if an unsigned loop counter is used, since wraparound is legal and so the compiler will have to zero-extend each value. As it turns out it doesn't, since of course the compiler can additionally prove that if wraparound occurs this particular loop won't even be entered (since the condition is `i < count`). This proof is quite fragile however, if the loop condition were different or if any other math occurs on index it would break as in [1]. So it is not the case that compiler exploitation of UB simply brings signed level with the performance of unsigned, but that it can make signed faster, and that's how it is usually presented/argued. Now I'm certainly not saying it is always true that is signed is no slower than unsigned: signed has the advantage of no-wrapping semantics, while unsigned has the advantage of zero extension being free in some scenarios (on some platforms) since it can be rolled into some other operation which implicitly zero extends, which is what I think you were referring to [2]. Which one has an effect (or greater) effect depends on the code. > I think the bugs are as likely to bite (or not bite) in either case, and need to be avoided by other means. Definitely not. Code with unsigned is always just one subtraction away from a huge (nonsense) value. That issue is a real one that occurs all the time, and why even the C++ folks (who ISTM rarely admit errors) generally acknowledge that using unsigned for things like container sizes was an error (albeit an uncorrectable one). Signed values on the other hand, only overflow if you actually have a gigantic value that exceeds 2^31 or 2^63 or whatever. In many cases such large values cannot occur due to range of possible values (i.e., the meaning of the values), and especially with 64-bit values are unlikely to occur outside of malice. Furthermore, unsigned values are still subject to this type of overflow - but at 2^32 (2^64) rather than 2^31 (2^63). I agree though that you should still use other mechanisms to avoid overflow even with signed values, but there is benefit to defense in depth. > I'm less sure of [compiler optimization having improved spectacularly]. You saw, for example, the current thread on RWT: https://www.realworldtech.com/forum/?threadid=182093&curpost... https://www.realworldtech.com/forum/?threadid=182093&curpost...? I think the situation is rather that recent compilers do a better job with naive code, but are more likely to pessimize consciously constructed code. I'm not saying compilers are infallible. I am definitely in the camp that compilers have a long way to go. I believe optimization has improved dramatically in the last few decades. Perhaps "spectacularly" was too strong. This RWT example doesn't seem to contradict this: this is code that should have been well-optimized even 20 years ago. It is not in the set of things that needed to get better, so it should stay about the same. Due to some flaws in gcc, it actually has gotten worse. It is is large, complex code with lots of abstractions where optimization has gotten better. You could try -O1 vs -O2 also for a kind of litmus test of the power of optimization. It's not an exact analogy, but -O1 code isn't that far off what older compilers could do, as many of the expensive optimizations enabled by -O2 are also the more recent ones (not coincidentally, since increasing hardware speed has made these palatable). On some code it makes no difference, and you'll even find cases where -O1 is faster. On lots of code -O2 is faster, and in some cases dramatically so (10x, etc). In fact, if you run this example on x86, you'll probably find a big difference between -O1, -O2 (on clang) and -O3 (on gcc). In any case, I don't think we even need to agree on whether optimization has improved, and by how much? It's not really central to the discussion. We just need to agree that optimization is an important part of C/C++ compilers, valued by many users and a key criteria of evaluation between compilers. I think that is not controversial? > I lack that belief, but I'm not sure if that makes me more or less cynical than you. Keep in mind these discussions involve not just compiler writers but any interested parties, including people who strongly object to the current behavior. So even if don't believe the compiler writers can see a solution due to some type of institutional myopia, where are all the clever suggestions from outsiders? How come every time this comes up the most popular opinion is some solution-free observation about how insane this is rather than "this is obviously how it should be done"? I put it to you that it is because there is no great solution waiting in the wings, and the three most sane solutions (UB, wrapping, trapping) are already implemented and selectable by the user. > I think the issue is that almost everyone involved in standards process is likely to share a set of core philosophical beliefs about programming that may be self-consistent but not cover the full range of possibility. I think the standards committee is only tangentially involved here. The primary way the standards committee is involved is that they defined signed overflow as UB, but that was almost certainly because _different machines implemented signed arithmetic differently_, not out of some desire for the occasional no-overflow optimization. Years later the compiler writers "abused" this to implement this type of optimization. So it is more about the compiler writers, IMO and whether their choice(s) are sane (see above). The standards committee may become involved again, primary because they may remove the compiler's license to optimize this as UB by defining the behavior, and I hope to see that in my lifetime. > It might not be the solution, but I think that what you are not seeing is that there are other interpretations of "implementation defined" besides "always wrap" or saturate. Yes, I am not seeing them, but I am open to them! In fact I am practically begging for someone to explain them to me (in a patient and precise way, not going silent when it becomes clear that the nugget of the idea they had actually doesn't survive when fleshed out). Also, to be precise, I do see other ideas, like: - Trapping on overflow. - Applying a heuristic to determine whether the user is actually anticipating overflow and allowing it to occur in that case, which would "fix" the overflow check case. The first is already implemented and selectable, as far as I know, and the second seems likely to be worse than the other solutions, extremely controversial and very hard to implement. > You (and I think the standards committees) seem to want all non-UB programs to be fully deterministic On the part of the committee, I would say "definitely not", since otherwise they wouldn't have implementation defined behavior. From my part, I would say "Yes, that is obviously highly desirable!". I definitely want a deterministic, portable language. Anything else seems like a big mistake. Only C and C++ really deviate from this norm and mostly all in the name of performance. Weird behavior in the name of performance... somehow it seems like we are reversing our positions here? One thing I'd like to see the standards committee do is have more implementation defined behavior, and feature-test macros to test it. The current implementation defined behavior is kind of useless to actually consume because you are supposed to hunt through the documentation for each compiler (if it even exists) and find what you want and hope it doesn't change in the future. You could have fine-grained test macros, but it would even be great to have something like TWOS_COMPLEMENT_NOT_INSANE that means the platform has 8-bit bytes, two's complement signed and unsigned values, and everything behaves "as expected" in terms of conversions between them, etc. Since pretty much every platform today has this, you could just have a: #if !TWOS_COMPLEMENT_NOT_INSANE #error "Sorry, this platform is to weird to support" #endif in your project and then basically write in a narrowed version of C/C++ which meets many more expectations of how things should work. --- [1] https://godbolt.org/z/BLsXJ5 https://godbolt.org/z/BLsXJ5 [2] Other than this example, it is obvious that signed or unsigned can be slower than the other in cases where you are mixing and matching signedness within a loop in an inefficient way, especially when combined with mixing lengths (e.g., 32-bit vs 64-bit) as well.