Most (all?) modern processors have instructions and flags specifically for querying and reacting to integer overflow/carry.
This seems like a situation where dropping to assembly to perform the addition and testing the flag makes sense. GCC has a mechanism for inline assembly where you list the variables you read from, you write to, and registers you modify and the optimizer plays nice with you. You could provide it as a function to use instead of doing the addition in C.
I have been doing a lot of assembly programming, and I can't help but agree with this. It is crazy how simple x86 assembly language programming makes checking overflow compared to C.
That's the issue: while x86 exposes such operations, not all assembly dialects do. They too have to support C and emulating overflow detection in software would be unreasonable.
It seems to me that it would not be unreasonable. If you need that detection, you need it at any price, on any platform. If you don't need it, meaning, the code doesn't test it, the compiler has still the right to optimize it away.
So it would be implementation defined behavior. I'm fine with that; personally, I think the standard was too quick to assign "undefined" here. But it can't mandate a certain behavior in the standard across all platforms.
> It is crazy how simple x86 assembly language programming makes checking overflow compared to C.
It's easy to write, but not so easy to optimize the performance.
Overflow checking creates a dependency in between operations and requires the emission of instructions that didn't have to be there before.
i += 1
<branch if overflow>
i += 1
<etc>
Means that you are executing more than twice the number of instructions and are getting further behind on the dependency chain each iteration. Eventually, your branch subsystem is completely overwhelmed by a branch that is taken effectively never.
> cmov's can make a difference. You can use them to avoid polluting your branch prediction tables, you can use them to make code smaller, and you can use them when they simply just fit the problem really well. It's just _not_ the case that they are "obviously better". They simply aren't.
The issue isn’t what the hardware can do, but rather that the C spec leaves it undefined. Over the last decade have increasingly abused this idea basically making the spec a land mine of UB and then claiming that UB allows them to do whatever they want, even when the programmer intent is obvious.
That said in modern clang and gcc you can (and should) use the overflow builtins which are all essentially:
Bool __builtin_[operation]_overflow(type a, type b, type* out)
Returning true if the operation overflowed. These produce “ideal” code, e.g
if(overflowoperation(x,y,&out)) ...
Produces the platform branch on overflow instructions.
> Over the last decade have increasingly abused this idea basically making the spec a land mine of UB and then claiming that UB allows them to do whatever they want, even when the programmer intent is obvious.
That is not an accurate or fair assessment.
However signed integer overflow being undefined behavior is a bit over the top given modern hardware. I'd like to see the C standard adopt a policy of wrapping for signed overflow, then either add new operators or functions for trapping behavior.
Most reasonable hardware has the instructions to trap or branch if add/sub/mul/div overflows or underflows so the checks are very cheap. Unlike the early days, in this day if C made it part of the standard processor manufacturers would simply stop selling CPUs without those features. Same goes for two's-compliment integer representations. They already did the same for 8-bit char/byte; other sizes are no longer supported.
There has been an ever increasing creep of undefined behaviour, which has broken existing code - type punning through unions was literally the “correct” mechanism for type coercion, which then became invalid when they established new, questionable, type aliasing rules that rendered it UB. The new type aliasing rules also mean that you can’t implement memcpy/memmove/bzero/memset without hitting UB. That requires hacks in the spec treatment of type aliasing to work around. Hell, even Malloc/free technically is UB due to type aliasing.
Obviously there is signed integer overflow, which has resulted in numerous security flaws for questionable performance gains.
Then there are other fun things, like technically it is UB for bzero to skip padding bytes in a struct (because that is UB), which means bzero(&somestruct, siseof(somestruct)) is not technically guaranteed to clear all of the struct, so I hope there’s not any secrets in left over memory.
> There has been an ever increasing creep of undefined behaviour, which has broken existing code - type punning through unions was literally the “correct” mechanism for type coercion, which then became invalid when they established new, questionable, type aliasing rules that rendered it UB.
Punning through unions is well-defined behavior in C99 and C11. In C++11 and newer, it's questionable, but it works in practice.
Works in practice resulted in the compilers adding security bugs - literally this article is about overflow checks that worked perfectly fine until the mid-2000s when compiler engineers realized they could abuse UB of overflow for “performance” and then a mountain of previously safe code became insecure.
Aliasing queries in the compiler basically work on a tiered system. There's a simple aliasing pass that first checks for really obvious alias information (such as, it's the same pointer, it's a bitcast of the pointer, it's a simple arithmetic expression of the same pointer), possibly some more advanced Andersen's-style alias passes, and only finally is there a last-ditch strict-aliasing based pass.
If you take the address of a float and cast that to an integer pointer to get at the bits of the float (and don't let either pointer escape the function), it will work, even with "abusive" compilers that have aggressive optimization turned on, even if you modify the lvalue in the intervening code. It's literally more work to make the compiler not realize that the two pointers alias, and there's no benefit to pessimizing a trivial query, so I would be shocked if any compiler actually miscompiled that code.
It “working” isn’t the problem, the problem is when the compiler makes use of the “this is UB” to make formerly correct code incorrect.
The definition of UB would allow a type punned union on the stack for instance to be treated as logically a struct - eg changes to the float value would not impact the integer value. While I agree that would not happen in practice today there is no reason it has to continue to work.
The fact that compilers already make sane decisions regarding some UB - evaluation order of parameters used to be unspecified and gcc at least would change evaluation order depending of optimisation level, but all compilers have realized that just insane and so left to right evaluation. Similarly bzero will initialise padding bytes in a struct even though that is technically UB, so could technically be skipped.
Sorry for run on horror sentences I’m typing on my phone :)
Punning through a union is undefined in C++11. I'd recommend against doing it in C99 (and newer) as well, even though it has a footnote in the standard saying it's ok, since someone is going to read your code and copy it over to an earlier C project, where it would be undefined. Just use memcpy as intended and let the compiler optimize it out for you.
"I've said this before, and I'll say it again: a standards paper is
just so much toilet paper when it conflicts with reality."
Sadly, the compiler maintainers at some points of time managed to push the real "breaking" changes through (for other UB cases). It seems that the "language lawyerism" is "easier" than the expected approach of the responsible engineers.
I disagree with Linus here. If you're going to go through a complicated set of hoops to make punning work (namely, doing it through a union), what's so hard about using the intended (memcpy) solution? Like, if you really think the standard is that terrible (which isn't what I'm arguing here–I haven't personally made up an opinion on this yet), why not just straight cast and force GCC to adopt a flag that makes your behavior legal?
All half-decent compilers elide a memcpy when you're using it for this purpose; they'll just generate the assembly you "intended" to create. And while I know casting to a char * is legal for all types, I don't believe casting from a char * is legal unless the memory the pointer references is actually the type you're casting to. I'll have to check on this–or if you have a link to the standard on this I can just look at that.
I'm curious, AFAICT Linux uses -fno-strict-aliasing with GCC which promises not to enable strict aliasing based optimizations even at higher optimization levels. But they also rely on the GCC blessed behavior of union based type punning which is redundant in that case.
I suppose union based type punning makes the intent a little clearer (vis a vis memcpy) but that's a weak argument at best.
> The issue isn’t what the hardware can do, but rather that the C spec leaves it undefined.
Eh, the undefined nature of signed overflow in C is a bit of a red herring. The most common overflow bug in practice is actually 100% well-defined and still 100% wrong: multiplying two numbers to feed into a malloc.
In practice, the undefined signed overflow really only comes into play when you're trying to explicitly check for overflow by doing the operation and then seeing if it overflowed. But, as you mention, compilers give you bultins to do this kind of checked overflow which is quite frankly a much easier way to solve the problem, especially since it's more likely to actually get matched to using hardware flag bits to check for overflow.
Yup you are right on that, but the obvious way to check for overflow is UB so gets optimized out. Many security bugs in code people thought was correct. The correct, standard compliant non-UB way to check for the overflow requires a zero check and a division. It’s super non obvious. I like to imagine C committee has made standardized definition for the current builtins but I also have low expectations.
The correct thing to do these days is to use compiler builtins, but assuming you can’t (such misery), the correct thing to do for multiplication involves division (because the performance cost of treating multiplication as fixed width 2s complement is so high ;) )
Specifically for
X = a * b
You have to do:
If (b == 0)
X = 0
Else if (MAX(type of X) / b >= a)
X = a * b
Else
Handle overflow error
That’s a solution of last resort, when all else has failed, an act of desperation: whosoever does that is saying: “I don’t know how to express this in a generic fashion”, or “it’s too much work to do it that way and I don’t want to deal with it”. Since the very reason we get paid as programmers is to deal with it, I argue that such an act constitutes a breach of grounds on which we are being paid.
By generically you mean "in C"? Sometimes the functionality is there, but there's no way to express it in C. Integer overflows are exactly such a case; many processors provide a built-in overflow detection mechanism, but there's no way to access it in C. (It's not even a special mechanism, it's just the command itself, in assembly you clearly see the overflow.) I'd say that using compiler-specific functions (provided they plug into these processor commands) or assembly is exactly the right way to deal with this issue. Yes, parts of the code will be non-portable, but if they're clearly isolated and marked as machine-specific, then why not? The pros certainly outweigh the cons.
I had exactly that happen when compiling PowerDNS: the developers relied on a GCC intrinsic, so I had to embark on an epic journey of building and packaging a newer version of GCC.
On the contrary, compiler built-ins can make your code more portable (across different hw architectures) at the expense of some other compilers.
GCC and Clang have mostly compatible compiler intrinsics and with those two compilers you can compile your software to almost any platform out there.
If you need to get your code compiled on MSVC or ICC or some niche C compiler, you can work around this by adding some kind of wrapper around them with #ifdefs (not pretty).
With compiler built-ins you get SIMD, atomics, bitops (popcnt, clz, etc) that are portable to almost any CPU instruction set out there. Without them, you have to re-write that code for every instruction set you intend to support.
So your options are:
1) Stick to standard C and write code that compiles to suboptimal output code (such as using division to get multiply-with-overflow)
2) Use compiler built-ins and get portability across different HW at the expense of not supporting other compilers
3) Write wrappers around compiler built-ins with #ifdefs to support all the compilers you want (add fallback to standard C if you need to)
4) Write your code using HW-specific intrinsics or inline assembly
Out of these, option 2) will give you maximum portability with least amount of effort. The other options are more effort and the benefits are arguable. If you think there are other viable options, please do share.
A variant of (3) that seems close to ideal is to use conditionally defined functions as suggested, that someone else has already written and generously shared, such as portable-snippets [0]. It has a component specifically to offer cross platform builtins with fallback/emulation on plaforms without direct support.
Thanks for the link, that looks quite useful if compiler portability is needed. It's still more effort than option #2, but quite an acceptable trade-off.
The absolutely unacceptable thing is C still not having standard (optional) SIMD syntax. It at least finally got atomics.
But wait, that's not portable to Keil so you cannot use that ;)
The reaction is, if you dislike fast but non standard code, push for standardization of those features, not begrudge developers for doing the right thing and using them.
I agree that it's unacceptable that SIMD Vector extensions [0] are not standardized yet.
However, they already work acceptably in GCC and Clang. There are only minor differences (e.g. __builtin_shufflevector) but it's usable with very minimal #ifdeffing.
Example:
typedef float vec4f __attribute__((vector_size(16)));
// initializer
vec4f a = { 1.0, 2.0, 3.0, 4.0 };
// literal expression
vec4f b = a + (vec4f){ 5.0, 6.0, 7.0, 8.0 };
// standard infix operators
vec4f c = -b * (a+b); // with --ffast-math, this will be a mad()
It's not perfect but it's quite usable and I've been using vector extensions in C for years.
SIMD is processor specific, and is important only in certain scenarios.
Trading portability for performance is a trade-off that most developers shouldn’t be willing to make if they are open sourcing their software, because it will prevent someone from running their software on that user’s desired platform, thereby showing the user the middle finger. That’s just wrong.
When it comes to anything that isn't embedded work, I only care about platforms that support GCC or Clang. I'd happily take an improvement to my development workflow at the cost of dropping support for fringe users. Making truly portable C or C++ code is an exercise in utter pointlessness, and is usually impractical for non-trivial software, be it a desktop application, an operating system kernel, and a lot of things in between. By marketshare an extreme majority of computers and phones in the world can be targeted with Clang alone. Giving up useful compiler extensions to gain compatibility for a platform used by 0.0001% of computer users seems very pointless. I'd rather be concerned with making my software accessible to those with visual impairments or other disabilities, a way, way huger demographic, one that you can't choose to not be a part of.
Well we disagree vehemently: writing clean C code that compiles with any ANSI C compiler is pretty damn easy. Take BIND for example, a non-trivial piece of software which compiles with any compiler.
Rust decided for overflows to cause panics in debug mode, but continue normally in release mode. They have special function calls for the few times that you actually WANT overflow. This seemed to he the only good option. It allows the programmer to express intent. In the future, if hardware allows for cheap hardware traps in the case of unintentionally overflow, they reserve the right to use them in release mode. C seems pretty hopeless since there is no way to know whether or not the coder intended on overflow or not.
Can the programmer express the intent that this will not overflow? i.e. is it possible to coerce the Rust compiler to optimize (x*2)/2 to x? "Cheap hardware traps" cannot substitute for such a compile-time optimization.
> C seems pretty hopeless since there is no way to know whether or not the coder intended on overflow or not.
Well this doesn't matter: if signed arithmetic overflows the code is undefined, regardless of intent.
In practice comments can express intent, and unsigned arithmetic can with some finessing produce most codegen. Explicit operators would be a big improvement though.
I don’t know much about rust specifically, but the overflow traps happen in debug (i.e. -O0) builds, and are not emitted when optimizations are enabled.
LLVM’s constant folding and canonicalization passes will convert (x * 2) / 2 from
Not by a lot, the divide by constant can be optimized to a multiplication, if you're doing it in a loop (why would you care if you do it only once), you can reach a throughput of one multiplication per cycle, so (x*3)/3 would take 2 cycles on average.
The point is rather that Rust has a bunch of methods on integer for dealing with overflows in a variety of ways, like saturating, wrapping or returning an Option with a None value on overflow (there's also one that returns a tuple with the carry).
That's all good but it misses my point: the one redeeming quality of C's integer overflow non-semantics is that it unlocks further compiler optimizations, and that's the mode which AFAICT Rust doesn't support.
For example, a for loop from 0-through-N (N:i32) will execute N times in C, but N-or-infinity times in Rust. That small difference of infinity controls how the loop condition is tested, whether it can be turned into a simple counter or not.
> For example, a for loop from 0-through-N (N:i32) will execute N times in C, but N-or-infinity times in Rust.
Eh, not quite true. If N is a 64-bit or an unsigned integer but your loop index is a signed 32-bit integer, then you will have this behavior. In Rust, the idiomatic for loop will have the index variable always be the same size as the condition variable, which prevents the infinity case from happening. In C, if you have a 64-bit condition but a 32-bit unsigned loop index variable, you get the N-or-infinity case as well.
In other words, if your language is subtly redesigned to make you jump through hoops to do things like have mismatched integer types in for loops, then the need to rely on undefined behavior to reclaim that performance is lessened.
> In the future, if hardware allows for cheap hardware traps in the case of unintentionally overflow, they reserve the right to use them in release mode.
This seems awfully close to implementation-defined behavior to me…I'm surprised to see something like this, which allows for (IMHO) unsafe behavior, from Rust.
Rust specifies that both signed and unsigned integer overflow will wrap as two’s complement. The Rust developers want to discourage overflow because it is a common source of logic bugs, but the behavior is not unsafe.
What I was trying to say is that it's kinda odd to me that code behaves differently debug and release builds–to me it seems like it could make it easy to write code that works in one configuration but is subtly broken, without realizing the issue, because the programmer was complacent about checking which build they were using. Changing behavior based on whether hardware traps are available makes it even worse. I see how it's not "unsafe" in the usual sense (e.g. memory corruption), but I really would prefer an all-or-nothing overflows just don't or overflows always trap (usually with a way to override this for specific computations, either for performance or for properties of 2s complement arithmetic).
Interesting. Does this compile down to the appropriate assembly instructions for checking for overflow, if available, or does it end up emulating it with a couple dozen instructions? That is, is this low-cost or does it have a measurable performance penalty and should only be used in debug builds?
It depends on the implementation, which does what it has to do to work with the backend. If the backend supports intrinsics that efficiently check for these things, then they'll be used.
It's really the same issue any language would have with the particular backend it uses.
What's different about the D approach, however, is that the user can select which of several behaviors on overflow, and then select which of several responses to overflow, instead of being stuck with just one or two options. The tests we've run shows it generates the same code as if one had done it by hand.
Processors had these en masse (6502, Z80, MC68000) since the ‘70’s of the past century. Without those, it’s extremely difficult to implement basic arithmetic with numbers larger than what the processor’s registers support.
Which contemporary processors don’t have the C and Z status bits?
Post scriptum, since “Hacker News” is of the opinion that I’m posting too fast:
MIPS does have a status register, and this register does have an overflow bit:
> MIPS does have a status register, and this register does have an overflow bit:
Yes, it does, but it doesn't function as a simple carry or zero bitfield. In MIPS, there are two instructions for performing addition: add and addu. add generates a trap which you can respond to in an exception handler (which is where you check for this exception code)–as opposed to addu, which overflows silently. Most programs explicitly looking to check for overflow just use addu followed by a sltu (set less than, unsigned).
That there is no bit used for the sign, and so I know when the registers will overflow. A lot of the assembler code I wrote exploits this very fact, since when used cleverly it’s a performance optimization.
You’ll never feel that penalty if you’re writing the assembler code by hand, not even on the oldest, slowest MIPS processor, and if you’re using a compiler, the compiler intrinsic has to do the same thing.
Mashey has a good argument, however I think that the sr overload could be solved with one sr per pipeline. For example, if the CPU is a six pipeline superscalar design, like MIPS R10000 was, implement sr0 to sr5.
70 comments
[ 2.7 ms ] story [ 146 ms ] threadThis seems like a situation where dropping to assembly to perform the addition and testing the flag makes sense. GCC has a mechanism for inline assembly where you list the variables you read from, you write to, and registers you modify and the optimizer plays nice with you. You could provide it as a function to use instead of doing the addition in C.
It's easy to write, but not so easy to optimize the performance.
Overflow checking creates a dependency in between operations and requires the emission of instructions that didn't have to be there before.
i += 1 <branch if overflow> i += 1 <etc>
Means that you are executing more than twice the number of instructions and are getting further behind on the dependency chain each iteration. Eventually, your branch subsystem is completely overwhelmed by a branch that is taken effectively never.
https://yarchive.net/comp/linux/cmov.html
That said in modern clang and gcc you can (and should) use the overflow builtins which are all essentially:
Returning true if the operation overflowed. These produce “ideal” code, e.gif(overflowoperation(x,y,&out)) ...
Produces the platform branch on overflow instructions.
That is not an accurate or fair assessment.
However signed integer overflow being undefined behavior is a bit over the top given modern hardware. I'd like to see the C standard adopt a policy of wrapping for signed overflow, then either add new operators or functions for trapping behavior.
Most reasonable hardware has the instructions to trap or branch if add/sub/mul/div overflows or underflows so the checks are very cheap. Unlike the early days, in this day if C made it part of the standard processor manufacturers would simply stop selling CPUs without those features. Same goes for two's-compliment integer representations. They already did the same for 8-bit char/byte; other sizes are no longer supported.
Obviously there is signed integer overflow, which has resulted in numerous security flaws for questionable performance gains.
Then there are other fun things, like technically it is UB for bzero to skip padding bytes in a struct (because that is UB), which means bzero(&somestruct, siseof(somestruct)) is not technically guaranteed to clear all of the struct, so I hope there’s not any secrets in left over memory.
Punning through unions is well-defined behavior in C99 and C11. In C++11 and newer, it's questionable, but it works in practice.
Aliasing queries in the compiler basically work on a tiered system. There's a simple aliasing pass that first checks for really obvious alias information (such as, it's the same pointer, it's a bitcast of the pointer, it's a simple arithmetic expression of the same pointer), possibly some more advanced Andersen's-style alias passes, and only finally is there a last-ditch strict-aliasing based pass.
If you take the address of a float and cast that to an integer pointer to get at the bits of the float (and don't let either pointer escape the function), it will work, even with "abusive" compilers that have aggressive optimization turned on, even if you modify the lvalue in the intervening code. It's literally more work to make the compiler not realize that the two pointers alias, and there's no benefit to pessimizing a trivial query, so I would be shocked if any compiler actually miscompiled that code.
The definition of UB would allow a type punned union on the stack for instance to be treated as logically a struct - eg changes to the float value would not impact the integer value. While I agree that would not happen in practice today there is no reason it has to continue to work.
The fact that compilers already make sane decisions regarding some UB - evaluation order of parameters used to be unspecified and gcc at least would change evaluation order depending of optimisation level, but all compilers have realized that just insane and so left to right evaluation. Similarly bzero will initialise padding bytes in a struct even though that is technically UB, so could technically be skipped.
Sorry for run on horror sentences I’m typing on my phone :)
And that is effectively a bug in that standard, says Linus, and I agree with him.
https://lkml.org/lkml/2018/6/5/769
"I've said this before, and I'll say it again: a standards paper is just so much toilet paper when it conflicts with reality."
Sadly, the compiler maintainers at some points of time managed to push the real "breaking" changes through (for other UB cases). It seems that the "language lawyerism" is "easier" than the expected approach of the responsible engineers.
Memcpy is a cost. Do it only when you actually need a copy.
I suppose union based type punning makes the intent a little clearer (vis a vis memcpy) but that's a weak argument at best.
How much more explicit can you get than a memcpy?
Eh, the undefined nature of signed overflow in C is a bit of a red herring. The most common overflow bug in practice is actually 100% well-defined and still 100% wrong: multiplying two numbers to feed into a malloc.
In practice, the undefined signed overflow really only comes into play when you're trying to explicitly check for overflow by doing the operation and then seeing if it overflowed. But, as you mention, compilers give you bultins to do this kind of checked overflow which is quite frankly a much easier way to solve the problem, especially since it's more likely to actually get matched to using hardware flag bits to check for overflow.
Specifically for
X = a * b
You have to do:
If (b == 0) X = 0 Else if (MAX(type of X) / b >= a) X = a * b Else Handle overflow error
Oof
Just to be able to serve DNS answers.
That’s just so wrong.
GCC and Clang have mostly compatible compiler intrinsics and with those two compilers you can compile your software to almost any platform out there.
If you need to get your code compiled on MSVC or ICC or some niche C compiler, you can work around this by adding some kind of wrapper around them with #ifdefs (not pretty).
With compiler built-ins you get SIMD, atomics, bitops (popcnt, clz, etc) that are portable to almost any CPU instruction set out there. Without them, you have to re-write that code for every instruction set you intend to support.
So your options are:
1) Stick to standard C and write code that compiles to suboptimal output code (such as using division to get multiply-with-overflow)
2) Use compiler built-ins and get portability across different HW at the expense of not supporting other compilers
3) Write wrappers around compiler built-ins with #ifdefs to support all the compilers you want (add fallback to standard C if you need to)
4) Write your code using HW-specific intrinsics or inline assembly
Out of these, option 2) will give you maximum portability with least amount of effort. The other options are more effort and the benefits are arguable. If you think there are other viable options, please do share.
[0] https://github.com/nemequ/portable-snippets
Writing code to two specific compilers (clang and gcc) is NOT portable code.
This is absolutely unacceptable behavior.
But wait, that's not portable to Keil so you cannot use that ;)
The reaction is, if you dislike fast but non standard code, push for standardization of those features, not begrudge developers for doing the right thing and using them.
However, they already work acceptably in GCC and Clang. There are only minor differences (e.g. __builtin_shufflevector) but it's usable with very minimal #ifdeffing.
Example:
It's not perfect but it's quite usable and I've been using vector extensions in C for years.[0] https://gcc.gnu.org/onlinedocs/gcc/Vector-Extensions.html
Trading portability for performance is a trade-off that most developers shouldn’t be willing to make if they are open sourcing their software, because it will prevent someone from running their software on that user’s desired platform, thereby showing the user the middle finger. That’s just wrong.
http://www.pixelbeat.org/programming/gcc/integer_overflow.ht...
Can the programmer express the intent that this will not overflow? i.e. is it possible to coerce the Rust compiler to optimize (x*2)/2 to x? "Cheap hardware traps" cannot substitute for such a compile-time optimization.
> C seems pretty hopeless since there is no way to know whether or not the coder intended on overflow or not.
Well this doesn't matter: if signed arithmetic overflows the code is undefined, regardless of intent.
In practice comments can express intent, and unsigned arithmetic can with some finessing produce most codegen. Explicit operators would be a big improvement though.
LLVM’s constant folding and canonicalization passes will convert (x * 2) / 2 from
%0 = mul i64 %x, i64 2
%1 = div i64 %0, i64 2
to
%0 = shl i64 %x, i64 1
%1 = shr i64 %0, i64 1
Which constant folding will reduce to just %x.
Rust made the right tradeoff here but it is a tradeoff!
For example, a for loop from 0-through-N (N:i32) will execute N times in C, but N-or-infinity times in Rust. That small difference of infinity controls how the loop condition is tested, whether it can be turned into a simple counter or not.
Eh, not quite true. If N is a 64-bit or an unsigned integer but your loop index is a signed 32-bit integer, then you will have this behavior. In Rust, the idiomatic for loop will have the index variable always be the same size as the condition variable, which prevents the infinity case from happening. In C, if you have a 64-bit condition but a 32-bit unsigned loop index variable, you get the N-or-infinity case as well.
In other words, if your language is subtly redesigned to make you jump through hoops to do things like have mismatched integer types in for loops, then the need to rely on undefined behavior to reclaim that performance is lessened.
This seems awfully close to implementation-defined behavior to me…I'm surprised to see something like this, which allows for (IMHO) unsafe behavior, from Rust.
https://huonw.github.io/blog/2016/04/myths-and-legends-about...
https://dlang.org/phobos/std_experimental_checkedint.html
It's really the same issue any language would have with the particular backend it uses.
What's different about the D approach, however, is that the user can select which of several behaviors on overflow, and then select which of several responses to overflow, instead of being stuck with just one or two options. The tests we've run shows it generates the same code as if one had done it by hand.
Which contemporary processors don’t have the C and Z status bits?
Post scriptum, since “Hacker News” is of the opinion that I’m posting too fast:
MIPS does have a status register, and this register does have an overflow bit:
https://www.doc.ic.ac.uk/lab/secondyear/spim/node10.html
------------------
Addendum addressing your updates:
> MIPS does have a status register, and this register does have an overflow bit:
Yes, it does, but it doesn't function as a simple carry or zero bitfield. In MIPS, there are two instructions for performing addition: add and addu. add generates a trap which you can respond to in an exception handler (which is where you check for this exception code)–as opposed to addu, which overflows silently. Most programs explicitly looking to check for overflow just use addu followed by a sltu (set less than, unsigned).