提问人:xis 提问时间:6/22/2011 最后编辑:Suraj Jainxis 更新时间:9/13/2019 访问量:233559
为什么 GCC 不将 a*a*a*a*a*a*a 优化为 (a*a*a)*(a*a*a)?
Why doesn't GCC optimize a*a*a*a*a*a to (a*a*a)*(a*a*a)?
问:
我正在对科学应用进行一些数值优化。我注意到的一件事是 GCC 会通过将调用编译成 来优化调用,但调用没有优化,实际上会调用库函数,这大大降低了性能。(相比之下,英特尔 C++ 编译器(可执行文件)将消除库调用。pow(a,2)
a*a
pow(a,6)
pow
icc
pow(a,6)
我很好奇的是,当我用 GCC 4.5.1 和选项 “” 替换时,它使用了 5 条指令:pow(a,6)
a*a*a*a*a*a
-O3 -lm -funroll-loops -msse4
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
评论
pow
pow
-fp-model precise
clang
gcc
-fassociative-math
a*a*a*a*a*a
(a*a*a)*(a*a*a)
-fassociative-math
Lambdageek 正确地指出,由于关联性不适用于浮点数,因此 to 的“优化”可能会改变该值。这就是 C99 不允许它的原因(除非用户通过编译器标志或编译指示明确允许)。一般来说,假设程序员写她所做的事情是有原因的,编译器应该尊重这一点。如果你愿意,就写。a*a*a*a*a*a
(a*a*a)*(a*a*a)
(a*a*a)*(a*a*a)
不过,写起来可能会很痛苦;为什么编译器不能在使用 时做 [你认为是] 正确的事情?因为这样做是错误的。在具有良好数学库的平台上,比 或 或 准确得多。为了提供一些数据,我在 Mac Pro 上做了一个小实验,测量了 [1,2] 之间所有单精度浮点数的 a^6 计算的最严重误差:pow(a,6)
pow(a,6)
a*a*a*a*a*a
(a*a*a)*(a*a*a)
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
使用乘法树代替乘法树可将误差范围降低 4 倍。编译器不应该(通常也不会)进行增加错误的“优化”,除非用户许可这样做(例如通过)。pow
-ffast-math
请注意,GCC 提供了 的替代方法,它应该生成一个内联乘法树。如果您想在精度与性能之间进行权衡,但又不想启用快速数学运算,请使用它。__builtin_powi(x,n)
pow( )
评论
_set_SSE2_enable(<flag>)
flag=1
pow
pow
a*a*a*a*a*a
另一个类似的情况是:大多数编译器不会优化到(这是一种优化,因为第二个表达式可以更好地流水线化)并按照给定的方式对其进行评估(即作为)。这也是因为极端情况:a + b + c + d
(a + b) + (c + d)
(((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
评论
因为 32 位浮点数(例如 1.024)不是 1.024。在计算机中,1.024 是一个区间:从 (1.024-e) 到 (1.024+e),其中“e”表示错误。有些人没有意识到这一点,并且还认为 a*a 中的 * 代表任意精度数字的乘法,这些数字没有任何错误。有些人没有意识到这一点的原因可能是他们在小学时进行的数学计算:只使用理想数字而不附加错误,并认为在执行乘法时简单地忽略“e”是可以的。他们没有看到“float a=1.2”、“a*a*a”和类似的 C 代码中隐含的“e”。
如果大多数程序员认识到(并能够执行)C 表达式 a*a*a*a*a*a 实际上不适用于理想数字的想法,那么 GCC 编译器就可以自由地将“a*a*a*a*a*a”优化为“t=(a*a);t*t*t“,这需要较少的乘法次数。但不幸的是,GCC 编译器不知道编写代码的程序员是否认为“a”是一个有或没有错误的数字。因此,GCC 只会做源代码的样子——因为这就是 GCC 用“肉眼”看到的。
...一旦你知道你是什么样的程序员,你就可以使用“-ffast-math”开关告诉 GCC“嘿,GCC,我知道我在做什么!这将允许 GCC 将 a*a*a*a*a*a 转换为不同的文本片段 - 它看起来与 a*a*a*a*a*a*a 不同 - 但仍会在 a*a*a*a*a*a*a 的错误间隔内计算一个数字。这没关系,因为您已经知道您正在使用间隔,而不是理想的数字。
评论
int x = 3
x
Distance
Fortran(专为科学计算而设计)有一个内置的幂运算符,据我所知,Fortran 编译器通常会以与您描述的类似的方式优化整数幂的提升。不幸的是,C/C++ 没有幂运算符,只有库函数。这并不妨碍智能编译器在特殊情况下进行特殊处理并以更快的方式计算它,但似乎它们不太常见......pow()
pow
几年前,我试图以最佳方式更方便地计算整数幂,并提出了以下方法。虽然它是 C++,而不是 C,但仍然取决于编译器在如何优化/内联方面有些聪明。无论如何,希望您在实践中会发现它有用:
template<unsigned N> struct power_impl;
template<unsigned N> struct power_impl {
template<typename T>
static T calc(const T &x) {
if (N%2 == 0)
return power_impl<N/2>::calc(x*x);
else if (N%3 == 0)
return power_impl<N/3>::calc(x*x*x);
return power_impl<N-1>::calc(x)*x;
}
};
template<> struct power_impl<0> {
template<typename T>
static T calc(const T &) { return 1; }
};
template<unsigned N, typename T>
inline T power(const T &x) {
return power_impl<N>::calc(x);
}
对好奇者的澄清:这并没有找到计算能力的最佳方法,但由于找到最佳解决方案是一个 NP 完全问题,而且无论如何这只值得为小功率做(而不是使用 pow
),没有理由在细节上大惊小怪。
然后将其用作 .power<6>(a)
这使得键入幂变得容易(无需用括号拼写出 6 秒),并允许您进行这种优化,而无需使用依赖于精度的东西,例如补偿求和(一个操作顺序至关重要的示例)。a
-ffast-math
您可能还会忘记这是 C++,只需在 C 程序中使用它(如果它使用 C++ 编译器编译)。
希望这能有用。
编辑:
这是我从编译器那里得到的:
为a*a*a*a*a*a
movapd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm1, %xmm0
为(a*a*a)*(a*a*a)
movapd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm0, %xmm0
为power<6>(a)
mulsd %xmm0, %xmm0
movapd %xmm0, %xmm1
mulsd %xmm0, %xmm1
mulsd %xmm0, %xmm1
评论
正如 Lambdageek 所指出的,浮点数乘法不是关联的,你可以得到更低的准确性,但当获得更好的准确性时,你可以反对优化,因为你想要一个确定性的应用程序。例如,在游戏模拟客户端/服务器中,每个客户端都必须模拟您希望浮点计算具有确定性的相同世界。
评论
这个问题已经有一些很好的答案,但为了完整起见,我想指出 C 标准的适用部分是 5.1.2.2.3/15(与 C++11 标准中的第 1.9/9 节相同)。本节指出,只有当运算符真正具有关联性或可交换性时,才能重新组合运算符。
评论
GCC 实际上会优化到 a 为整数时。我尝试了这个命令:a*a*a*a*a*a
(a*a*a)*(a*a*a)
$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -
有很多 gcc 标志,但没什么花哨的。它们的意思是:从 stdin 读取;使用 O2 优化级别;输出汇编语言列表而不是二进制文件;列表应使用英特尔汇编语言语法;输入是C语言(通常语言是从输入文件扩展名推断出来的,但从stdin读取时没有文件扩展名);并写入 stdout。
这是输出的重要部分。我已经用一些注释注释了它,表明汇编语言中发生了什么:
; x is in edi to begin with. eax will be used as a temporary register.
mov eax, edi ; temp = x
imul eax, edi ; temp = x * temp
imul eax, edi ; temp = x * temp
imul eax, eax ; temp = temp * temp
我在 Linux Mint 16 Petra 上使用系统 GCC,这是 Ubuntu 的衍生产品。下面是 gcc 版本:
$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1
正如其他海报所指出的,此选项在浮点运算中是不可能的,因为浮点算术不是关联的。
评论
unsigned int
a*a*a*a*a*a
(a*a*a)*(a*a*a)
目前还没有海报提到浮动表达式的收缩(ISO C 标准,6.5p8 和 7.12.2)。如果编译指示设置为 ,则允许编译器将表达式(例如单个操作)视为单个操作,就好像使用单个舍入精确计算一样。例如,编译器可以用更快、更准确的内部幂函数代替它。这特别有趣,因为该行为部分由程序员直接在源代码中控制,而最终用户提供的编译器选项有时可能会被错误地使用。FP_CONTRACT
ON
a*a*a*a*a*a
编译指示的默认状态是实现定义的,因此默认情况下允许编译器执行此类优化。因此,需要严格遵循 IEEE 754 规则的可移植代码应将其显式设置为 。FP_CONTRACT
OFF
如果编译器不支持此编译指示,则必须通过避免任何此类优化来保守,以防开发人员选择将其设置为 .OFF
GCC 不支持此编译指示,但使用默认选项时,它假定它是 ;因此,对于具有硬件 FMA 的目标,如果要防止转换为 fma(a,b,c),则需要提供一个选项,例如(将编译指示显式设置为 )或(告诉 GCC 符合某些 C 标准版本,这里是 C99,因此遵循上面的段落)。过去,后一种选择并不能阻止转型,这意味着GCC在这一点上不符合要求:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845ON
a*b+c
-ffp-contract=off
OFF
-std=c99
评论
像“pow”这样的库函数通常是经过精心设计的,以产生尽可能小的误差(在一般情况下)。这通常是通过样条曲线近似函数来实现的(根据 Pascal 的评论,最常见的实现似乎是使用 Remez 算法)
从根本上说,以下操作:
pow(x,y);
其固有误差的大小与任何单次乘法或除法中的误差大致相同。
而以下操作:
float a=someValue;
float b=a*a*a*a*a*a;
其固有误差大于单次乘法或除法误差的 5 倍以上(因为您要组合 5 次乘法)。
编译器应该非常小心它正在做的优化类型:
- 如果对其进行优化可能会提高性能,但会大大降低浮点数的准确性。
pow(a,6)
a*a*a*a*a*a
- 如果对其进行优化,实际上可能会降低准确性,因为“a”是一些特殊值,允许乘法而不会出错(2 的幂或一些小整数)
a*a*a*a*a*a
pow(a,6)
- 如果优化到或与功能相比,仍然可能会损失准确性。
pow(a,6)
(a*a*a)*(a*a*a)
(a*a)*(a*a)*(a*a)
pow
一般来说,您知道对于任意浮点值,“pow”比您最终可以编写的任何函数都具有更高的准确性,但是在某些特殊情况下,多次乘法可能具有更好的准确性和性能,这取决于开发人员选择更合适的方法,最终注释代码,以便其他人不会“优化”该代码。
唯一有意义的(个人意见,显然是 GCC 中任何特定优化或编译器标志的选择)进行优化应该是将“pow(a,2)”替换为“a*a”。这将是编译器供应商应该做的唯一理智的事情。
评论
GCC 实际上可以进行这种优化,即使是浮点数也是如此。例如
double foo(double a) {
return a*a*a*a*a*a;
}
成为
foo(double):
mulsd %xmm0, %xmm0
movapd %xmm0, %xmm1
mulsd %xmm0, %xmm1
mulsd %xmm1, %xmm0
ret
跟。但是,这种重新排序违反了 IEEE-754,因此它需要标志。-O -funsafe-math-optimizations
正如 Peter Cordes 在评论中指出的那样,有符号整数可以在没有的情况下进行这种优化,因为它恰好在没有溢出时成立,如果有溢出,则会得到未定义的行为。所以你得到-funsafe-math-optimizations
foo(long):
movq %rdi, %rax
imulq %rdi, %rax
imulq %rdi, %rax
imulq %rax, %rax
ret
只有.对于无符号整数,这甚至更容易,因为它们的模幂为 2,因此即使面对溢出也可以自由重新排序。-O
评论
-ffast-math
)
评论
(a*a)*(a*a)*(a*a)