> 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...? 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
[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.