(A + B + C)≠(A + C + B)和编译器重新排序

Tal*_*Tal 110 c c++ compiler-construction integer-overflow

添加两个32位整数会导致整数溢出:

uint64_t u64_z = u32_x + u32_y;
Run Code Online (Sandbox Code Playgroud)

如果首先将32位整数中的一个整流或添加到64位整数,则可以避免此溢出.

uint64_t u64_z = u32_x + u64_a + u32_y;
Run Code Online (Sandbox Code Playgroud)

但是,如果编译器决定重新排序添加:

uint64_t u64_z = u32_x + u32_y + u64_a;
Run Code Online (Sandbox Code Playgroud)

整数溢出可能仍会发生.

是否允许编译器进行这样的重新排序,或者我们是否可以相信它们会注意到结果不一致并保持表达顺序不变?

Kla*_*äck 84

如果优化器进行了这样的重新排序,它仍然绑定到C规范,因此这样的重新排序将变为:

uint64_t u64_z = (uint64_t)u32_x + (uint64_t)u32_y + u64_a;
Run Code Online (Sandbox Code Playgroud)

理由:

我们一开始

uint64_t u64_z = u32_x + u64_a + u32_y;
Run Code Online (Sandbox Code Playgroud)

添加从左到右进行.

整数提升规则表明在原始表达式的第一次添加中,u32_x被提升为uint64_t.在第二次加入,u32_y也将晋升为uint64_t.

因此,为了要符合与C说明书中,任何优化器必须促进u32_xu32_y64位无符号值.这相当于添加一个演员表.(实际的优化不是在C级别完成的,但我使用C表示法,因为这是我们理解的符号.)

  • @Useless:Klas确实将所有内容都投入了64位.现在订单根本没有任何区别.编译器不需要遵循关联性,它只需要生成完全相同的结果. (12认同)
  • 它似乎表明OP的代码将被评估,这是不正确的. (2认同)

Mar*_*ica 28

只允许编译器在as规则下重新排序.也就是说,如果重新排序将始终给出与指定排序相同的结果,则允许它.否则(如你的例子),不是.

例如,给出以下表达式

i32big1 - i32big2 + i32small
Run Code Online (Sandbox Code Playgroud)

已精心构造减去已知要大,但相似的两个值,并且只有然后添加其他小的值(从而避免任何上溢),编译器可以选择重新排序成:

(i32small - i32big2) + i32big1
Run Code Online (Sandbox Code Playgroud)

并且依赖于目标平台正在使用带有环绕的双补码算法以防止出现问题.(如果编译器被按下寄存器,那么这种重新排序可能是明智的,并且恰好已经存在i32small于寄存器中).

  • @chux:像UB这样的其他问题没有发挥作用,因为我们讨论的是在as-if规则下重新排序的编译器.特定编译器可以利用知道其自身的溢出行为. (3认同)

gna*_*729 16

在C,C++和Objective-C中有"似乎"规则:编译器可以做任何它喜欢的事情,只要没有一致的程序可以区分它们.

在这些语言中,a + b + c被定义为与(a + b)+ c相同.如果你可以区分它和例如a +(b + c),那么编译器就无法改变顺序.如果你不能区分,那么编译器可以自由改变顺序,但这没关系,因为你无法区分.

在你的例子中,当b = 64位,a和c 32位时,编译器将被允许评估(b + a)+ c或甚至(b + c)+ a,因为你无法区分,但是不是(a + c)+ b,因为你可以分辨出来.

换句话说,不允许编译器执行任何使您的代码与其应有的行为不同的操作.不需要生成您认为会生成的代码,或者您认为它应该生成的代码,但代码将为您提供应该完成的结果.

  • @csiz ...在_signed_变量上.无符号变量具有明确定义的溢出语义(环绕). (7认同)

Rah*_*thi 7

引用标准:

[注意:操作符可以按照通常的数学规则重新分组,只有操作符真正是关联的或可交换的.7例如,在下面的片段中,int a,b;

/? ... ?/
a = a + 32760 + b + 5;
Run Code Online (Sandbox Code Playgroud)

表达式语句的行为完全相同

a = (((a + 32760) + b) + 5);
Run Code Online (Sandbox Code Playgroud)

由于这些运算符的关联性和优先级.因此,总和(a + 32760)的结果接下来被添加到b,然后该结果被添加到5,这导致分配给a的值.在溢出产生异常并且int可表示的值范围为[-32768,+ 32767]的机器上,实现不能将此表达式重写为

a = ((a + b) + 32765);
Run Code Online (Sandbox Code Playgroud)

因为如果a和b的值分别是-32754和-15,则a + b和将产生异常而原始表达式不会产生异常; 也不能将表达式重写为

a = ((a + 32765) + b);
Run Code Online (Sandbox Code Playgroud)

要么

a = (a + (b + 32765));
Run Code Online (Sandbox Code Playgroud)

因为a和b的值可能分别是4和-8或-17和12.但是在溢出不产生异常并且溢出结果是可逆的机器上,上面的表达式语句可以由上述任何方式的实现重写,因为会出现相同的结果. - 结束说明]