简体   繁体   English

C 按位与 -O0 和 -O2 给出不同的结果

[英]C bitwise AND gives different result with -O0 and -O2

I'm working on a PC emulator using both Bochs and DOSBox as references.我正在使用 Bochs 和 DOSBox 作为参考来开发 PC 模拟器。

When emulating the "NEG Ed" instruction (two's complement negation of a doubleword) I'm getting different results if I compile with -O0 rather than -O2 .在模拟“NEG Ed”指令(双字的二进制补码否定)时,如果我使用-O0而不是-O2编译,我会得到不同的结果。

This is a test program with just the relevant bits:这是一个只有相关位的测试程序:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>

int main(int argc, const char **argv)
{
    uint32_t value = strtol(argv[1], NULL, 16);
    uint32_t negation = -(int32_t)(value);
    bool sign = negation & 0x80000000;

    printf("value=%X, negation=%X, sign=%X\n", value, negation, sign);
    
    return 0;
}

The -(int32_t)(value); -(int32_t)(value); part is taken from Bochs' NEG_EdM() function;部分取自 Bochs 的NEG_EdM() function; for the equivalent operation DOSBox doesn't cast to signed int.对于等效操作,DOSBox 不会强制转换为带符号的 int。

If you compile this program with GCC 10 using the -O2 option and use the hexadecimal value 0x80000000 as input, you'll get a wrong result for sign :如果您使用-O2选项使用 GCC 10 编译此程序并使用十六进制值0x80000000作为输入,您将得到错误的sign结果:

value=80000000, negation=80000000, sign=0

When compiled with -O0 the result is correct:使用-O0编译时,结果是正确的:

value=80000000, negation=80000000, sign=1

Is this undefined behaviour?这是未定义的行为吗?

As far as I know casting to/from signed and unsigned integers is well defined, as is bitwise & on unsigned values.据我所知,有符号和无符号整数的转换是明确定义的,无符号值的按位 & 也是如此。

Source of Undefined Behavior未定义行为的来源

The key part of the problem is in the negation of -(int32_t)value .问题的关键部分在于否定-(int32_t)value 1 1

At this point, value is 80000000 16 (2 31 ).此时, value 80000000 16 (2 31 )。 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.由于这在int32_t中无法表示,因此转换由 C 2018 6.3.1.3 3 管理,这表示行为是实现定义的。 GCC 10.2 defines it to wrap modulo 2 N , where the destination width is N bits. GCC 10.2 将其定义为模 2 N包装,其中目标宽度为N位。 Wrapping 80000000 16 to int32_t modulo 2 32 produces −80000000 16 .将 80000000 16包装到int32_t模 2 32产生 −80000000 16

Then the negation operator, - , is applied.然后应用否定运算符- The mathematical negation of −80000000 16 is of course 80000000 16 , but this is not representable in int32_t . -80000000 16的数学否定当然是 80000000 16 ,但这在int32_t中无法表示。 2 The behavior is governed by C 2018 6.5 5: 2行为受 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.当使用-O0时,编译器生成简单的直接代码。 Godbolt shows it generates a negate instruction that wraps, producing the output 80000000 16 for the input bits 80000000 16 (which would represent −80000000 16 as a signed 32-bit integer). Godbolt 显示它会生成一个否定指令,该指令会生成 output 80000000 16用于输入位 80000000 16 (将 -80000000 16表示为带符号的 32 位整数)。 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.当使用-O2时,编译器会对程序进行复杂的分析和转换,缺乏定义的行为使编译器可以自由地产生任何结果。 Indeed, Godbolt shows the negate instruction is absent .事实上, Godbolt 表明否定指令不存在 Effectively, the compiler “knows” that negating an int32_t value can never produce a result with the 2 31 bit set in a program with defined behavior.实际上,编译器“知道”取反int32_t值永远不会产生在具有定义行为的程序中设置 2 31位的结果。

Discussion of Optimization优化讨论

Consider that the range of values representable in int32_t is −2 31 to 2 31 −1.考虑int32_t中可表示的值的范围是 -2 31到 2 31 -1。 The mathematical negations of these are −(2 31 −1) to 2 31 .这些的数学否定是 -(2 31 -1) 到 2 31 However, 2 31 overflows, resulting in an exceptional condition.但是,2 31溢出,导致异常情况。 The range of results that do not overflow is −(2 31 −1) to 2 31 −1.不溢出的结果范围是 -(2 31 -1) 到 2 31 -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 2 31 bit set.在这些结果中没有一个是 2 31位集。 Therefore, in a program with defined behavior, negation & 0x80000000 is always zero, and the compiler may generate code based on this.因此,在具有定义行为的程序中, negation & 0x80000000始终为零,编译器可能会基于此生成代码。

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 2 32 .看来您想要测试符号位是否会设置在使用二进制补码取反的int32_t中,即包装结果模 2 32 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:如果x是一个int32_t值或一个uint32_t ,其中包含表示此类值的位,则可以通过以下任一方式获得取反值的符号位:

bool sign = - (uint32_t) x & 0x80000000u;
bool sign = - (uint32_t) x >> 31;

Footnote脚注

1 We deduce long is wider than 32 bits. 1我们推断long比 32 位更宽。 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. 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将是 - LONG_MAX ,并且在程序的优化和未优化版本中, sign都将为零。

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. 2如果int32_tint窄,则操作数将在取反之前提升为int ,并且数学结果将是可表示的。 That is not the case with the GCC version and options you used, which we can deduce from the observed results.您使用的 GCC 版本和选项并非如此,我们可以从观察结果中推断出。

There are some issues in your code:您的代码中存在一些问题:

  • the value returned by strtol("0x80000000", NULL, 16) depends on the range of type long : if type long has 32 bits, the returned value should be LONG_MAX , which is 2147483647 , whereas if long is larger, 2147483648 will be returned. strtol("0x80000000", NULL, 16)返回的值取决于long类型的范围:如果long类型为 32 位,则返回值应为LONG_MAX ,即2147483647 ,而如果long更大,则返回2147483648 . Converting these values to uint32_t does not change the value as both as within the range of uint32_t .将这些值转换为uint32_t不会在uint32_t的范围内更改值。 Type long on your system seems to have 64 bits.您的系统上的long类型似乎有 64 位。 You could avoid this implementation defined behavior using strtoul() instead of strtol() .您可以使用strtoul()而不是strtol()来避免这种实现定义的行为。

  • there is no need for the intermediary cast to (int32_t) : negating the unsigned value is well defined and -0x80000000 has the value 0x80000000 for type uint32_t .不需要中间转换为(int32_t) :否定无符号值是明确定义的,并且-0x80000000对于uint32_t类型的值为0x80000000

  • furthermore, this conversion is counterproductive and the likely cause of the observed behavior as negating the value INT32_MIN has undefined behavior because of signed arithmetic overflow.此外,这种转换会适得其反,并且观察到的行为的可能原因是否定值INT32_MIN由于有符号算术溢出而具有未定义的行为。 With optimisations enabled, the compiler determines that you are extracting the sign as if by bool sign = -(int32_t)value < 0 and simplifies this expression as bool sign = (int32_t)value > 0 , which is correct for all values except INT32_MIN for which the compiler considers any behavior to be OK, since the behavior is undefined anyway.启用优化后,编译器确定您正在提取符号,就好像通过bool sign = -(int32_t)value < 0并将此表达式简化为bool sign = (int32_t)value > 0 ,这对于除INT32_MIN之外的所有值都是正确的编译器认为任何行为都可以,因为无论如何该行为都是未定义的。 You can check the code on Godbolt's Compiler Explorer .您可以在Godbolt 的 Compiler Explorer上查看代码。

  • you use type bool without including <stdbool.h> : the program should not compile.你使用bool类型而不包括<stdbool.h> :程序不应该编译。 Is this a copy/paste error or do you compile as c++?这是复制/粘贴错误还是您编译为 c++? The C99 _Bool semantics add an implicit test in the intialization statement, but it would be better to make it explicit and write: C99 _Bool语义在初始化语句中添加了一个隐式测试,但最好让它显式并编写:

     bool sign = (negation & 0x80000000);= 0;
  • finally, you pass uint32_t values to printf for %X conversion specifiers.最后,将uint32_t值传递给printf以获取%X转换说明符。 This is incorrect if the type int has fewer than 32 bits on your platform.如果平台上的int类型少于 32 位,则这是不正确的。 Use the macros from <inttypes.h> .使用<inttypes.h>中的宏。

Try this modified version:试试这个修改后的版本:

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <inttypes.h>

int main(int argc, const char **argv)
{
    uint32_t value = strtoul(argv[1], NULL, 16);
    uint32_t negation = -value;
    bool sign = (negation & 0x80000000) != 0;

    printf("value=%"PRIX32", negation=%"PRIX32", sign=%d\n", value, negation, sign);
    
    return 0;
}

Your unfortunate experience is rooted in the undefined behavior of signed arithmetic overflow.您不幸的经历源于有符号算术溢出的未定义行为。 Compilers can take advantage of undefined behavior to implement advanced optimisations such as removing the end test in for (int i = 0; i > 0; i++) as well as more obvious yet non-trivial ones such as converting void f(int i) { int j = i * 2 / 2; ...编译器可以利用未定义的行为来实现高级优化,例如删除for (int i = 0; i > 0; i++)中的最终测试以及更明显但非平凡的优化,例如转换void f(int i) { int j = i * 2 / 2; ... void f(int i) { int j = i * 2 / 2; ... to int j = i; void f(int i) { int j = i * 2 / 2; ...int j = i; which may exhibit different behavior for values exceeding 0x3fffffff .对于超过0x3fffffff的值,这可能会表现出不同的行为。

Other languages (ie: java) try and remove undefined behavior and fully specify two's complement implementation and behavior, hence will not perform these optimisations.其他语言(即:java)尝试删除未定义的行为并完全指定二进制补码实现和行为,因此不会执行这些优化。

The Standard C language committee seems to favor more optimisations at the cost of some surprises in border cases, which may be very difficult to spot and address.标准 C 语言委员会似乎支持更多的优化,但代价是边境案件中的一些意外情况,这可能很难发现和解决。 Your example is a perfect illustration of this problem.你的例子是这个问题的完美例证。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM