Source of Undefined Behavior
The key part of the problem is in the negation of -(int32_t)value
.1
At this point, value
is 8000000016 (231). Since that is not representable in int32_t
, the conversion is governed by C 2018 6.3.1.3 3, which says the behavior is implementation-defined. GCC 10.2 defines it to wrap modulo 2N, where the destination width is N bits. Wrapping 8000000016 to int32_t
modulo 232 produces −8000000016.
Then the negation operator, -
, is applied. The mathematical negation of −8000000016 is of course 8000000016, but this is not representable in int32_t
.2 The behavior is governed by C 2018 6.5 5:
If an exceptional condition occurs during the evaluation of an expression (that is, if the result is not mathematically defined or not in the range of representable values for its type), the behavior is undefined.
Thus, the negation has undefined behavior. When -O0
is used, the compiler generates simple direct code. Godbolt shows it generates a negate instruction that wraps, producing the output 8000000016 for the input bits 8000000016 (which would represent −8000000016 as a signed 32-bit integer). When -O2
is used, the compiler performs complicated analysis and transformation of the program, and the lack of defined behavior leaves the compiler free to produce any result. Indeed, Godbolt shows the negate instruction is absent. Effectively, the compiler “knows” that negating an int32_t
value can never produce a result with the 231 bit set in a program with defined behavior.
Discussion of Optimization
Consider that the range of values representable in int32_t
is −231 to 231−1. The mathematical negations of these are −(231−1) to 231. However, 231 overflows, resulting in an exceptional condition. The range of results that do not overflow is −(231−1) to 231−1. Therefore, in a program with defined behavior, only these results occur, and therefore the compiler may optimize as if only these results occur. In none of these results is the 231 bit set. Therefore, in a program with defined behavior, negation & 0x80000000
is always zero, and the compiler may generate code based on this.
Fix
It appears you want a test for whether the sign bit would be set in an int32_t
that is negated using two’s complement, that is, wrapping the result modulo 232. For this, unsigned arithmetic can be used. If x
is either an int32_t
value or a uint32_t
containing the bits that would represent such a value, then the sign bit of the negated value may be obtained with either of:
bool sign = - (uint32_t) x & 0x80000000u;
bool sign = - (uint32_t) x >> 31;
Footnote
1 We deduce long
is wider than 32 bits. Were it not, strtol("0x80000000", NULL, 16)
would return LONG_MAX
, per C 2018 7.22.1.4 8. That would be representable in uint32_t
and int32_t
, so value
would be initialized to LONG_MAX
, converting to int32_t
would keep that value, negation
would be −LONG_MAX
, and sign
would be zero in both the optimized and unoptimized versions of the program.
2 If int32_t
were narrower than int
, the operand would be promoted to int
before the negation, and the mathematical result would be representable. That is not the case with the GCC version and options you used, which we can deduce from the observed results.