Date: Wed, 3 Sep 2025 13:23:27 +0200
On 02/09/2025 22:21, Oliver Hunt via Std-Proposals wrote:
>
>
>> On Sep 2, 2025, at 12:56 PM, Julien Villemure-Fréchette via Std-Proposals <std-proposals_at_[hidden]> wrote:
>>
>>> That is the mathematically correct answer for a modular arithmetic ring.
>>
>> But the signed integer types are not intended to model modular arithmetic on a finite ring of characteristic 2^N. An operation "a + b" is meant to express the mathematical operation on the integers, not modular arithmetic.
>>
>
> This _might_ be a reasonable argument for `int` or `int32_t` (basically because legacy, and the widespread use of int32 induction variables on 64bit machine - which at this point seems to be the only meaningfully performance impacting application of signed overflow is UB)
There is no "might be" here at all. Signed integer types in C, C++, and
many other languages model mathematical integers as best they can - they
do not model modular arithmetic rings. (And even in languages that
define signed integer overflow as wrapping, such as Java, the integer
types are almost never used as modular types - an overflow is a bug in
the code, regardless of how defined or undefined the behaviour may be.)
Since we want to model integers, not modular rings, UB on overflow is
actually the best model. Julien explained it very well in terms of
mathematical domains for mathematical functions. It makes no more sense
to add two "int" values leading to an overflow, than it does to ask for
the square root of a negative number or how to divide 5 by 3 without a
remainder.
And there are many other situations where UB on signed overflow leads to
more optimal code. An example situation would be when you have a 32-bit
int as an index in an array:
<https://godbolt.org/z/TPP968nbs>
When faced with something like "xs[i++];" in a loop, with 64-bit
pointers and a 32-bit int, if int overflow is UB then the compiler can
do transforms to calculate a pointer to the initial address of "xs[i]",
then for each loop round it simply increments the pointer by the
appropriate amount (from the size of xs[0]). The final "i", if needed,
can be got by subtracting the end pointer from the starting pointer and
dividing.
If int overflow is defined as wrapping, however, then the compiler must
assume that the "i++" bit could wrap. So for every loop iteration it
must increment "i" with 32-bit wrapping and add that to the base of "xs"
to get the pointer.
In all sorts of situations where you have loops (and if you don't have a
loop, and are simply doing something once, the efficiency matters
little) the fact that incrementing or decrementing C and C++ ints is
simple, monotonic and usually has easily calculable start and end points
is very useful. A modular int type is very different, and generally
significantly more costly to work with.
>
> But for `_BitInt(N)` that legacy is not relevant, the optimization is not relevant (the stated use cases are things like fpgas where _BitInt(N) is likely always going to mean N-bit integer unit rather than M bit unit with N<=M), so the use cases are more reasonably assumed to be operating on the “finite field” definition. The reality is developers already assume that, and the only reason it is incorrect is because the specification says so.
>
FPGAs are /one/ stated use for _BitInt types - not the only one. And
no, finite fields (or finite modular rings, more accurately) are not
something that can reasonably be assumed because that is not what people
generally use integers for - no matter what size they are. You don't
get to claim that two's complement wrapping is good just because you
think UB is bad (actually attempting to execute UB /is/ always bad).
You don't get to claim modular rings are good just because they are
defined behaviour.
If you want to argue for having modular behaviour for signed _BitInt
types, then show real-world use-cases where that behaviour is directly
beneficial. It is not enough to say UB overflow is bad - all overflow,
no matter how defined the behaviour is, is bad if it is not what the
programmer wants in the code.
Show where people would want wrapping overflow - in examples where there
are very clear benefits in writing the code that way rather than
alternative constructions with different tests, use of unsigned types,
or other techniques.
> Trapping, wrapping, saturating - going to infinity? (I recall early gpu shader languages have “int” but using floats, and I don’t know if such an implementation could possibly be conforming?) - are all reasonable definitions, maybe there are a few other options, but no hardware I’m aware of gives a non-deterministic result to overflow for any integral operation so continuing to pretend so for _new_ types is not a reasonable path forward.
>
C and C++ are not based on what hardware implementations do. Hardware
works the way it works because that is the cheapest and/or fastest way
to implement things. C and C++ work the way they do because that is the
best way to write software, within the limitations of practical hardware
implementations. If you want to write in assembly to match your cpu
instructions, you go right ahead and leave the high level programming
languages to people who prefer them.
> _If_ a compiler (or a user) really wanted a “assume this cannot overflow” path there are a number of options as language extensions (pragma, type attribute, …), or restructuring the code (the UB advantage for overflow in induction variables can be easily resolved simply by using the machine word type explicitly and removing the need for UB to permit that type when lowering).
>
If the user wants to say "assume this cannot overflow", they use signed
integer arithmetic - that has been the case for 50 years or so for C and
later C++. If they want to say "make this wrapping", they use unsigned
integer arithmetic.
>
>
>> On Sep 2, 2025, at 12:56 PM, Julien Villemure-Fréchette via Std-Proposals <std-proposals_at_[hidden]> wrote:
>>
>>> That is the mathematically correct answer for a modular arithmetic ring.
>>
>> But the signed integer types are not intended to model modular arithmetic on a finite ring of characteristic 2^N. An operation "a + b" is meant to express the mathematical operation on the integers, not modular arithmetic.
>>
>
> This _might_ be a reasonable argument for `int` or `int32_t` (basically because legacy, and the widespread use of int32 induction variables on 64bit machine - which at this point seems to be the only meaningfully performance impacting application of signed overflow is UB)
There is no "might be" here at all. Signed integer types in C, C++, and
many other languages model mathematical integers as best they can - they
do not model modular arithmetic rings. (And even in languages that
define signed integer overflow as wrapping, such as Java, the integer
types are almost never used as modular types - an overflow is a bug in
the code, regardless of how defined or undefined the behaviour may be.)
Since we want to model integers, not modular rings, UB on overflow is
actually the best model. Julien explained it very well in terms of
mathematical domains for mathematical functions. It makes no more sense
to add two "int" values leading to an overflow, than it does to ask for
the square root of a negative number or how to divide 5 by 3 without a
remainder.
And there are many other situations where UB on signed overflow leads to
more optimal code. An example situation would be when you have a 32-bit
int as an index in an array:
<https://godbolt.org/z/TPP968nbs>
When faced with something like "xs[i++];" in a loop, with 64-bit
pointers and a 32-bit int, if int overflow is UB then the compiler can
do transforms to calculate a pointer to the initial address of "xs[i]",
then for each loop round it simply increments the pointer by the
appropriate amount (from the size of xs[0]). The final "i", if needed,
can be got by subtracting the end pointer from the starting pointer and
dividing.
If int overflow is defined as wrapping, however, then the compiler must
assume that the "i++" bit could wrap. So for every loop iteration it
must increment "i" with 32-bit wrapping and add that to the base of "xs"
to get the pointer.
In all sorts of situations where you have loops (and if you don't have a
loop, and are simply doing something once, the efficiency matters
little) the fact that incrementing or decrementing C and C++ ints is
simple, monotonic and usually has easily calculable start and end points
is very useful. A modular int type is very different, and generally
significantly more costly to work with.
>
> But for `_BitInt(N)` that legacy is not relevant, the optimization is not relevant (the stated use cases are things like fpgas where _BitInt(N) is likely always going to mean N-bit integer unit rather than M bit unit with N<=M), so the use cases are more reasonably assumed to be operating on the “finite field” definition. The reality is developers already assume that, and the only reason it is incorrect is because the specification says so.
>
FPGAs are /one/ stated use for _BitInt types - not the only one. And
no, finite fields (or finite modular rings, more accurately) are not
something that can reasonably be assumed because that is not what people
generally use integers for - no matter what size they are. You don't
get to claim that two's complement wrapping is good just because you
think UB is bad (actually attempting to execute UB /is/ always bad).
You don't get to claim modular rings are good just because they are
defined behaviour.
If you want to argue for having modular behaviour for signed _BitInt
types, then show real-world use-cases where that behaviour is directly
beneficial. It is not enough to say UB overflow is bad - all overflow,
no matter how defined the behaviour is, is bad if it is not what the
programmer wants in the code.
Show where people would want wrapping overflow - in examples where there
are very clear benefits in writing the code that way rather than
alternative constructions with different tests, use of unsigned types,
or other techniques.
> Trapping, wrapping, saturating - going to infinity? (I recall early gpu shader languages have “int” but using floats, and I don’t know if such an implementation could possibly be conforming?) - are all reasonable definitions, maybe there are a few other options, but no hardware I’m aware of gives a non-deterministic result to overflow for any integral operation so continuing to pretend so for _new_ types is not a reasonable path forward.
>
C and C++ are not based on what hardware implementations do. Hardware
works the way it works because that is the cheapest and/or fastest way
to implement things. C and C++ work the way they do because that is the
best way to write software, within the limitations of practical hardware
implementations. If you want to write in assembly to match your cpu
instructions, you go right ahead and leave the high level programming
languages to people who prefer them.
> _If_ a compiler (or a user) really wanted a “assume this cannot overflow” path there are a number of options as language extensions (pragma, type attribute, …), or restructuring the code (the UB advantage for overflow in induction variables can be easily resolved simply by using the machine word type explicitly and removing the need for UB to permit that type when lowering).
>
If the user wants to say "assume this cannot overflow", they use signed
integer arithmetic - that has been the case for 50 years or so for C and
later C++. If they want to say "make this wrapping", they use unsigned
integer arithmetic.
Received on 2025-09-03 11:23:31