26

Can anyone provide some code examples that act differently when compiled with -fwrapv vs without?

The gcc documentation says that -fwrapv instructs the compiler to assume that signed arithmetic overflow of addition, subtraction and multiplication wraps around using twos-complement representation.

But whenever I try overflowing the result is the same with or without -fwrapv.

wovano
  • 4,543
  • 5
  • 22
  • 49
ktal
  • 263
  • 1
  • 3
  • 4
  • 5
    I think better wording would be that it instructs the compiler to **ensure** that signed arithmetic wraps around, instead of causing undefined behavior. Usually that's what the underlying hardware would do anyway, and it is just a matter of making the compiler take that into account when optimizing. But the current wording makes it sound like the compiler is making an assumption that may or may not be justified. In fact the compiler *ensures* that this assumption is justified. – Nate Eldredge Mar 20 '22 at 15:46

5 Answers5

35

Think of this function:

int f(int i) {
    return i+1 > i;
}

Mathematically speaking, i+1 should always be greater than i for any integer i. However, for a 32-bit int, there is one value of i that makes that statement false, which is 2147483647 (i.e. 0x7FFFFFFF, i.e. INT_MAX). Adding one to that number will cause an overflow and the new value, according to the 2's compliment representation, will wrap-around and become -2147483648. Hence, i+1>i becomes -2147483648>2147483647 which is false.

When you compile without -fwrapv, the compiler will assume that the overflow is 'non-wrapping' and it will optimize that function to always return 1 (ignoring the overflow case).

When you compile with -fwrapv, the function will not be optimized, and it will have the logic of adding 1 and comparing the two values, because now the overflow is 'wrapping' (i.e. the overflown number will wrap according to the 2's compliment representation).

The difference can be easily seen in the generated assembly - in the right pane, without -fwrapv, function always returns 1 (true).

Aziz
  • 20,065
  • 8
  • 63
  • 69
  • I passed INT_MIN into your function and compiled without fwrapv and I got 0. The assembly code also seems to indicate that even without the fwrapv option, the compiler still checks for overflow based on the assembly output. movl %edi, -4(%rbp) movl -4(%rbp), %eax addl $1, %eax cmpl %eax, -4(%rbp) setl %al movzbl %al, %eax Are we assuming -O2? – ktal Nov 11 '17 at 00:40
  • 3
    @ktal I'm assuming compiler optimizations are on (I used `-O3`). [Here is the comparison of the two compiled codes](https://godbolt.org/g/LvBqEh) – Aziz Nov 11 '17 at 00:45
  • 1
    If it was called with a constant `INT_MAX` rather than a variable whose value the compiler can't see, it's also possible that the function got inlined and subjected to constant-propagation before collapse of `i+1>i` to true, in which case it might have evaluated to false. – R.. GitHub STOP HELPING ICE Nov 11 '17 at 01:05
8
for (int i=0; i>=0; i++)
    printf("%d\n", i);

With -fwrapv, the loop will terminate after INT_MAX iterations. Without, it could do anything since undefined behavior is unconditionally invoked by evaluation of i++ when i has the value INT_MAX. In practice, an optimizing compiler will likely omit the loop condition and produce an infinite loop.

R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711
  • Rather than using the typical scare tactic ("it could do anything") which doesn't seem to be very effective tbh, perhaps it might be best to use that tidbit of information about `-ftrapv` mentioned earlier to build a case where `-fwrapv` *isn't* used and the *undefined behaviour* you mention causes *something other than wrapping*... – autistic Nov 11 '17 at 00:25
  • So what you're saying is that INT_MAX + 1 wraps to INT_MIN with fwrapv but it is undefined without, correct? I tried running the loop you gave but it always seems to assume INT_MAX + 1 = INT_MIN regardless of the fwrapv option. Is this supposed to happen? – ktal Nov 11 '17 at 00:32
  • @ktal The behaviour is undefined in the realm of standard C, but in reality you won't likely stumble across an implementation (at least not blindly) that doesn't use twos complement arithmetic with overflow semantics. – autistic Nov 11 '17 at 00:37
  • @Sebivor So fwrapv is basically implied when overflow occurs? What should happen according to the C standards if I tried INT_MAX + 1 without fwrapv? – ktal Nov 11 '17 at 00:45
  • @ktal: Make sure you are using at least `-O2`. GCC's default optimization level is `-O0` which is basically "be intentionally obtuse". – R.. GitHub STOP HELPING ICE Nov 11 '17 at 01:00
  • @autistic: You probably won't run into an implementation that doesn't use twos-complement arithmetic, but you can easily run into an implementation that assumes the overflow doesn't happen and rewrites your loop in some unrecognizable form that's technically equivalent provided the calculation never overflows but results in an infinite loop otherwise. – Daniel McLaury May 22 '19 at 00:38
  • @DanielMcLaury: GCC will do worse than that, given a chance. Not only will gcc assume that traits which would apply to the arithmetically-correct results of a calculation (e.g. that the sum of two positive number will be positive) will apply to the result, but gcc will sometimes back-propagate assumptions in ways that cause overflow to arbitrarily disrupt program behavior *even if the result of the calculation would otherwise end up being ignored*. – supercat Jun 10 '21 at 19:52
2

The ISO standard working group WG14 exists to establish a convention that all C compilers must adhere to. Some compilers may (and do) also implement extensions. According to ISO standard C, those extensions are considered one of the following:

  • implementation-defined, meaning the compiler devs must make a choice, document that choice and maintain the lot in order to be considered a compliant C implementation.
  • C11/3.4.3 establishes a definition for undefined behaviour and gives an extremely familiar example, which is vastly superior to anything I could write:

    1 undefined behavior behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements

    2 NOTE Possible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message).

    3 EXAMPLE An example of undefined behavior is the behavior on integer overflow.


There's also an unspecified behaviour, though I'll leave it as an exercise to you to read about that in the standard.

Be careful where you tread. This is one of the few generally accepted undefined behaviours where it's typically expected that a LIA-style wrapping will occur upon a twos complement representation without a trap repesentation. It's important to realise that there are implementations that use a trap representation corresponding to the bit representation containing all ones.

In summary, fwrapv and ftrapv exist to pass on a choice to you, a choice which the developers would have otherwise had to make on your behalf, and that choice is what happens when signed integer overflow occurs. Of course, they must select a default, which in your case appears to correlate to fwrapv rather than ftrapv. That needn't be the case, and it needn't be the case that these compiler options change anything what-so-ever.

autistic
  • 1
  • 3
  • 35
  • 80
1

When using a new version of the gcc compiler I stumbled into a problem that made the flag -fwrapv clear.

char Data[8]={0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
int a=256*(256*(256*Data[3]+Data[2])+Data[1])+Data[0];

The result will be -1 if you use the -fwrapv flag or not. But is you do:

if(a<0)
  printf("the value of a %d is negative\n",a);
else
  printf("the value of a %d is positive\n",a);

It will print "the value of a -1 is positive" because when optimising it removes the first part because + - and * of positive numbers will always be positive (char is unsigned).
When you compile with the -fwrapv flag it will print the correct answer.
If you use this:

int a=(Data[3]<<24)+(Data[2]<<16)+(Data[1]<<8)+Data[0];

The code works as expected with or without the flag.

  • That behavior would rank fairly low on my "astonishment-o-meter" if `a` is an automatic object whose address is not observed, compared with e.g. `unsigned mul_mod_65536(unsigned short x, unsigned short y) { return (x*y) & 0xFFFFu; }` which will in gcc sometimes cause the caller to behave nonsensically if `x` exceeds `INT_MAX/y`. – supercat Jan 26 '21 at 22:31
  • What astounded me was that the value was -1 but that the program insisted that is was NOT < 0. – Peter v.d. Vos Jan 27 '21 at 13:21
  • On some platforms, the most efficient way to process `int`-sized math would sometimes be to substitute a larger type. For example, `long1 = long2 + (int1 * int2);` might be more efficiently processed with an `int*int->long` multiply instruction than an `int*int->int` instruction followed by sign extension. While that wouldn't generally be true of addition, I don't regard as astonishing any behavior that would fit with such a model for temporary values, and I only regard as "moderately astonishing" behavior that would be consistent with treating some automatic objects likewise. – supercat Jan 27 '21 at 15:34
  • 1
    IMHO, even when using `-fwrapv`, if a programmer wants integer values to wrap, rather than allowing a compiler to perform computations as though with a larger type, the programmer should use a casting operator to make such intentions clear. The reason I find the `mul_mod_65536` function above astonishing is that it can affect *calling code* in nonsensical ways even though the behaviors of wrapping and extra-promoted operations would be identical. – supercat Jan 27 '21 at 15:36
0

-fwrapv tells the compiler that overflow of signed integer arithmetic must be treated as well-defined behavior, even though it is undefined in the C standard.

Nearly all CPU architectures in widespread use today use the "2's complement" representation of signed integers and use the same processor instructions for signed and unsigned addition, subtraction and non-widening multiplication of signed and unsigned values. So at a CPU architecture level both signed and unsigned arthimetic wrap around modulo 2n.

The C standard says that overflow of signed integer arithmetic is undefined behavior. Undefined behavior means that "anything can happen". Anything includes "what you expected to happen", but it also includes "the rest of your program will behave in ways that are not self-consistent".

In particular, when undefined behavior is invoked on modern compilers, the optimiser's assumptions about the value in a variable can become out of step with the value actually stored in that variable.

Therefore if you allow signed arithmetic overflow to happen in your programs and do not use the fwrapv option, things are likely to look ok at first, your simple test programs will produce the results you expect.

But then things can go horribly wrong. In particular checks on whether the value of a variable is nonnegative can be optimised away because the compiler assumes that the variable must be positive, when in fact it is negative.

plugwash
  • 9,724
  • 2
  • 38
  • 51