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


108

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

uint64_t u64_z = u32_x + u32_y;

如果首先将32位整数之一转换为64位整数或将其添加到64位整数,则可以避免这种溢出。

uint64_t u64_z = u32_x + u64_a + u32_y;

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

uint64_t u64_z = u32_x + u32_y + u64_a;

整数溢出仍可能发生。

是否允许编译器进行这种重新排序,还是我们可以信任它们以注意到结果不一致并按原样保留表达式顺序?


15
您实际上并没有显示整数溢出,因为您似乎被添加了uint32_t值-它们不会溢出,而是溢出。这些不是不同的行为。
Martin Bonner

5
请参阅c ++标准的1.9节,它可以直接回答您的问题(甚至有一个示例几乎与您的示例完全相同)。
Holt

3
@Tal:正如其他人已经指出的那样:没有整数溢出。unsigned定义为自动换行,对于signed,则为undefined行为,因此任何实现都可以使用,包括nasal守护程序。
对此网站来说太老实了

5
@Tal:废话!正如我已经写道:标准是很清楚,需要包装,不饱和(这将是可能的签约,因为这是UB标准AS-的。
太老实了这个网站

15
@rustyx:无论您称其为换行还是溢出,该点仍然会((uint32_t)-1 + (uint32_t)1) + (uint64_t)0导致0,而(uint32_t)-1 + ((uint32_t)1 + (uint64_t)0)导致0x100000000,并且这两个值不相等。因此,编译器是否可以应用该转换非常重要。但是,是的,该标准仅对有符号整数使用“溢出”一词,而不是无符号整数。
史蒂夫·杰索普

Answers:


84

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

uint64_t u64_z = (uint64_t)u32_x + (uint64_t)u32_y + u64_a;

理由:

我们从

uint64_t u64_z = u32_x + u64_a + u32_y;

从左到右执行加法。

整数提升规则规定,在原始表达式的第一个加法中,u32_x将提升为uint64_t。在第二个中,u32_y还将提升为uint64_t

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


是不是左联想,所以(u32_x + u32_t) + u64_a
没用的2016年

12
@Useless:Klas确实将所有内容都转换为64位。现在,顺序根本没有任何区别。编译器不需要遵循关联性,它只需要产生与产生完全相同的结果即可。
gnasher729

2
似乎暗示OP的代码将像这样被评估,这是不正确的。
没用的2016年

@Klas-仔细解释为什么会这样,以及如何精确地得出代码示例?
rustyx 2016年

1
@rustyx它确实需要解释。感谢您推动我添加一个。
克拉斯·林德贝克

28

编译器只允许按照asif规则重新排序。也就是说,如果重新排序将始终提供与指定顺序相同的结果,则允许重新排序。否则(如您的示例所示),否则。

例如,给定以下表达式

i32big1 - i32big2 + i32small

经过精心构造,它减去了两个已知较大但相似的值,然后再加上另一个较小的值(从而避免了任何溢出),编译器可以选择重新排序为:

(i32small - i32big2) + i32big1

并依赖于目标平台使用带回合的双补码算法来防止问题的事实。(如果对编译器施压以获取寄存器,并且碰巧已经存在i32small于寄存器中,那么这样的重新排序可能是明智的)。


OP的示例使用无符号类型。i32big1 - i32big2 + i32small表示带符号的类型。其他问题也起作用。
chux-恢复莫妮卡

@chux绝对。我试图说明的一点是,尽管我无法编写(i32small-i32big2) + i32big1(因为它可能导致UB),但是编译器可以对其进行有效地重新排列,因为编译器可以确信该行为是正确的。
Martin Bonner

3
@chux:诸如UB之类的其他问题并没有发挥作用,因为我们正在讨论的是按常规规则对编译器进行重新排序。特定的编译器可能会利用了解自己的溢出行为的优势。
MSalters '16

16

在C,C ++和Objective-C中有“好像”规则:只要没有合格的程序可以分辨出差异,编译器就可以做任何喜欢的事情。

在这些语言中,将a + b + c定义为与(a + b)+ c相同。如果您能分辨出它与例如+(b + c)之间的区别,那么编译器将无法更改顺序。如果您无法分辨出差异,则编译器可以自由更改顺序,但这很好,因为您无法分辨出差异。

在您的示例中,使用b = 64位,a和c 32位,允许编译器求值(b + a)+ c甚至(b + c)+ a,因为您无法分辨出区别,但是不是(a + c)+ b,因为您可以分辨出差异。

换句话说,不允许编译器执行任何使您的代码行为与应有的行为不同的事情。不需要生成您认为会生成的代码,也不必生成您认为应该生成的代码,但是该代码将为您提供应有的结果。


但是有一个很大的警告;编译器可以自由地假定没有未定义的行为(在这种情况下为溢出)。这类似于如何if (a + 1 < a)优化溢出检查。
csiz

7
@csiz ...关于有符号变量。无符号变量具有定义明确的溢出语义(环绕)。
加文·扬西

7

引用标准

[注意:只有在运算符确实是关联或可交换的情况下,才可以根据通常的数学规则对运算符进行重组。

/∗ ... ∗/
a = a + 32760 + b + 5;

表达式语句的行为与

a = (((a + 32760) + b) + 5);

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

a = ((a + b) + 32765);

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

a = ((a + 32765) + b);

要么

a = (a + (b + 32765));

因为a和b的值可能分别是4和-8或-17和12。但是,在其中溢出不会产生异常并且溢出结果可逆的机器上,上述表达式语句可以由实现以上述任何一种方式重写,因为将发生相同的结果。—尾注]


4

是否允许编译器进行这种重新排序,还是我们可以信任它们以注意到结果不一致并按原样保留表达式顺序?

编译器只有在给出相同结果的情况下才能重新排序-如您所见,此处没有。


如果需要的话,可以编写一个函数模板,该函数模板会std::common_type在添加之前将所有参数都升级为一个模板-这是安全的,并且不依赖于参数顺序或手动强制转换,但是它很笨重。


我知道应该使用显式转换,但是我希望知道当错误地省略了这种转换时的编译器行为。
2016年

1
就像我说的,没有显式的强制转换:左加法是首先执行的,没有整体提升,因此需要包装。该结果,添加,可能是包裹的,是升任uint64_t为除了最右边的值。
没用的2016年

您对假设规则的解释是完全错误的。例如,C语言指定了抽象计算机上应该执行的操作。只要没有人能分辨出差异,“按条件”规则就可以绝对地做任何想要的事情。
gnasher729

这意味着只要结果与所示的左联想和算术转换规则所确定的结果相同,编译器就可以执行所需的任何操作。
没用的2016年

1

它取决于的位宽unsigned/int

下面的2个不相同(当unsigned <= 32位时)。 u32_x + u32_y变为0。

u64_a = 0; u32_x = 1; u32_y = 0xFFFFFFFF;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + u32_y + u64_a;  // u32_x + u32_y carry does not add to sum.

它们是相同的(当 unsigned >= 34位)。整数促销造成的 u32_x + u32_y在64位数学上进行加法运算。顺序无关紧要。

它是UB(unsigned == 33位)。整数提升导致在加号的33位数学上进行加法运算,并且加号的溢出为UB。

是否允许编译器进行这种重新排序...?

(32位数学运算):重新排序是,但是必须发生相同的结果,因此OP 不会建议重新排序。下面是一样的

// Same
u32_x + u64_a + u32_y;
u64_a + u32_x + u32_y;
u32_x + (uint64_t) u32_y + u64_a;
...

// Same as each other below, but not the same as the 3 above.
uint64_t u64_z = u32_x + u32_y + u64_a;
uint64_t u64_z = u64_a + (u32_x + u32_y);

...我们可以让他们注意到结果不一致并按原样保留表达式顺序吗?

相信是的,但是OP的编码目标尚不清楚。应该u32_x + u32_y携带捐款吗?如果OP希望做出贡献,则代码应为

uint64_t u64_z = u64_a + u32_x + u32_y;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + (u32_y + u64_a);

但不是

uint64_t u64_z = u32_x + u32_y + u64_a;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.