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 是相同的。

但是乘法呢?对于两个测试,foomsfoomu 都被简化为三个乘法 (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*afoomsfoomu 会发生什么 - 即。 奇数个乘法?他们仍然优化相同吗?对于偶数,符号无关紧要,因为结果总是正数。 @kdopen,它是一样的:foomsfoomu 产生相同的代码并对a*a*a*a*a 使用 3 次乘法。 【参考方案1】:

    不,乘法在有符号整数中不具有关联性。考虑 (0 * x) * x0 * (x * x) - 后者可能具有未定义的行为,而前者始终是已定义的。

    未定义行为的可能性只会引入新的优化机会,经典示例是针对已签名的xx + 1 > x 优化为true,这是一种不适用于无符号整数。

我认为您不能假设 gcc 未能将 a - (b - c) 更改为 (a + c) - b 代表错过了优化机会;这两个计算在 x86-64 上编译为相同的两条指令(lealsubl),只是顺序不同。

确实,实现有权假设算术是关联的,并将其用于优化,因为在 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 个as 的乘积不能溢出。

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++ 中带符号整数表达式的代数约简的主要内容,如果未能解决你的问题,请参考以下文章

Matlab在线性代数中的应用:相似矩阵及二次型

setState Dart/Flutter 中带有花括号的胖箭头符号

java中怎么将字符串(带运算符号加减乘除)转换成代数算式运算

MATLAB的符号运算变量如何创建?

急!matlab solve用法

数据库 关系代数中 join 的意思是啥 怎么用