C/C++中有符号整数表达式的代数约简

Algebraic reductions of signed integer expressions in C/C++

本文关键字:表达式 整数 C++ 符号      更新时间:2023-10-16

我想看看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不是。

据我所知,无符号算术可以从下面的公式中推导出来

(a%n + b%n)%n = (a+b)%n

其可用于表明无符号算术是关联的。但是,由于有符号溢出是未定义的行为,这种等式不一定适用于有符号加法(即,有符号加法不是关联的(,这解释了为什么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假设有符号整数乘法是关联的。

这对我来说似乎是矛盾的。因为加法符号算术不是联想的,但乘法是。

两个问题:

  1. 加法和有符号整数不相关,而乘法是在C/C++中,这是真的吗?

  2. 如果有符号溢出用于优化,GCC不减少代数表达式的事实不是优化的失败吗?优化使用-fwrapv不是更好吗(我知道a - (b - c)(a + c) - b并没有减少多少,但我担心更复杂的情况(?这是否意味着优化有时使用-fwrapv更有效,有时则不然?

  1. 不,乘法在有符号整数中是不相关的。考虑(0 * x) * x0 * (x * x)——后者具有潜在的未定义行为,而前者总是被定义的。

  2. 未定义行为的潜力只会带来新的优化机会,经典的例子是为有符号x优化x + 1 > xtrue,这是一种不适用于无符号整数的优化。

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

事实上,实现有权假设算术是关联的,并将其用于优化,因为UB上可能发生任何事情,包括模算术或无限范围算术。然而,作为程序员无权假设关联性,除非您能够保证没有中间结果溢出。

作为另一个示例,try (a + a) - a-gcc将对有符号a和无符号a进行优化。

只要对任何定义的输入集具有相同的结果,就可以执行有符号整数表达式的代数约简。因此,如果表达式

a * a * a * a * a * a

定义——也就是说,a足够小,在计算过程中不会发生有符号溢出——那么乘法的任何重新分组都将产生相同的值,因为少于六个a的乘积都不会溢出。

CCD_ 32也是如此。

如果相乘(或相加(的变量不完全相同,或者加法与减法混合,情况就会发生变化。在这些情况下,重新组合和重新排列计算可能会导致有符号溢出,而这在规范计算中不会发生。

例如,以表达式为例

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做出这种假设。