Why doesn't GCC optimize a*a*a*a*a*a to (a*a*a)*(a*a*a)?

Why doesn’t GCC optimize aaaaaa to (aaa)(aaa)?

技术背景

在代码编写中,我们常常希望编译器能够进行优化,以提高代码的执行效率。例如,将 a*a*a*a*a*a 优化为 (a*a*a)*(a*a*a) ,这样可以减少乘法运算的次数。然而,GCC 编译器在处理这类优化时却较为保守,尤其是对于浮点数运算。

实现步骤

浮点数运算不优化的原因

浮点数运算不满足结合律,操作数的分组方式会影响结果的数值精度。例如:

1
2
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,说明不同的分组方式导致了不同的结果。因此,大多数编译器在重新排序浮点数计算时非常保守,除非能确保结果不变,或者用户明确表示不关心数值精度。

编译器选项

GCC 提供了一些选项来允许重新关联浮点运算,如 -fassociative-math 选项允许 GCC 重新关联浮点运算,-ffast-math 选项则允许更激进地在精度和速度之间进行权衡。

使用 pow 函数

在具有良好数学库的平台上,pow(a, 6)a*a*a*a*a*a(a*a*a)*(a*a*a) 更准确。例如:

1
2
3
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 倍。

自定义幂函数

可以使用模板元编程实现一个自定义的幂函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
}

使用方式为 power<6>(a)

整数运算优化

a 是整数时,GCC 会将 a*a*a*a*a*a 优化为 (a*a*a)*(a*a*a)。例如:

1
$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -

生成的汇编代码如下:

1
2
3
4
5
; 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

FP_CONTRACT 编译指示

ISO C 标准中的 FP_CONTRACT 编译指示允许编译器将表达式视为单个操作。如果将其设置为 ON,编译器可以用内部幂函数替换表达式,从而提高速度和精度。GCC 默认假设其为 ON,若要防止 a*b + c 转换为 fma(a, b, c),可以使用 -ffp-contract=off-std=c99 选项。

核心代码

自定义幂函数模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
}

最佳实践

  • 对于需要高精度的浮点数运算,优先使用 pow 函数。
  • 如果对精度要求不高,且希望提高性能,可以使用 -ffast-math 选项。
  • 对于整数运算,GCC 会自动进行优化,无需额外处理。
  • 在代码中合理使用自定义幂函数,以提高代码的可读性和性能。

常见问题

为什么 GCC 不默认对浮点数运算进行优化?

因为浮点数运算不满足结合律,优化可能会改变结果的精度,而编译器默认尊重程序员编写代码的意图,除非用户明确要求。

如何让 GCC 对浮点数运算进行优化?

可以使用 -fassociative-math-ffast-math 选项,但这些选项会牺牲一定的精度。

pow 函数和多次乘法运算哪个更准确?

在大多数情况下,pow 函数更准确,尤其是对于任意浮点数。但对于一些特殊值,多次乘法运算可能更准确。


Why doesn't GCC optimize a*a*a*a*a*a to (a*a*a)*(a*a*a)?
https://119291.xyz/posts/2025-05-16.why-doesnt-gcc-optimize-aaaaaa-to-aaa-aaa/
作者
ww
发布于
2025年5月16日
许可协议