(x+1) > x 如何计算为 0 和 1?

How can (x+1) > x evaluate to both 0 and 1?

我正在了解未定义的行为,并且在没有任何明确解释的情况下偶然发现了这段代码:

#include <stdio.h>
#include <limits.h>

int foo ( int x) {
    printf ("% d\n" ,  x );   //2147483647
    printf ("% d\n" ,  x+1 ); //-2147483648  overflow
    return ( x+1 ) > x ;      // 1 but How????
}

int main ( void ) {
    printf ("% d\n" ,  INT_MAX );     //2147483647
    printf ("% d\n" ,  INT_MAX+1 );   //-2147483648  overflow
    printf ("% d\n" , ( INT_MAX+1 ) > INT_MAX );  //0  makes sense, since -ve < +ve
    printf ("% d\n" ,  foo(INT_MAX) );  //1
    return 0;
}

在gcc上编译时,编译器发出警告:

warning: integer overflow in expression of type 'int' results in '-2147483648'

所以,显然 INT_MAX+1 的值是负数,这解释了为什么 (INT_MAX+1) > INT_MAX 的计算结果为 0。

但是,对于 foo(...) 中的 x = INT_MAX(x+1) > x 为什么(或如何)评估为 1

当程序表现出 undefined behavior 时,C 标准不会预测程序将做什么。程序可能会崩溃,可能会输出奇怪的结果,或者看起来工作正常。

事实上,编译器通常会假设程序不包含未定义的行为。

在这个表达式的情况下:

( x+1 ) > x 

鉴于 x 的类型为 int,编译器知道有符号溢出是 UB 并且在它不会发生的假设下工作。考虑到这一点,x 在这个表达式可能为假的地方没有值,因此编译器可以优化表达式并将其替换为值 1.

当我在 gcc 4.8.5 下 运行 这个程序时,我得到以下 -O0-O1 的结果:

 2147483647
-2147483648
 0
 2147483647
-2147483648
 0

以及 -O2-O3 的以下内容:

 2147483647
-2147483648
 0
 2147483647
-2147483648
 1

然后在后一种情况下查看 foo 的程序集:

foo:
.LFB11:
    .file 1 "x1.c"
    .loc 1 4 0
    .cfi_startproc
.LVL0:
    pushq   %rbx                // first call to printf
    .cfi_def_cfa_offset 16
    .cfi_offset 3, -16
    .loc 1 5 0
    movl    %edi, %esi
    .loc 1 4 0
    movl    %edi, %ebx
    .loc 1 5 0
    xorl    %eax, %eax
    movl    $.LC0, %edi
.LVL1:
    call    printf
.LVL2:
    .loc 1 6 0                  // second call to printf
    leal    1(%rbx), %esi
    movl    $.LC0, %edi
    xorl    %eax, %eax
    call    printf
.LVL3:
    .loc 1 8 0                  // return value
    movl    , %eax
    popq    %rbx
    .cfi_def_cfa_offset 8
.LVL4:
    ret
    .cfi_endproc

我们可以看到这正是编译器所做的:它优化了比较并始终 returns 1.

这说明了编译器如何利用未定义的行为来应用各种优化。

编写标准时,传统体系结构的编译器通常会以环绕二进制补码的方式执行整数运算,但有时做其他事情可能更有用。举几个例子:

  1. 如果已知某个程序不会故意导致整数溢出,那么在溢出时设置一个实现陷阱要比输出表面上有效但错误的输出要好。

  2. 即使在普通平台上,有时像使用比指定类型更宽的类型来执行算术也是有利的。例如,在 8086 上,乘法指令将采用两个 16 位操作数并产生 32 位结果,因此在执行像 int32a=int16a*int16b+int32b; 这样的计算时,保留 32 位乘法结果比使用符号扩展指令将结果的低 16 位提升为 32 位。此外,该抽象模型将允许简化多种表达式,例如将 (x*30/15) 替换为 (x*2),或(如示例所示)将 x+y > x 替换为 y > 0 .

与其尝试猜测可能对实现处理整数溢出有用的所有方式,或者冒着阻止实现以客户认为最有用的方式处理整数溢出的风险,标准让实现选择任何方式他们认为最有用的方法。 gcc 的作者已经决定,处理整数溢出最有用的方法是使用它来产生不受正常因果律约束的扩展推理。

考虑,例如:

unsigned arr[32771];
unsigned mul_mod_32768(unsigned short x, unsigned short y)
{
    /* Note that the authors of the Standard specified that the multiply
       here happens as signed, because--according to the Rationale--they
       expected that commonplace implementations would process signed and
       unsigned math identically in cases like this! */
    return (x * y) & 0x7FFFu;
}
void test(unsigned short n)
{
    unsigned total=0;        
    unsigned short s2=65535;
    for (unsigned short i=32768; i < n; i++)
    {
        total += mul_mod_32768(i, 65535);
    }
    if (n < 32770)
        arr[n] = total;
}

在优化级别 2 或 3,gcc 将为 test() 生成的代码恰好等同于:

void test(unsigned short n)
{
    arr[n] = 0;
}

如果 n 等于或小于 32768,则根本不会 运行 循环,total 将为零,total 将存储到 arr[n] 中。如果n为32769,则循环运行一次,total加0,存入arr[n]。如果 n 为 32770 或更大,则标准不会强加任何要求,因此 gcc 将以与处理其他情况相同的方式处理这些情况,盲目地将零存储到 arr[n].

该标准有意不尝试禁止专门用于特定狭窄目的的实现以使其不适合许多其他目的的方式运行。 gcc 在这里的行为可能适用于处理仅来自可靠来源的数据的程序,但这并不意味着它应该被视为适用于其他任何东西。不幸的是,clang 和 gcc 寻求处理的语言与 C 标准委员会特许描述的语言非常不同。