为什么 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)?

提问人:xis 提问时间:6/22/2011 最后编辑:Suraj Jainxis 更新时间:9/13/2019 访问量:233559

问:

我正在对科学应用进行一些数值优化。我注意到的一件事是 GCC 会通过将调用编译成 来优化调用,但调用没有优化,实际上会调用库函数,这大大降低了性能。(相比之下,英特尔 C++ 编译器(可执行文件)将消除库调用。pow(a,2)a*apow(a,6)powiccpow(a,6)

我很好奇的是,当我用 GCC 4.5.1 和选项 “” 替换时,它使用了 5 条指令:pow(a,6)a*a*a*a*a*a-O3 -lm -funroll-loops -msse4mulsd

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 汇编 浮点 编译器优化 快速数学

评论

16赞 Varun Madiath 6/22/2011
“识别 pow(a,6)”是什么意思?
707赞 Damon 6/22/2011
呃......你知道 a a a aa a 和(a a a)*(a a*a)与浮点数不同,不是吗?您必须使用 -funsafe-math 或 -ffast-math 或其他东西。
210赞 12/21/2012
一个完全合理的问题。20 年前,我问了同样的一般性问题,通过打破这个单一的瓶颈,将蒙特卡罗模拟的执行时间从 21 小时缩短到 7 小时。在这个过程中,内部循环中的代码被执行了13万亿次,但它使模拟进入了一个通宵的窗口。(见下面的答案)
132赞 Phil Armstrong 6/22/2011
我建议你读一读大卫·戈德堡(David Goldberg)的《每个计算机科学家都应该知道的浮点运算》(What Every Computer Scientist Should Know About Floating Point Arithmetic)一书:download.oracle.com/docs/cd/E19957-01/806-3568/......之后,你就会对你刚刚走进的焦油坑有更完整的了解!
33赞 Rok Kralj 8/12/2015
也许也可以加入其中。相同的乘法次数,但可能更准确。(a*a)*(a*a)*(a*a)

答:

26赞 Mark Ransom 6/22/2011 #1

我根本没想到这个案例会得到优化。表达式包含可以重新分组以删除整个操作的子表达式的情况并不常见。我希望编译器编写者将时间投入到更有可能带来明显改进的领域,而不是涵盖很少遇到的边缘情况。

我很惊讶地从其他答案中得知,这个表达式确实可以通过适当的编译器开关进行优化。要么是微不足道的优化,要么是更常见的优化的边缘情况,要么是编译器编写者非常彻底。

像你在这里所做的那样,向编译器提供提示并没有错。在微观优化过程中,重新排列语句和表达式以查看它们会带来什么差异是正常且预期的部分。

虽然编译器可能有理由考虑这两个表达式以提供不一致的结果(没有适当的开关),但您没有必要受到该限制的约束。差异将非常小 - 如此之大,以至于如果差异对您很重要,您首先不应该使用标准浮点运算。

评论

18赞 Alice 6/30/2014
正如另一位评论者所指出的那样,这是不真实的,以至于荒谬;差异可能高达成本的一半到10%,如果在紧密循环中运行,这将转化为许多指令的浪费,以获得可能微不足道的额外精度。说你在做蒙特卡洛时不应该使用标准的 FP,有点像说你应该总是使用飞机穿越国家;它忽略了许多外部性。最后,这不是一个罕见的优化;死代码分析和代码缩减/重构非常普遍。
2912赞 6 revs, 3 users 86%Lambdageek #2

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

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

评论

16赞 xis 6/22/2011
是的。使用 -ffast-math,它正在进行这样的优化。好主意!但是,由于我们的代码关注的准确性高于速度,因此最好不要通过它。
25赞 tc. 6/22/2011
IIRC C99 允许编译器进行这种“不安全”的 FP 优化,但 GCC(在 x87 以外的任何设备上)都合理地尝试遵循 IEEE 754 - 它不是“误差边界”;正确答案只有一个
16赞 Stephen Canon 1/3/2013
的实现细节既不在这里也不在那里;这个答案甚至没有参考.powpow
19赞 Stephen Canon 3/28/2014
@nedR:ICC 默认允许重新关联。如果要获得符合标准的行为,则需要使用 ICC 进行设置。 并默认为严格符合 w.r.t. 重新关联。-fp-model preciseclanggcc
66赞 Paul Draper 8/25/2014
@xis,这并不是真的不准确;仅此而已,又不一样。这与准确性无关;它关乎标准一致性和严格可重复的结果,例如,在任何编译器上都得到相同的结果。浮点数已经不准确了。使用 编译 很少不合适。-fassociative-matha*a*a*a*a*a(a*a*a)*(a*a*a)-fassociative-math
710赞 Stephen Canon 6/22/2011 #3

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( )

评论

33赞 TkTech 6/23/2011
另请注意,Visual C++ 提供了 pow() 的“增强”版本。如果可能,通过调用 ,它将使用 SSE2。这会稍微降低准确性,但会提高速度(在某些情况下)。MSDN:_set_SSE2_enable()pow()_set_SSE2_enable(<flag>)flag=1
23赞 Stephen Canon 6/23/2011
@TkTech:任何精度的降低都是由于Microsoft的实现,而不是所用寄存器的大小。如果库编写者有这样的动力,则可以仅使用 32 位寄存器提供正确的舍入。有些基于 SSE 的实现比大多数基于 x87 的实现准确,也有一些实现会牺牲一些准确性来换取速度。powpow
12赞 Stephen Canon 6/23/2011
@TkTech:当然,我只是想澄清一下,准确性的降低是由于图书馆作者所做的选择,而不是使用 SSE 所固有的。
10赞 j_random_hacker 9/25/2013
我很想知道你在这里用什么作为计算相对误差的“黄金标准”——我通常认为会是这样,但显然不是这样!:)a*a*a*a*a*a
12赞 Stephen Canon 9/25/2013
@j_random_hacker:由于我比较的是单精度结果,因此双精度对于黄金标准来说就足够了——以双精度计算的 a a a aaa的误差远远小于任何单精度计算的误差。
193赞 sanjoyd 6/23/2011 #4

另一个类似的情况是:大多数编译器不会优化到(这是一种优化,因为第二个表达式可以更好地流水线化)并按照给定的方式对其进行评估(即作为)。这也是因为极端情况: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

评论

14赞 CoffeDeveloper 7/7/2014
这并不完全相同。Changin 乘法/除法顺序(不包括除以 0)比 Changin 求和/减法顺序更安全。以我的拙见,编译器应该尝试关联 mults./divs.,因为这样做可以减少操作总数,除了性能提升之外,还可以提高精度。
10赞 Ben Voigt 3/5/2015
@DarioOO:这并不安全。乘法和除法与指数的加法和减法相同,改变顺序很容易导致临时值超过指数的可能范围。(不完全相同,因为指数不会遭受精度损失......但是表示仍然非常有限,重新排序可能会导致无法表示的值)
13赞 CoffeDeveloper 3/5/2015
我认为你缺少一些微积分背景。将 2 个数字进行多除和除以会引入相同数量的误差。虽然减去/加 2 个数字可能会引入更大的误差,尤其是当 2 个数字相差一个数量级时,因此重新排列 mul/divide 比 sub/add 更安全,因为它引入了最终误差的微小变化。
12赞 Peter Cordes 7/30/2015
@DarioOO:mul/div 的风险不同:重新排序要么使最终结果的变化可以忽略不计,要么指数在某个点溢出(以前不会发生),结果大不相同(可能是 +inf 或 0)。
1赞 curiousguy 9/30/2019
@GameDeveloper 以不可预测的方式强加精度增益是一个巨大的问题。
51赞 user811773 6/23/2011 #5

因为 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 的错误间隔内计算一个数字。这没关系,因为您已经知道您正在使用间隔,而不是理想的数字。

评论

66赞 Donal Fellows 6/24/2011
浮点数是精确的。它们不一定完全符合您的预期。此外,epsilon 技术本身就是如何处理现实中问题的近似值,因为真正的预期误差是相对于尾数的尺度的,也就是说,你通常最多可以输出大约 1 LSB,但如果你不小心,每次执行操作都会增加,所以在做任何非平凡的浮点运算之前,请咨询数值分析师。如果可能的话,使用适当的库。
4赞 supercat 11/18/2012
@DonalFellows:IEEE 标准要求浮点计算产生的结果与源操作数为精确值时的结果最精确匹配,但这并不意味着它们实际上表示精确值。在许多情况下,将 0.1f 视为 (1,677,722 +/- 0.5)/16,777,216 更有帮助,它应该以该不确定性所暗示的十进制位数显示,而不是将其视为精确数量 (1,677,722 +/- 0.5)/16,777,216(应显示为 24 位十进制数字)。
31赞 Stephen Canon 1/4/2013
@supercat:IEEE-754 非常清楚地表明浮点数据确实代表了精确值;第 3.2 - 3.4 条是相关部分。当然,你可以选择以其他方式解释它们,就像你可以选择解释为3+/-0.5的含义一样。int x = 3x
8赞 Stephen Canon 1/5/2013
@supercat:我完全同意,但这并不意味着它不完全等于它的数值;这意味着数值只是正在建模的某个物理量的近似值。Distance
13赞 gnasher729 3/11/2014
对于数值分析,如果你不是将浮点数解释为区间,而是解释为精确值(恰好不是你想要的值),你的大脑会感谢你。例如,如果 x 在 4.5 左右的某个地方,误差小于 0.1,并且您计算 (x + 1) - x,则“区间”解释会留下一个从 0.8 到 1.2 的区间,而“精确值”解释会告诉您结果将为 1,双精度误差最多为 2^(-50)。
89赞 Szabolcs 6/23/2011 #6

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

评论

37赞 Marc Glisse 2/1/2013
找到最佳幂树可能很困难,但由于它只对小幂感兴趣,显而易见的答案是预先计算一次(Knuth 提供了一个最多 100 的表)并使用该硬编码表(这就是 gcc 在内部为 powi 所做的)。
7赞 gnasher729 3/11/2014
在现代处理器上,速度受到延迟的限制。例如,乘法的结果可能在五个周期后可用。在这种情况下,找到最快的方法来产生一些力量可能会更加棘手。
3赞 gnasher729 3/11/2014
您还可以尝试查找给出相对舍入误差的最低上限或最低平均相对舍入误差的幂树。
1赞 gast128 8/3/2017
Boost 也支持此功能,例如 boost::math::p ow<6>(n);我认为它甚至试图通过提取公因数来减少乘法次数。
1赞 tobi_s 8/17/2021
这是 Fortran 做出正确选择的情况之一(编译器可以使用关联性,除非用户使用括号,这是一种众所周知的表示法来表示评估顺序),而 C 做出了错误的选择(没有办法进行关联数学)
31赞 Bjorn 6/23/2011 #7

正如 Lambdageek 所指出的,浮点数乘法不是关联的,你可以得到更低的准确性,但当获得更好的准确性时,你可以反对优化,因为你想要一个确定性的应用程序。例如,在游戏模拟客户端/服务器中,每个客户端都必须模拟您希望浮点计算具有确定性的相同世界。

评论

3赞 Alice 9/7/2014
@greggo 不,它仍然是确定性的。在任何意义上都没有添加随机性。
9赞 greggo 9/8/2014
@Alice 似乎相当清楚的是,Bjorn在这里使用了“确定性”,即代码在不同的平台和不同的编译器版本等(可能超出程序员控制范围的外部变量)上给出相同的结果,而不是在运行时缺乏实际的数字随机性。如果你指出这不是这个词的正确用法,我不会反驳这一点。
5赞 Alice 9/10/2014
@greggo 除了你对他所说的话的解释,它仍然是错误的;这就是 IEEE 754 的全部意义所在,即为大多数(如果不是全部)跨平台操作提供相同的特性。现在,他没有提到平台或编译器版本,如果您希望每个远程服务器/客户端上的每个操作都相同,这将是一个合理的问题。但这从他的声明中并不明显。一个更好的词可能是“可靠相似”或其他什么。
8赞 Lanaru 12/3/2014
@Alice你通过争论语义来浪费每个人的时间,包括你自己的时间。他的意思很明确。
12赞 Alice 12/8/2014
@Lanaru 标准的全部要点是语义;他的意思显然不清楚。
24赞 Rastaban 10/2/2013 #8

这个问题已经有一些很好的答案,但为了完整起见,我想指出 C 标准的适用部分是 5.1.2.2.3/15(与 C++11 标准中的第 1.9/9 节相同)。本节指出,只有当运算符真正具有关联性或可交换性时,才能重新组合运算符。

评论

0赞 gnasher729 11/21/2022
或者你可以说编译器必须从左到右执行这些操作,即取 a,乘以 a 五倍,但编译器可以自由地替换任何产生完全相同结果的代码。例如,a+a+a = 3*a(总是给出相同的结果)。
73赞 picomancer 3/29/2014 #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

正如其他海报所指出的,此选项在浮点运算中是不可能的,因为浮点算术不是关联的。

评论

21赞 Peter Cordes 7/30/2015
这对于整数乘法来说是合法的,因为 2 的补码溢出是未定义的行为。如果发生溢出,无论重新排序操作如何,它都会在某个地方发生。因此,没有溢出的表达式的计算结果相同,溢出的表达式是未定义的行为,因此编译器可以更改溢出发生点。GCC 也使用 来做到这一点。unsigned int
3赞 Daniel McLaury 10/2/2020
@PeterCordes:我认为它合法的一个更好的理由是,与浮点乘法不同,整数乘法(mod n)是关联的。当然,有符号整型溢出仍然是未定义的行为,但假装不是,你总是会从 和 得到相同的结果。(当然,对于无符号类型,溢出无论如何都不是 UB。a*a*a*a*a*a(a*a*a)*(a*a*a)
1赞 Peter Cordes 10/2/2020
@DanielMcLaury:哦,是的,我没有说明这个关键要求。:P显然,早在 2015 年,我认为每个人都已经知道了这一点,或者正在谈论可能的 UB,在确定实际整数结果是相同的之后,这可能是一个担忧。(OTOH,我想我记得我见过一个案例,GCC 没有像无符号一样优化有符号整数数学,因为一些过于保守的“不要引入 UB”逻辑,当最终结果相同时,这是没有意义的。
43赞 vinc17 6/28/2014 #10

目前还没有海报提到浮动表达式的收缩(ISO C 标准,6.5p8 和 7.12.2)。如果编译指示设置为 ,则允许编译器将表达式(例如单个操作)视为单个操作,就好像使用单个舍入精确计算一样。例如,编译器可以用更快、更准确的内部幂函数代替它。这特别有趣,因为该行为部分由程序员直接在源代码中控制,而最终用户提供的编译器选项有时可能会被错误地使用。FP_CONTRACTONa*a*a*a*a*a

编译指示的默认状态是实现定义的,因此默认情况下允许编译器执行此类优化。因此,需要严格遵循 IEEE 754 规则的可移植代码应将其显式设置为 。FP_CONTRACTOFF

如果编译器不支持此编译指示,则必须通过避免任何此类优化来保守,以防开发人员选择将其设置为 .OFF

GCC 不支持此编译指示,但使用默认选项时,它假定它是 ;因此,对于具有硬件 FMA 的目标,如果要防止转换为 fma(a,b,c),则需要提供一个选项,例如(将编译指示显式设置为 )或(告诉 GCC 符合某些 C 标准版本,这里是 C99,因此遵循上面的段落)。过去,后一种选择并不能阻止转型,这意味着GCC在这一点上不符合要求:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845ONa*b+c-ffp-contract=offOFF-std=c99

评论

3赞 Pascal Cuoq 6/28/2014
长寿的热门问题有时会显示他们的年龄。这个问题在2011年被提出并回答,当时GCC可以原谅不完全遵守当时最新的C99标准。当然现在是 2014 年,所以 GCC......咳咳。
0赞 Pascal Cuoq 6/28/2014
但是,您不应该在没有接受答案的情况下回答相对较新的浮点问题吗?咳嗽 stackoverflow.com/questions/23703408 咳嗽
0赞 David Monniaux 11/19/2016
我找到了...令人不安的是,gcc 没有实现 C99 浮点编译指示。
1赞 Tim Seguine 9/4/2018
根据定义,@DavidMonniaux编译指示是可选的。
2赞 vinc17 9/4/2018
@TimSeguine 但是,如果未实现编译指示,则其默认值需要是实现时限制性最强的值。我想这就是大卫在想的。在 GCC 中,如果使用 ISO C 模式,这个问题现在已修复FP_CONTRACT:它仍然不实现编译指示,但在 ISO C 模式下,它现在假定编译指示已关闭。
32赞 CoffeDeveloper 1/4/2015 #11

像“pow”这样的库函数通常是经过精心设计的,以产生尽可能小的误差(在一般情况下)。这通常是通过样条曲线近似函数来实现的(根据 Pascal 的评论,最常见的实现似乎是使用 Remez 算法)

从根本上说,以下操作:

pow(x,y);

其固有误差的大小与任何单次乘法或除法中的误差大致相同

而以下操作:

float a=someValue;
float b=a*a*a*a*a*a;

其固有误差大于单次乘法或除法误差的 5 倍以上(因为您要组合 5 次乘法)。

编译器应该非常小心它正在做的优化类型:

  1. 如果对其进行优化可能会提高性能,但会大大降低浮点数的准确性。pow(a,6)a*a*a*a*a*a
  2. 如果对其进行优化,实际上可能会降低准确性,因为“a”是一些特殊值,允许乘法而不会出错(2 的幂或一些小整数)a*a*a*a*a*apow(a,6)
  3. 如果优化到或与功能相比,仍然可能会损失准确性。pow(a,6)(a*a*a)*(a*a*a)(a*a)*(a*a)*(a*a)pow

一般来说,您知道对于任意浮点值,“pow”比您最终可以编写的任何函数都具有更高的准确性,但是在某些特殊情况下,多次乘法可能具有更好的准确性和性能,这取决于开发人员选择更合适的方法,最终注释代码,以便其他人不会“优化”该代码。

唯一有意义的(个人意见,显然是 GCC 中任何特定优化或编译器标志的选择)进行优化应该是将“pow(a,2)”替换为“a*a”。这将是编译器供应商应该做的唯一理智的事情。

评论

7赞 CoffeDeveloper 1/4/2015
反对者应该意识到这个答案是完全可以的。我可以引用数十个来源和文档来支持我的答案,而且我可能比任何反对者都更多地参与浮点精度。在 StackOverflow 中添加其他答案未涵盖的缺失信息是完全合理的,因此请礼貌并解释您的原因。
3赞 Pascal Cuoq 1/4/2015
在我看来,斯蒂芬·佳能(Stephen Canon)的回答涵盖了您要说的内容。您似乎坚持认为 libms 是用样条实现的:它们更典型地使用参数约简(取决于正在实现的函数)加上单个多项式,其系数已由 Remez 算法或多或少的复杂变体获得。对于libm函数来说,连接点的平滑度不被认为是一个值得追求的目标(如果它们最终足够准确,那么无论域被分割成多少块,它们都会自动变得非常平滑)。
3赞 Pascal Cuoq 1/4/2015
你答案的后半部分完全忽略了编译器应该生成实现源代码所说的代码的要点。此外,当您表示“准确性”时,您也会使用“精确”一词。
0赞 CoffeDeveloper 1/4/2015
感谢您的输入,我稍微更正了答案,最后 2 行中仍然存在一些新内容^^
15赞 Charles 6/17/2016 #12

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

评论

2赞 Peter Cordes 6/17/2016
带有 double、int 和 unsigned 的 Godbolt 链接。GCC 和 Clang 都以相同的方式优化这三者(使用-ffast-math)