我正在做一些科学应用的数值优化。我注意到的一点是,gcc会通过编译成a*a来优化调用pow(a,2),但是调用pow(a,6)没有优化,实际上会调用库函数pow,这大大降低了性能。(相反,英特尔C++编译器,可执行的EDOCX1,4),将消除对EDCOX1(2)的库调用。
我好奇的是,当我使用GCC 4.5.1和选项"-O3 -lm -funroll-loops -msse4"替换pow(a,6)和a*a*a*a*a*a时,它使用了5个mulsd指令:
1 2 3 4 5 6
| movapd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm14, %xmm13 |
如果我写(a*a*a)*(a*a*a),它会产生
1 2 3 4
| movapd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm13, %xmm13 |
它将乘法指令的数目减少到3。icc有类似的行为。
为什么编译器不认识这种优化技巧?
- "承认战俘(A,6)"是什么意思?
- 我很惊讶gcc没有优化这一点。我在CDC赛博上使用的20世纪70年代的Fortran编译器进行了这种转换,即使没有选择优化。我认为UnixV6(c.1978)C编译器在启用优化时就完成了这项工作,尽管它所做的许多优化都是为了节省代码空间,这在当时是一种宝贵的商品。
- 嗯。。。你知道a aa aaa和(aaa)*(a a*a)与浮点数不一样,是吗?你必须用-funsafe数学或-ffast数学之类的。
- 我建议你阅读大卫·戈德伯格的《每一个计算机科学家都应该知道的关于浮点运算的知识》:download.oracle.com/docs/cd/e19957-01/806-3568/…之后,你将对你刚刚走进的沥青坑有一个更完整的了解!
- 一个完全合理的问题。20年前,我提出了同样的一般性问题,通过突破这个瓶颈,蒙特卡洛模拟的执行时间从21小时缩短到了7小时。在这个过程中,内环中的代码被执行了13万亿次,但是它把模拟带入了一个过夜窗口。(见下面的答案)
- 也可以把(a*a)*(a*a)*(a*a)扔进混合物中。相同数量的乘法,但可能更准确。
- 首先,如何进行优化很大程度上取决于a的类型…
- 无耻的插头:除了戈德伯格的论文,我建议阅读我的,hal.archives ouvertes.fr/file/index/docid/281429/filename/…
- @事实上,一个好的优化器可以让这一点更进一步。A*A只需要做一次。这个结果可以很容易地重用,从而将其减少到只有3个乘法运算。
- @企鹅359:是的,3,和(a*a*a)*(a*a*a)完全一样,这是我建议的替代方案。你想说什么?
因为浮点数学不是关联的。对浮点乘法中的操作数进行分组的方式对答案的数值精度有影响。
因此,大多数编译器对于重新排序浮点计算非常保守,除非他们可以确保答案保持不变,或者除非您告诉他们您不关心数字精度。例如:GCC的-fassociative-math选项允许GCC重新关联浮点运算,甚至允许-ffast-math选项允许更积极地权衡精度与速度。
- 是的。它是一个ffast数学做搜索优化。好主意!但因为我们的代码的准确性比速度更多的关注,它可能是最好不要通过它。
- 这个C99编译器可以做搜索"不安全"的GCC(FP基准,但在任何其他比在合理的尝试使助记符)以下IEEE 754 -它不是"错误激活";有正确的答案只有一个。
- 外部的细节,无论是在powNOR有答案;这甚至没有pow参考。
- "lohoris,我的论点的是,任何常规性收敛,在同样的浮点硬件作为一系列的multiplies,不可能更准确。这是真正的痛苦变成收敛,因为作弊程序,一个很好的方式。他们之间的预先计算的值是已知的interpolate表2边界上的权力。因此,在插值的误差是非常小的。所以我想告诉你,为小的权力,电力类函数在整数6,只是为好。如果你是安静的,I’m a编译器的作家,当然你会把那些战俘(在基准)。
- 所以我的问题是……那算是表演意味着英特尔编译器的优化的准确性和正确性的AT的费用?或是找到另一个有用的方式来确保正确的时间吗?
- nedr:缺省为"ICC之间的重新组合。如果你要协调行为的标准,你需要设置一个-fp-model preciseICC。clang到严格的一致性w.r.t. gcc违约和重组。
- "东西,它真的是很-fassociative-mathinaccurrate;只是那a*a*a*a*a*a和(a*a*a)*(a*a*a)是不同的。它不是关于它的准确性,一致性和严格的标准repeatable结果相同的结果,例如在任何编译器。精确的浮点数是个困境。它是一个seldomly不慎-fassociative-math编译。
- 如果你想选择的准确性,(AAA)*(AAA)。可能的原因是更多的大学参与平衡计分卡的尺寸operands,AAAAA(>a>(大于),和操作数减少,截断。
lambdageek正确地指出,由于浮点数不具有关联性,因此a*a*a*a*a*a到(a*a*a)*(a*a*a)的"优化"可能会改变该值。这就是为什么C99不允许它(除非用户特别允许,通过编译器标志或pragma)。一般来说,假设是程序员写了她所做的事情是有原因的,编译器应该尊重这一点。如果你想要(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进行评估时的最大误差:
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倍的误差。除非用户(如通过-ffast-math许可)进行"优化",否则编译器不应(通常也不应)进行增加错误的"优化"。
注意,GCC提供__builtin_powi(x,n)作为pow( )的替代方案,它应该生成一个内联乘法树。如果你想用精确性来换取性能,但又不想启用快速数学,那么就使用它。
- 注因此,Visual C++提供的增强的版本(战俘)。通过调用一个_set_SSE2_enable()flag=1它想使用SSE2,如果可能的。这减少了在A位的精度,但在某些情况下,创建的速度)。的MSDN:_集SSE2 _ _使战俘()和()
- "xis19:一个良好的数学库,同样要保持双(实际上,任何支持浮点型)。
- "tktech:使用SSE2需要精度不减少的困境,甚至。最现代的数学库使用SSE在x86,当它可用的,和许多人提供非常准确的结果。
- "史蒂芬:MSDN文档本身有可能注意当使用SSE2指令精简精度(即是由于他们是默认的)是在登记册80bit中级和当使用64位浮点SSE2。
- "tktech:任何还原精度是由于微软的实现,需要使用的寄存器的大小。它可能提供一正确的圆形pow仅用32位的寄存器,如果图书馆的作家是这样的动机。我们的实现是基于SSE pow更准确比最助记符基实现,因此,有一些实现的权衡对速度的精度。
- @斯蒂芬:当然,你可以不使用8位寄存器和一个小数组来实现更精确的方法。但是,我仍然在具体地讨论VisualC++的POWER()实现。"可能"和"可能"不是"是":)
- @当然,我只是想澄清一下,准确性的降低是由图书馆作者的选择造成的,而不是SSE的固有使用。
- 我有兴趣知道你在这里用什么作为计算相对误差的"黄金标准"——我通常以为是a*a*a*a*a*a,但显然不是这样!:)
- @随机黑客:因为我比较了单精度的结果,双精度足以满足黄金标准——双精度计算的AAAAAA误差比任何单精度计算的误差都要小得多。
- @Stephencanon-三条评论。(a)+ 1。这是一个非常好的答案。(b)更好的金本位:std::pow((long double)a,6)。(c)有第三种方法:中间计算采用双精度,例如通过power<6,double>(a)调用szabolcs的power模板函数。现在,您可以得到半个ULP精度(作为float结果),但性能损失很小(比a*a*a*a*a*a作为float结果长1.4倍)。与调用std::pow(float,float)所导致的巨大性能损失(我的机器上的时间延长了32.4倍)相比。
- @Davidhammen long double对MSVC的实现没有任何作用,而且不止一个;它们键入pun long double到just double。在说这是一个更好的黄金标准之前,你需要确保长双得到很好的支持。
- 也可以把(a*a)*(a*a)*(a*a)扔进混合物中。相同数量的乘法,但可能更准确。
- "一般来说,假设是程序员写她所做的事情是有原因的,编译器应该尊重这一点。如果你想要(aaa)*(aaa),写下它,"基于这个推理,所有的事情都可以/应该被忘记,因为第一个支持宏的汇编程序……
另一个类似的例子是:大多数编译器不会将a + b + c + d优化为(a + b) + (c + d)(这是一种优化,因为第二个表达式可以更好地进行管道化),并按照给定的方式对其进行评估(即,作为(((a + b) + c) + d))。这也是因为角落案例:
1 2 3
| float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e
", a + b + c + d, (a + b) + (c + d)); |
输出1.000000e-05 0.000000e+00。
- 这不完全一样。按乘法/除法的顺序(不包括0除法)比按和/减法的顺序进行更改更安全。在我看来,编译器应该尝试将mults./divs关联起来。因为这样做减少了操作的总数,除了性能增益之外,还有一个精度增益。
- @达里奥:不安全。乘法和除法与指数的加法和减法相同,更改顺序很容易导致时间超出指数的可能范围。(不完全相同,因为指数不受精度损失…但是表示仍然很有限,重新排序可能导致不可表示的值)
- 我想你缺少一些微积分的背景。多个数和两个数的除法会产生相同的误差。当减法/加法2个数可能会带来更大的误差,特别是当2个数的数量级不同时,因此重新排列mul/除法比重新排列sub/加法更安全,因为它会导致最终误差的微小变化。
- @darioo:使用mul/div:reordering的风险是不同的:要么对最终结果进行微不足道的更改,要么指数在某个点上溢(以前不会出现),结果则大不相同(可能是+inf或0)。
Fortran(为科学计算而设计)有一个内置的幂运算符,据我所知,Fortran编译器通常会以与您描述的类似的方式优化提升为整数幂。C+C++不幸的是没有一个幂算子,只有库函数EDCOX1〔0〕。这并不能阻止智能编译器专门处理pow,并在特殊情况下以更快的方式计算它,但似乎它们不太常见…
几年前,我试图使以最佳方式计算整数幂更为方便,并提出了以下几点。它是C++,而不是C,它仍然依赖于编译器在如何优化/内嵌东西方面有点聪明。不管怎样,希望你能在实践中发现它有用:
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);
} |
好奇的澄清:这找不到计算功率的最佳方法,但由于找到最佳解决方案是一个NP完全问题,而且这只对小功率来说是值得的(与使用pow相反),没有理由对细节大惊小怪。
然后把它当作power<6>(a)来使用。
这使得输入幂很容易(不需要用parens拼写6个as),并且允许您在没有-ffast-math的情况下进行这种优化,以防您具有精度依赖性,如补偿求和(一个操作顺序至关重要的示例)。
您可能还可以忘记这是C++,只是在C程序中使用它(如果它用C++编译器编译)。
希望这是有用的。
编辑:
这就是我从编译器中得到的:
对于a*a*a*a*a*a,
1 2 3 4 5 6
| movapd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm1, %xmm0 |
对于(a*a*a)*(a*a*a),
1 2 3 4
| movapd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm0, %xmm0 |
对于power<6>(a),
1 2 3 4
| mulsd %xmm0, %xmm0
movapd %xmm0, %xmm1
mulsd %xmm0, %xmm1
mulsd %xmm0, %xmm1 |
- 找到最佳的权力树可能很困难,但因为它只对小权力感兴趣,显而易见的答案是预先计算它一次(Knuth提供了一个高达100的表),并使用硬编码的表(这是GCC内部为POWI所做的)。
- 在现代处理器上,速度受到延迟的限制。例如,乘法的结果可能在五个周期后可用。在这种情况下,找到最快的方法来创造一些能量可能会更困难。
- 您还可以尝试查找为相对舍入错误提供最低上限的幂树,或为最小平均相对舍入错误提供最低上限的幂树。
- Boost也支持这一点,例如Boost::Math::Pow<6>(n);我认为它甚至试图通过提取公共因子来减少乘法的数量。
- 好主意!我已经在做阶乘预计算了。
- 注意,最后一个相当于(a**2)**3
当a是整数时,gcc确实将aaa aaa优化为(aaa)(aaa)。我试过这个命令:
1
| $ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c - |
海湾合作委员会的旗帜很多,但没有什么特别之处。它们的意思是:从stdin读取;使用o2优化级别;输出汇编语言列表而不是二进制文件;列表应使用Intel汇编语言语法;输入为C语言(通常从输入文件扩展名推断语言,但从stdin读取时没有文件扩展名);并写入stdout。
这是输出的重要部分。我用一些注释对其进行了注释,指出了汇编语言中正在发生的事情:
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 |
我在LinuxMint16 Petra上使用系统gcc,这是一个Ubuntu的衍生工具。以下是GCC版本:
1 2
| $ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1 |
正如其他海报所指出的,这个选项在浮点中是不可能的,因为浮点运算实际上是不相关的。
- 这对于整数乘法是合法的,因为二的补码溢出是未定义的行为。如果发生溢出,不管重新排序操作如何,它都会在某个地方发生。因此,没有溢出的表达式的计算结果相同,溢出的表达式是未定义的行为,因此编译器可以更改溢出发生的点。GCC也会对unsigned int这样做。
因为32位浮点数(如1.024)不是1.024。在计算机中,1.024是一个间隔:从(1.024-e)到(1.024+e),其中"e"表示一个错误。有些人没有意识到这一点,也认为A*中的*表示任意精度数字的乘法,而这些数字没有任何错误。有些人之所以没有意识到这一点,可能是因为他们在小学里进行的数学计算:只使用理想数字而不附加错误,并且认为在乘法时简单地忽略"e"是可以的。他们没有看到"float a=1.2"、"a*a*a"和类似的C代码中隐含的"e"。
如果大多数程序员认识到(并且能够执行)C表达式a*a*a*a*a*a*a实际上不能处理理想数字的想法,那么GCC编译器就可以自由地将"a*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的错误间隔内计算数字。这是可以的,因为您已经知道您使用的是间隔,而不是理想的数字。
- 浮点数是精确的。它们不一定是你所期望的。此外,使用epsilon的技术本身就是如何在现实中处理事情的近似值,因为真正的预期误差是相对于尾数的大小的,也就是说,你通常最多只能处理1个LSB,但是如果你不小心的话,每执行一个操作都会增加这个误差,所以在执行任何操作之前,请咨询一位数字分析师。n-浮点数的平凡值。如果可能的话,使用合适的库。
- @Donalfellows:IEEE标准要求浮点计算产生的结果最精确地匹配源操作数是精确值时的结果,但这并不意味着它们实际上代表精确值。在许多情况下,将0.1f视为(1677722+/-0.5)/16777216,比将其视为精确的数量(1677722+/-0.5)/16777216(应显示为24位小数)更有帮助,因为0.1f应以不确定性所隐含的小数位数显示。
- @supercat:ieee-754非常清楚浮点数据确实代表精确值;第3.2-3.4条是相关章节。当然,您可以选择以其他方式来解释它们,正如您可以选择将int x = 3解释为x是3+/-0.5。
- @史蒂芬卡农:我想这取决于你所说的"代表"是什么意思。在大多数应用中,变量被用来模拟具体的事物。例如,在物理模拟中,它们可以表示各种物体位置和速度等的x、y和z分量。如果我说Distance = Math.Sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)+(z2-z1)*(z2-z1)),Distance的目的是表示(x1、y1、z1)和(x2、y2、z2)之间的欧氏距离。存储在Distance中的精确数字不太可能是两点之间的精确欧几里得距离,但是…
- @supercat:我完全同意,但这并不意味着Distance不完全等于它的数值;这意味着数值只是一个近似的物理量被建模。
- …尽管如此,传统用法是说,Distance表示该值,或者可能Distance表示某种在所有实际用途上都"足够接近"的值,而不是明确说明Distance表示执行上述操作所产生的精确浮点数值。操作顺序。从执行数学原语(乘法、加法、sqrt等)的硬件的角度来看,需要精确地评估数量,但对于消费者来说,它们表示近似值。
- 我的观点是,如果代码执行someSingle = 1.0/10.0,结果将和消费者预期的一样精确;如果代码执行someDouble = 1.0f/10.0f,结果将比消费者知道float数量恰好代表精确值更容易预期的多个数量级。如果一个Double被强制转换为float而从未被强制转换,那么用户在准确性方面不会感到意外。然而,从float到Double的转换更可能产生"惊喜"。
- 对于数值分析,如果你不把浮点数解释为区间,而是把它解释为精确的值(恰好不是你想要的值),你的大脑会感谢你。例如,如果x是4.5的某个圆,误差小于0.1,并且计算(x+1)-x,则"间隔"解释会给您留下0.8到1.2的间隔,而"精确值"解释会告诉您结果是1,双精度误差至多为2^(-50)。
还没有海报提到浮动表达式的收缩(ISO C标准,6.5P8和7.12.2)。如果FP_CONTRACT杂注设置为ON,则编译器可以将表达式(如a*a*a*a*a*a)视为单个操作,就像使用单个舍入精确计算一样。例如,编译器可以用更快、更准确的内部幂函数替换它。这特别有趣,因为行为部分由程序员直接在源代码中控制,而最终用户提供的编译器选项有时可能被错误地使用。
FP_CONTRACTpragma的默认状态是实现定义的,因此默认情况下允许编译器进行此类优化。因此,需要严格遵循IEEE754规则的可移植代码应该显式地将其设置为OFF。
如果编译器不支持此pragma,那么它必须避免任何此类优化,以防开发人员选择将其设置为OFF。
GCC不支持这种实用主义,但在默认选项下,它假定它是ON;因此,对于具有硬件fma的目标,如果要防止a*b+c转换为fma(a、b、c),需要提供一个选项,例如-ffp-contract=off(明确地将实用主义设置为OFF或-std=c99(告诉GCC混淆M到一些C标准版本,这里是C99,因此遵循上面的段落)。过去,后一个选项并没有阻止转换,这意味着GCC不符合这一点:https://gcc.gnu.org/bugzilla/show_bug.cgi?ID=37845
- 长期流行的问题有时会显示出他们的年龄。这个问题是在2011年被问到和回答的,当时GCC可以原谅不完全遵守当时的C99标准。当然现在是2014年了,所以GCC…嗯。
- 不过,难道你不应该回答相对较新的浮点问题而没有一个公认的答案吗?咳嗽stackoverflow.com/questions/23703408咳嗽
- 我发现…令人不安的是,gcc没有实现c99浮点pragma。
- @davidmonniaux pragma根据定义是可选的,可以实现。
- @timseguine但是如果一个pragma没有实现,它的默认值需要是实现中最严格的。我想大卫就是这么想的。对于gcc,如果使用iso c模式,就可以修复fp_契约的问题:它仍然不实现pragma,但是在iso c模式下,它现在假定pragma已关闭。
正如lambdageek所指出的,浮点乘法不具有关联性,您可以获得较少的精度,但当获得更好的精度时,您可以反对优化,因为您需要确定性应用程序。例如,在游戏模拟客户机/服务器中,每个客户机都必须模拟您希望浮点计算具有确定性的相同世界。
- 浮点总是确定性的。
- @Alice-只有在编译器不重新排序的情况下,才可能根据编译器版本、目标机器等以不同的方式进行排序。
- @格雷戈,不,那时候它还是决定性的。在这个词的任何意义上都不加随机性。
- @Alice很明显bjorn使用的是"确定性"代码,在不同的平台和不同的编译器版本等(可能超出程序员控制的外部变量)上给出相同的结果,而不是在运行时缺乏实际的数字随机性。如果你指出这不是这个词的正确用法,我不想跟你争辩。
- @Greggo,除了你对他所说的话的解释之外,它仍然是错误的;这就是IEEE754的全部观点,为跨平台的大多数(如果不是全部)操作提供相同的特性。现在,他没有提到平台或编译器版本,如果您希望每个远程服务器/客户机上的每个操作都是相同的,那么这将是一个值得关注的问题……但从他的声明中看,这并不明显。一个更好的词可能是"可靠地相似"或其他。
- @爱丽丝,你在为语义学争论,这是在浪费每个人的时间,包括你自己的时间。他的意思很清楚。
- @拉纳鲁的整个标准都是语义学,他的意思显然不清楚。
我根本没想到这个案例会被优化。在表达式包含可以重新分组以删除整个操作的子表达式的情况下,这种情况不太常见。我希望编译器编写人员将他们的时间投入到更可能导致显著改进的领域,而不是覆盖很少遇到的边缘情况。
我很惊讶地从其他答案中得知,这个表达式确实可以通过适当的编译器开关进行优化。要么优化是微不足道的,要么它是一个更常见的优化的边缘案例,要么编译器编写人员非常彻底。
像您在这里所做的那样,向编译器提供提示并没有错。重新排列语句和表达式,看看它们会带来什么样的差异,这是微优化过程中一个正常的和预期的部分。
虽然编译器可能有理由考虑这两个表达式来传递不一致的结果(没有适当的开关),但您不需要受该限制的约束。这种差异会非常微小——如此之大以至于如果差异对你很重要,你不应该首先使用标准的浮点运算。
- 他指出,这是由另一个评论者,untrue是荒谬的,这一点,可以为多个半差分到10%的成本,如果运行在一个闭环形式,翻译的许多指令可以让我重新开始insignificant量额外的精度。我想说的是,当你使用标准的FP T是保持一种蒙特卡洛样说你应该总是使用在飞机要在乡村;它在盘的标准。最后,这是一个不寻常的死代码分析和代码优化;降阶/重构是非常普通的。
像"pow"这样的库函数通常是经过精心设计的,以产生尽可能小的错误(在一般情况下)。这通常是用样条曲线来逼近函数(根据Pascal的评论,最常见的实现似乎是使用remez算法)。
基本上是以下操作:
与任何一个乘法或除法中的误差大小近似相同的固有误差。
同时进行以下操作:
1 2
| float a=someValue;
float b=a*a*a*a*a*a; |
具有大于单个乘法或除法错误5倍的固有错误(因为您组合了5个乘法)。
编译器应该非常小心地进行优化:
如果将pow(a,6)优化到a*a*a*a*a*a,可能会提高性能,但会大大降低浮点数的精度。
如果将a*a*a*a*a*a优化为pow(a,6),实际上可能会降低精度,因为"a"是一个特殊值,允许无误差乘法(2的幂或一些小整数)。
如果将pow(a,6)优化到(a*a*a)*(a*a*a)或(a*a)*(a*a)*(a*a)的话,与pow函数相比,仍然会有精度损失。
一般来说,您知道对于任意的浮点值,"pow"比您最终可以编写的任何函数都具有更好的准确性,但是在某些特殊情况下,多重乘法可能具有更好的准确性和性能,这取决于开发人员选择更合适的值,最终对代码进行注释,以便其他任何人都不会"优化""那个代码"。
唯一有意义的事情(个人观点,以及在GCC中选择任何特定的优化或编译器标志)应该是用"a*a"替换"pow(a,2)"。这将是编译器供应商应该做的唯一明智的事情。
- 投反对票的人应该意识到这个答案是完全正确的。我可以引用几十个来源和文档来支持我的答案,我可能比任何下选者更关注浮点精度。在StackOverflow中添加其他答案不包含的缺失信息是完全合理的,因此请礼貌地解释您的原因。
- 在我看来,史蒂芬·卡农的回答涵盖了你必须说的话。您似乎坚持认为libms是用样条曲线实现的:它们通常使用参数约简(取决于正在实现的函数)加上一个单一的多项式,这些多项式的系数是由remez算法的或多或少复杂的变体获得的。对于libm函数来说,连接点的平滑度并不是一个值得追求的目标(如果它们最终足够精确,那么不管域分成多少块,它们都会自动相当平滑)。
- 答案的后半部分完全没有提到编译器应该生成实现源代码所说的句点的代码。当你的意思是"精确"时,你也用"精确"这个词。
- 谢谢你的意见,我稍微更正了一下答案,最后两行还有一些新的内容。^^
对于这个问题已经有一些好的答案,但是为了完整性,我想指出C标准的适用部分是5.1.2.2.3/15(与C++ 11标准中的1.9/9节相同)。本节说明,只有当运算符真正是关联的或交换的时,才可以对其重新分组。
GCC实际上可以进行这种优化,即使对于浮点数也是如此。例如,
1 2 3
| double foo(double a) {
return a*a*a*a*a*a;
} |
变成
1 2 3 4 5 6
| foo(double):
mulsd %xmm0, %xmm0
movapd %xmm0, %xmm1
mulsd %xmm0, %xmm1
mulsd %xmm1, %xmm0
ret |
与-O -funsafe-math-optimizations一起。但是,这种重新排序违反了IEEE-754,因此需要标记。
正如PeterCordes在评论中指出的那样,有符号整数可以在不使用-funsafe-math-optimizations的情况下进行优化,因为它在没有溢出的情况下保持不变,如果有溢出,则会得到未定义的行为。所以你得到
1 2 3 4 5 6
| foo(long):
movq %rdi, %rax
imulq %rdi, %rax
imulq %rdi, %rax
imulq %rax, %rax
ret |
只带着-O。对于无符号整数,因为它们的mod幂为2,所以即使在溢出的情况下也可以自由地重新排序,所以更容易。
- 带双、int和无符号的godbolt链接。GCC和Clang都以相同的方式优化了这三个方面(使用-ffast-math)
- @彼得卡兹,谢谢!