协慌网

登录 贡献 社区

为什么 GCC 不优化 a * a * a * a * a * a 到(a * a * a)*(a * a * a)?

我正在对科学应用进行一些数值优化。我注意到的一件事是,GCC 将通过将其编译为a*a来优化呼叫pow(a,2) ,但是呼叫pow(a,6)未被优化并且实际上将调用库函数pow ,这大大减慢了表现。 (相比之下, 英特尔 C ++ 编译器 ,可执行的icc ,将消除对pow(a,6)的库调用。)

我很好奇的是,当我用a*a*a*a*a*a替换pow(a,6)时使用 GCC 4.5.1 和选项 “ -O3 -lm -funroll-loops -msse4 ”,它使用 5 个mulsd说明:

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13

如果我写(a*a*a)*(a*a*a) ,它会产生

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm13, %xmm13

这将乘法指令的数量减少到 3. icc具有类似的行为。

为什么编译器不能识别这种优化技巧?

答案

因为浮点数学不是关联的 。在浮点乘法中对操作数进行分组的方式会影响答案的数值准确性。

因此,大多数编译器对浮点计算的重新排序非常保守,除非他们能够确定答案保持不变,或者除非你告诉他们你不关心数值精度。例如:gcc -fassociative-math选项允许 gcc 重新关联浮点运算,甚至-ffast-math选项允许更准确地权衡速度。

Lambdageek正确地指出,因为关联性不适用于浮点数, a*a*a*a*a*a(a*a*a)*(a*a*a)的 “优化” 可能会改变价值。这就是 C99 不允许的原因(除非用户特别允许,通过编译器标志或编译指示)。一般来说,假设程序员为了某个原因编写了她所做的事情,编译器应该尊重这一点。如果你想要(a*a*a)*(a*a*a) ,那就写下来。

但是,这可能是一种痛苦; 当你使用pow(a,6)时,为什么编译器不能做 [你认为是什么] 正确的事情?因为这样做是错误的。在具有良好数学库的平台上, pow(a,6)a*a*a*a*a*a(a*a*a)*(a*a*a)明显更准确。为了提供一些数据,我在我的 Mac Pro 上运行了一个小实验,测量了 [1,2] 之间所有单精度浮点数的 ^ 6 评估中的最差错误:

worst relative error using    powf(a, 6.f): 5.96e-08
worst relative error using (a*a*a)*(a*a*a): 2.94e-07
worst relative error using     a*a*a*a*a*a: 2.58e-07

使用pow而不是乘法树可将误差限制为 4 倍 。编译器不应该(并且通常不会)进行 “优化” 以增加错误,除非用户许可这样做(例如通过-ffast-math )。

注意,GCC 提供__builtin_powi(x,n)作为pow( )的替代,它应该生成内联乘法树。如果您想要牺牲性能的准确性,但又不想启用快速数学运算,请使用它。

另一个类似的情况:大多数编译器不会优化a + b + c + d(a + b) + (c + d) (这是一个优化,因为第二个表达式可以更好地流水线化)并将其评估为给定(即 as (((a + b) + c) + d) )。这也是因为角落的情况:

float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e\n", a + b + c + d, (a + b) + (c + d));

输出1.000000e-05 0.000000e+00