C/C++ 中带符号整数表达式的代数约简
Posted
技术标签:
【中文标题】C/C++ 中带符号整数表达式的代数约简【英文标题】:Algebraic reductions of signed integer expressions in C/C++ 【发布时间】:2015-05-21 12:25:32 【问题描述】:我想看看 GCC 是否会使用有符号和无符号整数将 a - (b - c)
减少到 (a + c) - b
,所以我创建了两个测试
//test1.c
unsigned fooau(unsigned a, unsigned b, unsigned c) return a - (b - c);
signed fooas(signed a, signed b, signed c) return a - (b - c);
signed fooms(signed a) return a*a*a*a*a*a;
unsigned foomu(unsigned a) return a*a*a*a*a*a;
//test2.c
unsigned fooau(unsigned a, unsigned b, unsigned c) return (a + c) - b;
signed fooas(signed a, signed b, signed c) return (a + c) - b;
signed fooms(signed a) return (a*a*a)*(a*a*a);
unsigned foomu(unsigned a) return (a*a*a)*(a*a*a);
我首先用gcc -O3 test1.c test2.c -S
编译并查看了程序集。对于这两个测试,fooau
相同,但 fooas
不同。
据我所知,无符号算术可以从the following formula推导出来
(a%n + b%n)%n = (a+b)%n
可以用来表明无符号算术是结合的。但由于signed overflow is undefined behavior 这种等式不一定适用于有符号加法(即有符号加法不是关联的),这就解释了为什么 GCC 没有将有符号整数的 a - (b - c)
减少到 (a + c) - b
。但是我们可以通过-fwrapv
告诉 GCC 使用这个公式。对两个测试使用此选项fooas
是相同的。
但是乘法呢?对于两个测试,fooms
和 foomu
都被简化为三个乘法 (a*a*a*a*a*a to (a*a*a)*(a*a*a)
)。但是乘法可以写成重复加法,所以使用上面的公式我认为可以证明
((a%n)*(b%n))%n = (a*b)%n
我认为这也可以表明无符号模乘也是结合的。但由于 GCC 仅对 foomu
使用了三个乘法,这表明 GCC 假设有符号整数乘法是关联的。
这对我来说似乎很矛盾。对于加法,有符号算术不是关联的,但对于乘法,它是关联的。
两个问题:
加法与有符号整数无关,但乘法在 C/C++ 中是真的吗?
如果使用有符号溢出进行优化,GCC 不减少代数表达式是不是优化失败?使用-fwrapv
优化优化不是更好吗(我知道a - (b - c)
到(a + c) - b
并没有太大的减少,但我担心更复杂的情况)?这是否意味着有时使用-fwrapv
进行优化会更有效,有时却不是?
【问题讨论】:
如果您将身体设为a*a*a*a*a
,fooms
和 foomu
会发生什么 - 即。 奇数个乘法?他们仍然优化相同吗?对于偶数,符号无关紧要,因为结果总是正数。
@kdopen,它是一样的:fooms
和 foomu
产生相同的代码并对a*a*a*a*a
使用 3 次乘法。
【参考方案1】:
不,乘法在有符号整数中不具有关联性。考虑 (0 * x) * x
与 0 * (x * x)
- 后者可能具有未定义的行为,而前者始终是已定义的。
未定义行为的可能性只会引入新的优化机会,经典示例是针对已签名的x
将x + 1 > x
优化为true
,这是一种不适用于无符号整数。
我认为您不能假设 gcc 未能将 a - (b - c)
更改为 (a + c) - b
代表错过了优化机会;这两个计算在 x86-64 上编译为相同的两条指令(leal
和 subl
),只是顺序不同。
确实,实现有权假设算术是关联的,并将其用于优化,因为在 UB 上可能发生任何事情,包括模算术或无限范围算术。然而,你作为程序员无权假设关联性,除非你能保证没有中间结果溢出。
再举一个例子,试试(a + a) - a
- gcc 将把它优化为a
用于有符号的a
以及无符号的。
【讨论】:
为什么第一个有潜在的UB?因为值大? @G.Samaras 是的,x * x
可以溢出。
至于您的第二点,GCC 没有将 a - (b - c)
减少到 (a + c) - b
,因为已签名的溢出是 UB,但是当我使用 -fwrapv
时,这意味着已定义的已签名溢出行为确实会减少。这是否意味着 GCC 避免了由于 UB 的优化机会?
@Zboson 很难将重写视为减少,更不用说优化了 - 它访问相同的寄存器并使用相同的指令,只是顺序不同。它可能会在更大的执行中产生影响,但您没有为编译器提供机会来证明这一点。【参考方案2】:
可以执行有符号整数表达式的代数约简,前提是它对于任何定义输入集具有相同的结果。所以如果表达式
a * a * a * a * a * a
已定义——也就是说,a
足够小,以至于在计算过程中不会发生有符号溢出——然后乘法的任何重新组合都将产生相同的值,因为小于 6 个a
s 的乘积不能溢出。
a + a + a + a + a + a
也是如此。
如果相乘(或相加)的变量不完全相同,或者加法与减法混合在一起,情况就会发生变化。在这些情况下,重新组合和重新排列计算可能会导致有符号溢出,而这在规范计算中不会发生。
例如取表达式
a - (b - c)
在代数上相当于
(a + c) - b
但是编译器不能进行这种重新排列,因为中间值a+c
可能会溢出不会导致原始值溢出的输入。假设我们有a=INT_MAX-1; b=1; c=2;
然后a+c
导致溢出,但a - (b - c)
计算为a - (-1)
,即INT_MAX
,没有溢出。
如果编译器可以假设有符号溢出不会捕获,而是以 INT_MAX+1
为模计算,那么这些重新排列是可能的。 -fwrapv
选项允许 gcc 做出这样的假设。
【讨论】:
但是a*a*a*a*a*a
肯定会溢出,fooms
必须假设任何a
,即使没有-fwrapv
,GCC 也会减少这种情况。
@Zboson: GCC 可以假设a*a*a*a*a*a
不会溢出,因为如果它溢出会导致未定义的行为。 (可以这么说,未定义的行为取消了所有义务。)因此,GCC 只需要在规范计算((((((a*a)*a)*a)*a)*a)*a)
不会发生溢出的情况下确保一致性。如果在这种情况下没有发生溢出,则计算(((a*a)*a)*((a*a)*a))
不会发生溢出,因此替换是有效的。
谢谢!我想我现在明白了。由于 GCC 可以假设不会发生溢出,因此它可以根据需要使用无限范围的算术,这是关联的。我还在学习 C。
@Zboson:这是未定义行为最重要的一点;飞蜥的图像色彩鲜艳,但通常不相关。始终允许编译器假设编写的程序不会产生未定义的行为,并且可以(并且确实)根据该假设进行优化。
@zboson:编译器可以假设编写的程序不会溢出。也就是说,程序员计算 a、b 和 c 的方式保证了使用这些变量的表达式不会导致 UB。不能假设程序中没有的其他表达式不会导致 UB。以上是关于C/C++ 中带符号整数表达式的代数约简的主要内容,如果未能解决你的问题,请参考以下文章
setState Dart/Flutter 中带有花括号的胖箭头符号