(A + B + C) ≠ (A + C + B ) 和编译器重新排序
(A + B + C) ≠ (A + C + B) and compiler reordering
两个 32 位整数相加会导致整数溢出:
uint64_t u64_z = u32_x + u32_y;
如果先将其中一个 32 位整数转换或添加到 64 位整数,则可以避免这种溢出。
uint64_t u64_z = u32_x + u64_a + u32_y;
但是,如果编译器决定重新排序加法:
uint64_t u64_z = u32_x + u32_y + u64_a;
整数溢出仍有可能发生。
是否允许编译器进行这样的重新排序,或者我们是否可以相信他们会注意到结果不一致并保持表达式顺序不变?
如果优化器进行这样的重新排序,它仍然受限于 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_x
和 u32_y
提升为 64 位无符号值。这相当于加了一个cast。 (实际优化不是在 C 级别完成的,但我使用 C 表示法,因为这是我们理解的表示法。)
在 C、C++ 和 Objective-C 中有 "as if" 规则:编译器可以为所欲为,只要没有符合标准的程序可以区分。
在这些语言中,a + b + c 的定义与 (a + b) + c 相同。如果你能分辨出这和 a + (b + c) 之间的区别,那么编译器就不能改变顺序。如果您无法区分,那么编译器可以随意更改顺序,但这没关系,因为您无法区分。
在您的示例中,b = 64 位,a 和 c 为 32 位,编译器可以计算 (b + a) + c 甚至 (b + c) + a,因为您不能区分,但不是 (a + c) + b 因为你可以区分。
换句话说,不允许编译器做任何使您的代码行为与其应有的行为不同的事情。不需要生成您认为它会生成的代码,或者您认为它应该生成的代码,但代码 将 准确地为您提供它应该生成的结果。
编译器只允许在 as if 规则下重新排序。也就是说,如果重新排序总是会给出与指定排序相同的结果,那么它是允许的。否则(如您的示例),不是。
例如,给定以下表达式
i32big1 - i32big2 + i32small
经过精心构造以减去已知较大但相似的两个值,并且仅然后添加另一个较小的值(从而避免任何溢出),编译器可能会选择重新排序为:
(i32small - i32big2) + i32big1
并依赖于目标平台使用带循环的二补码算法来防止出现问题。 (如果编译器需要寄存器,并且恰好寄存器中已经有 i32small
,那么这种重新排序可能是明智的)。
Are compilers allowed to do such a reordering or can we trust them to notice the result inconsistency and keep the expression order as is?
编译器只有在给出相同结果的情况下才能重新排序 - 在这里,正如您所观察到的,它不会。
如果需要,可以编写一个函数模板,在添加之前将所有参数提升为 std::common_type
- 这将是安全的,并且不依赖于参数顺序或手动转换,但它很漂亮笨重。
取决于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。
Are compilers allowed to do such a reordering ...?
(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);
... can we trust them to notice the result inconsistency and keep the expression order as is?
相信是的,但 OP 的编码目标并不 crystal 明确。 u32_x + u32_y
应该carry贡献吗?如果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;
引自standards:
[ Note: Operators can be regrouped according to the usual
mathematical rules only where the operators really are associative or
commutative.7 For example, in the following fragment int a, b;
/∗ ... ∗/
a = a + 32760 + b + 5;
the expression statement behaves exactly the same as
a = (((a + 32760) + b) + 5);
due to the associativity and precedence of these operators. Thus, the
result of the sum (a + 32760) is next added to b, and that result is
then added to 5 which results in the value assigned to a. On a machine
in which overflows produce an exception and in which the range of
values representable by an int is [-32768,+32767], the implementation
cannot rewrite this expression as
a = ((a + b) + 32765);
since if the values for a and b were, respectively, -32754 and -15,
the sum a + b would produce an exception while the original expression
would not; nor can the expression be rewritten either as
a = ((a + 32765) + b);
or
a = (a + (b + 32765));
since the values for a and b might have been, respectively, 4 and -8
or -17 and 12. However on a machine in which overflows do not produce
an exception and in which the results of overflows are reversible, the
above expression statement can be rewritten by the implementation in
any of the above ways because the same result will occur. — end note ]
两个 32 位整数相加会导致整数溢出:
uint64_t u64_z = u32_x + u32_y;
如果先将其中一个 32 位整数转换或添加到 64 位整数,则可以避免这种溢出。
uint64_t u64_z = u32_x + u64_a + u32_y;
但是,如果编译器决定重新排序加法:
uint64_t u64_z = u32_x + u32_y + u64_a;
整数溢出仍有可能发生。
是否允许编译器进行这样的重新排序,或者我们是否可以相信他们会注意到结果不一致并保持表达式顺序不变?
如果优化器进行这样的重新排序,它仍然受限于 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_x
和 u32_y
提升为 64 位无符号值。这相当于加了一个cast。 (实际优化不是在 C 级别完成的,但我使用 C 表示法,因为这是我们理解的表示法。)
在 C、C++ 和 Objective-C 中有 "as if" 规则:编译器可以为所欲为,只要没有符合标准的程序可以区分。
在这些语言中,a + b + c 的定义与 (a + b) + c 相同。如果你能分辨出这和 a + (b + c) 之间的区别,那么编译器就不能改变顺序。如果您无法区分,那么编译器可以随意更改顺序,但这没关系,因为您无法区分。
在您的示例中,b = 64 位,a 和 c 为 32 位,编译器可以计算 (b + a) + c 甚至 (b + c) + a,因为您不能区分,但不是 (a + c) + b 因为你可以区分。
换句话说,不允许编译器做任何使您的代码行为与其应有的行为不同的事情。不需要生成您认为它会生成的代码,或者您认为它应该生成的代码,但代码 将 准确地为您提供它应该生成的结果。
编译器只允许在 as if 规则下重新排序。也就是说,如果重新排序总是会给出与指定排序相同的结果,那么它是允许的。否则(如您的示例),不是。
例如,给定以下表达式
i32big1 - i32big2 + i32small
经过精心构造以减去已知较大但相似的两个值,并且仅然后添加另一个较小的值(从而避免任何溢出),编译器可能会选择重新排序为:
(i32small - i32big2) + i32big1
并依赖于目标平台使用带循环的二补码算法来防止出现问题。 (如果编译器需要寄存器,并且恰好寄存器中已经有 i32small
,那么这种重新排序可能是明智的)。
Are compilers allowed to do such a reordering or can we trust them to notice the result inconsistency and keep the expression order as is?
编译器只有在给出相同结果的情况下才能重新排序 - 在这里,正如您所观察到的,它不会。
如果需要,可以编写一个函数模板,在添加之前将所有参数提升为 std::common_type
- 这将是安全的,并且不依赖于参数顺序或手动转换,但它很漂亮笨重。
取决于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。
Are compilers allowed to do such a reordering ...?
(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);
... can we trust them to notice the result inconsistency and keep the expression order as is?
相信是的,但 OP 的编码目标并不 crystal 明确。 u32_x + u32_y
应该carry贡献吗?如果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;
引自standards:
[ Note: Operators can be regrouped according to the usual mathematical rules only where the operators really are associative or commutative.7 For example, in the following fragment int a, b;
/∗ ... ∗/ a = a + 32760 + b + 5;
the expression statement behaves exactly the same as
a = (((a + 32760) + b) + 5);
due to the associativity and precedence of these operators. Thus, the result of the sum (a + 32760) is next added to b, and that result is then added to 5 which results in the value assigned to a. On a machine in which overflows produce an exception and in which the range of values representable by an int is [-32768,+32767], the implementation cannot rewrite this expression as
a = ((a + b) + 32765);
since if the values for a and b were, respectively, -32754 and -15, the sum a + b would produce an exception while the original expression would not; nor can the expression be rewritten either as
a = ((a + 32765) + b);
or
a = (a + (b + 32765));
since the values for a and b might have been, respectively, 4 and -8 or -17 and 12. However on a machine in which overflows do not produce an exception and in which the results of overflows are reversible, the above expression statement can be rewritten by the implementation in any of the above ways because the same result will occur. — end note ]