我正在对科学应用进行一些数值优化。我注意到的一件事是,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