28

Consider the following program.

#include <stdio.h>

int negative(int A) {
    return (A & 0x80000000) != 0;
}
int divide(int A, int B) {
    printf("A = %d\n", A);
    printf("negative(A) = %d\n", negative(A));
    if (negative(A)) {
        A = ~A + 1;
        printf("A = %d\n", A);
        printf("negative(A) = %d\n", negative(A));
    }
    if (A < B) return 0;
    return 1;
}
int main(){
    divide(-2147483648, -1);
}

When it is compiled without compiler optimizations, it produces expected results.

gcc  -Wall -Werror -g -o TestNegative TestNegative.c
./TestNegative
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 1

When it is compiled with compiler optimizations, it produces the following incorrect output.

gcc -O3 -Wall -Werror -g -o TestNegative TestNegative.c
./TestNegative 
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 0

I am running gcc version 5.4.0.

Is there a change I can make in the source code to prevent the compiler from producing this behavior under -O3?

merlin2011
  • 71,677
  • 44
  • 195
  • 329
  • The `-O3` option is known for sometimes generating faulty code, if you looked into the issue a little. Scale back to `-O2` and add individual optimizations if you need them. – Some programmer dude Feb 13 '18 at 07:53
  • 25
    `A = ~A + 1;` is UB, if `A == INT_MIN`, the `+1` makes a signed integer overflow. – mch Feb 13 '18 at 07:54
  • 1
    @Someprogrammerdude, I'm not the one running the compiler. I'm writing this code to run in someone else's environment (programming-contest type website), so I have no control over the compiler flags. – merlin2011 Feb 13 '18 at 07:54
  • 8
    Isn't `0x7FFFFFFF + 1` undefined behaviour anyway for 32-bit `int` type? – Weather Vane Feb 13 '18 at 07:54
  • 5
    @mch, I think you hit the nail on the head. It never occurred to me that signed integer overflow doesn't behave like unsigned integer overflow. I just saw [this question](https://stackoverflow.com/q/18195715/391161) so now I know the difference. – merlin2011 Feb 13 '18 at 07:56
  • However at first glance it is a bit odd that the behaviour would be different with optimsation turned on / off, even if the C language specification does not define the behaviour. What's the compiler doing, finding an alternative op code for adding two integers when optimising the code?! – bazza Feb 13 '18 at 08:00
  • @merlin2011 , when UB is introduced, the compiler has a number of alternative options and it decides what to implement. When speed is of the essence, the minimal machine code option is preferred. Other optimization levels might prefer to treat the `int` as `unsigned` and produce more complicated code.. – Myst Feb 13 '18 at 08:03
  • @Myst, Are we assuming that UB is only in effect for inputs equivalent to `INT_MIN` or to all inputs? – merlin2011 Feb 13 '18 at 08:05
  • @merlin2011 I'm not sure, but I think the code will be flagged for all inputs, because `A` *might* be `INT_MIN`. The compiler *should* recognize code that *might* lead to UB. More often, the compiler will assume that the value is **not** the value that will lead to UB and write matching code (If memory serves, that's the clang approach). However, the compiler is quite free to make other assumptions and for high optimizations it might discard the code path as a whole. – Myst Feb 13 '18 at 08:11
  • 1
    @bazza: when the compiler detects UB, it's allowed to do whatever it likes, so with optimizations turned on it will arrange the code in any code it finds appropriate. Check [this post](http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html) for an example of how the compiler might catch you off guard. – vgru Feb 13 '18 at 08:17
  • 20
    @Someprogrammerdude No, it absolutely *isn’t* known for generating faulty code. Sure, there are compiler bugs in GCC (everything has bugs) but `-O3` is a safe default, and there are not fundamentally more correctness-affecting bugs in that setting than in GCC in general. https://stackoverflow.com/a/11546263/1968 – Konrad Rudolph Feb 13 '18 at 12:23
  • @Konrad I think it's fair to say that *any* optimisation increases the chance of a possible bug. More and particularly more complex code being executed simply increases chances to have bugs. Hell compiling with no optimisation has a good chance of hiding real bugs wrt UB and co (I don't think I ever stumbled upon code that produced faulty results when compiled with `O0` but correct code with `O3`). – Voo Feb 13 '18 at 15:58
  • 3
    @Voo It’s fair to say that this is *plausible*. But, barring evidence, it’s simply not the case in current compilers (`-O3` *used to* be experimental, and hence buggy; but it hasn’t been for a long time). To say that it “is known to sometimes generate faulty code” is flat out wrong. – Konrad Rudolph Feb 13 '18 at 16:37
  • @Konrad Oh no arguing on that and just as with any presumed compiler or framework bug: 99 out of a 100 times it's in your own code. You'll have orders of magnitudes more bugs in your own code than in ones that the compiler produces. But to be nitpicky, there's no doubt that higher optimisation levels increase the chances for bugs - we do have lots of proof in form of all the bug reports on the gcc bug tracker that only pertain to optimisations enabled by higher optimisation levels. – Voo Feb 13 '18 at 18:22
  • 1
    @Groo, yes it's all jolly entertaining when undefined behaviour is involved. Thanks for the link, very good reading indeed. Gone straight into my bookmarks. Happy coding! – bazza Feb 13 '18 at 20:24
  • when compiling without optimizations, the compiler outputs a warning about comparing a signed value with a unsigned value. on this line: `return (A & 0x80000000) != 0;` Suggest fixing that problem – user3629249 Feb 14 '18 at 16:26

4 Answers4

83
  1. -2147483648 does not do what you think it does. C doesn't have negative constants. Include limits.h and use INT_MIN instead (pretty much every INT_MIN definition on two's complement machines defines it as (-INT_MAX - 1) for a good reason).

  2. A = ~A + 1; invokes undefined behavior because ~A + 1 causes integer overflow.

It's not the compiler, it's your code.

JFMR
  • 23,265
  • 4
  • 52
  • 76
Art
  • 19,807
  • 1
  • 34
  • 60
  • Does the second line invoke undefined behavior *only* if the input is `INT_MIN`? – merlin2011 Feb 13 '18 at 08:03
  • @merlin2011 yes. – Art Feb 13 '18 at 08:06
  • https://stackoverflow.com/a/3803981/995714 https://stackoverflow.com/q/26003893/995714 `-214748364` is actually an `unsigned long` constant on C90 https://stackoverflow.com/q/25658485/995714 – phuclv Feb 13 '18 at 13:18
  • 2
    Ok, I've got to ask. What's happening? Who linked to this? I'm getting an insane amount of votes for what is a trivial answer to a trivial question. – Art Feb 14 '18 at 11:42
46

The compiler replaces your A = ~A + 1; statement with a single neg instruction, i.e. this code:

int just_negate(int A) {
    A = ~A + 1;
    return A;
}

will be compiled to:

just_negate(int):
  mov eax, edi
  neg eax         // just negate the input parameter
  ret

But the compiler is also smart enough to realize that, if A & 0x80000000 was non-zero before negation, it must be zero after negation, unless you are relying on undefined behavior.

This means that the second printf("negative(A) = %d\n", negative(A)); can be "safely" optimized to:

mov edi, OFFSET FLAT:.LC0    // .string "negative(A) = %d\n"
xor eax, eax                 // just set eax to zero
call printf

I use the online godbolt compiler explorer to check the assembly for various compiler optimizations.

vgru
  • 49,838
  • 16
  • 120
  • 201
  • 18
    This is a useful explanation of *why* the compiler produces unexpected results at high optimization levels. It is not looking for ways to trick the programmer when they invoke UB; it is looking for ways to make the code run faster, and assuming no UB to do that. – Martin Bonner supports Monica Feb 13 '18 at 10:28
18

To explain in detail what's going on here:

  • In this answer I'm assuming that long is 32 bits and long long is 64 bits. This is the most common case, but not guaranteed.

  • C does not have signed integer contants. -2147483648 is actually of type long long, on which you apply the unary minus operator.

    The compiler picks the type of the integer constant after checking if 2147483648 can fit:

    • Inside an int? No it cannot.
    • Inside a long? No it cannot.
    • Inside a long long? Yes it can. So the type of the integer constant will therefore be long long. Then apply unary minus on that long long.
  • Then you try to show this negative long long to a function expecting an int. A good compiler might warn here. You force an implicit conversion to a smaller type ("lvalue conversion").
    However, assuming 2's complement, the value -2147483648 can fit inside an int, so no implementation-defined behavior is needed for the conversion, which would otherwise have been the case.
  • Next tricky part is the function negative where you use 0x80000000. This is not an int either, nor is it a long long, but an unsigned int (see this for an explanation).

    When comparing your passed int with an unsigned int, "the usual arithmetic conversions" (see this) force an implicit conversion to the int to unsigned int. It doesn't affect the result in this specific case, but this is why gcc -Wconversion users do get a nice warning here.

    (Hint: enable -Wconversion already! It is good for catching subtle bugs, but not part of -Wall or -Wextra.)

  • Next you do ~A, a bitwise inverse of the binary representation of the value, ending up with the value 0x7FFFFFFF. This is, as it turns out, the same value as INT_MAX on your 32 or 64 bit system. Thus 0x7FFFFFFF + 1 gives a signed integer overflow which leads to undefined behavior. This is the reason why the program is misbehaving.

    Cheekily, we could change the code to A = ~A + 1u; and suddenly everything works as expected, again because of implicit integer promotion.


Lessons learned:

In C, integer constants, as well as implicit integer promotions, are very dangerous and unintuitive. They can subtly change the meaning of the program completely and introduce bugs. At each and every operation in C, you need to consider the actual types of the operands involved.

Playing around with C11 _Generic could be a good way to see the actual types. Example:

#define TYPE_SAFE(val, type) _Generic((val), type: val)
...
(void) TYPE_SAFE(-2147483648, int); // won't compile, type is long or long long
(void) TYPE_SAFE(0x80000000, int);  // won't compile, type is unsigned int

Good safety measures to protect yourself from bugs like these is to always use stdint.h and to use MISRA-C.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • 1
    @Groo _Generic is a really nice feature and reason alone to go for C11. There's apparently some [ambiguous behavior](https://gustedt.wordpress.com/2015/05/11/the-controlling-expression-of-_generic/) about it still, where different compilers interpret the standard differently. Hopefully this will be fixed in Cxx. – Lundin Feb 14 '18 at 11:04
13

You are relying on undefined behavior. 0x7fffffff + 1 for 32 bit signed integers results in signed integer overflow, which is undefined behavior according to the standard, so anything goes.

In gcc you can force wraparound bahavior by passing -fwrapv; still, if you have no control over the flags - and more in general, if you want a more portable program - you should do all these tricks on unsigned integers, which are required by the standard to wrap around (and have well defined semantics for bitwise operations, unlike signed integers).

First convert the int to unsigned (well defined according to the standard, yields the expected result), do your stuff, convert back to int - implementation-defined (≠ undefined) for values bigger than the range of int, but actually defined by every compiler working in 2's complement to do the "right thing".

int divide(int A, int B) {
    printf("A = %d\n", A);
    printf("negative(A) = %d\n", negative(A));
    if (negative(A)) {
        A = ~((unsigned)A) + 1;
        printf("A = %d\n", A);
        printf("negative(A) = %d\n", negative(A));
    }
    if (A < B) return 0;
    return 1;
}

Your version (at -O3):

A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 0

My version (at -O3):

A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 1
Matteo Italia
  • 123,740
  • 17
  • 206
  • 299