使用pow(x,2)而不是x*x,在x是双精度时是否有任何优势?

56

使用这段代码是否有优势?

double x;
double square = pow(x,2);

换成这个怎么样?

double x;
double square = x*x;

我更喜欢使用x*x,而且在我所使用的Microsoft实现中,对于特定的平方情况,我发现使用pow没有任何优势,因为x*x更简单。

是否有任何情况下pow更占优势?


2
正如我在答案中所述,如果你想在C++11中将其用作constexpr,这个问题会有稍微不同的转折。 - Shafik Yaghmour
可能是什么更有效?使用pow平方还是直接乘以自己?的重复问题。 - underscore_d
8个回答

62

值得一提的是,在MacOS X 10.6系统上,配合-O3编译器标志,使用gcc-4.2进行编译。

x = x * x;

并且

y = pow(y, 2);

结果在 相同 的汇编代码中:

#include <cmath>

void test(double& x, double& y) {
        x = x * x;
        y = pow(y, 2);
}

合并为:

    pushq   %rbp
    movq    %rsp, %rbp
    movsd   (%rdi), %xmm0
    mulsd   %xmm0, %xmm0
    movsd   %xmm0, (%rdi)
    movsd   (%rsi), %xmm0
    mulsd   %xmm0, %xmm0
    movsd   %xmm0, (%rsi)
    leave
    ret

只要你使用一个不错的编译器,就写对你应用程序更有意义的方式,但请注意

pow(x,2)永远不会比纯乘法更优。


39
顺便说一下,-O6-O3 是相同的。g++ 并不会调到11。 - Tobu
8
显然,它甚至没有达到四 :) - Karl Knechtel
2
@Alnitak +1 我非常喜欢你查看汇编代码的方法。 - Alessandro Jacopson
2
我一直对查看汇编代码来证明程序行为持谨慎态度。工具链(尤其是标准)不能保证汇编输出,因此它可能随时更改。这几乎不是一个可靠的方法来证明关于程序的任何事情,除了你现在正在查看的特定两个可执行文件。 - Lightness Races in Orbit
8
就这个问题而言,微软的编译器绝对不会为pow(x, 2)x * x生成相同的代码。它总是调用pow函数,导致一系列分支和大量缓慢的代码。如果你的意思是x * x,那么请将其写成x * x。避免使用过于通用的函数来显示自己的聪明才智。我们正在讨论一个巨大的速度差异。因此,你的答案是非常误导人的。如果你要分析汇编代码,至少需要反汇编相关编译器生成的代码,特别是因为提问者确实提到了它。 - Cody Gray
显示剩余13条评论

26

std::pow更加表达性强,如果您的意思是,而x*x在表示x*x时更具表达性,特别是当您仅编写科学论文并且读者应该能够理解您的实现与论文时。对于x*x/,差异可能微小,但我认为通常使用命名函数会增加代码表达性和可读性。

在像g++ 4.x这样的现代编译器上,std::pow(x,2)将被内联,即使它甚至不是编译器内置的函数,并且会被优化为x*x。如果默认情况下不是这样的,并且您不关心IEEE浮点类型符合性,请查阅您的编译器手册以获取快速数学开关(例如g++ == -ffast-math)。


附注:已经提到包含math.h会增加程序大小。我的回答是:

在C++中,您需要#include <cmath>,而不是math.h。此外,如果您的编译器不是非常古老,那么它只会增加你所使用的内容(在一般情况下),如果你的std::pow实现只是内联到相应的x87指令,并且现代g++将使用x*x进行强制规约,那么就不会有明显的大小增加。程序大小也永远不应该决定您让代码表达性如何。

cmathmath.h更有优势的另一个方面是,它为每种浮点数类型提供了std::pow重载函数,而使用math.h则在全局命名空间中提供了powpowf等,因此cmath增加了代码的适应性,特别是在编写模板时。

一般而言:相对于基于性能和二进制大小的不可靠推测的代码,更偏爱表达清晰的代码。

另请参见 Knuth:

"我们应该忘记小小的效率问题,在97%的情况下都是如此:过早优化是万恶之源。"

以及 Jackson:

程序优化的第一条规则:不要去做优化。程序优化的第二条规则(仅适用于专家):暂时不要进行优化。


1
+1,编写任何源代码时遵循“最小惊奇原则”确实非常重要。 - Alok Save
2
+1 对于所有事情都要加一,特别是你费心使用了正确的上标2字符 :) - Lightness Races in Orbit
@hsmyers:如果您认真阅读我的帖子和评论,而不是散布毫无根据的言论,您可能会明白我并没有建议永远不要优化。甚至我引用的语录明确表示“大约有97%的时间”和“还没到时候”。此外,“更喜欢表达清晰的代码”明确不等于“总是写出表达清晰的代码”。我唯一感到惊讶的是:为什么这么多人在攻击作者之前不看完整篇文章呢? - Sebastian Mach
@SebastianMach 我应该更清楚地表明我并不是特别指向你,而是那些在前往下一个大项目的路上随意抛出这个单一语句的人。当我开始使用IBM 360BAL编码时,这不是“过早”的优化问题。实际上,我确实读了你写的东西,但我错过了第二条规则的解释——抱歉!哦,相信我说这不是攻击,如果我曾经攻击过你,你会清楚地记得这种区别... - hsmyers
@hsmyers:我明白了。我向您致以敬意,并真诚地为我的误解道歉。 - Sebastian Mach
显示剩余7条评论

14

x*x不仅更清晰,而且肯定至少与pow(x,2)一样快。


1
使用几乎所有编译器都会更快,通常比一个数量级还要快。 - Sven Marnach
1
@sven 我也这么认为,但我猜编译器可以将pow优化并内联为简单的乘法。所以我还是谨慎一些! - David Heffernan
2
@David的确,编译器可以对pow(x, 2)进行优化——请参见我的回答。 - Alnitak
2
@Sven:你如何定义“几乎每个编译器”?如果你不关心IEEE合规性,可以使用g++ -fast-math ,它会将pow(x,2)强度降低为x * x,因此没有区别。 - Sebastian Mach
8
出于性能原因,我们曾经不得不用1.0 / (x*x*sqrt(x))替换pow(x, -2.5)。在进行计算的特定机器上,这给出了令人难以置信的加速效果。(编译器是gcc4.4,如果我没记错的话) - Sven Marnach

13

这个问题涉及到大多数使用C和C++进行科学编程实现的主要弱点之一。在从Fortran转向C约20年后,然后转向C ++,这仍然是偶尔让我怀疑这种转变是否是正确选择的痛点之一。

问题的核心:

  • 实现pow最简单的方法是Type pow(Type x; Type y){return exp(y*log(x));}
  • 大多数C和C ++编译器采取了简单方法。
  • 一些编译器可能会“恰当地处理”,但只有在高优化级别下才能实现。
  • x * x相比,pow(x,2)的简单方法在计算上非常昂贵,并且失去了精度。

与专注于科学编程的语言进行比较:

  • 您不需要编写pow(x,y)。这些语言具有内置的指数运算符。 C和C ++始终拒绝实现指数运算符,这使得许多科学程序员激动不已。对于一些顽固的Fortran程序员来说,这就足以理由永远不切换到C。
  • 对于所有小整数幂,需要对Fortran(和其他语言)进行“正确处理”,其中小是从-12到12的任何整数。 (如果编译器无法“正确处理”,则编译器不符合规范。)此外,它们还需要在关闭优化的情况下执行。
  • 许多Fortran编译器还知道如何提取一些有理根而不必采用简单方法。

仅依赖高优化级别“恰当地处理”存在问题。我曾为多个组织工作,这些组织禁止在安全关键软件中使用优化。在某些优化编译器中出现错误导致损失了数百万到数亿美元后,记忆可能会非常长(长达数十年)。

在C或C ++中,我认为永远不应该使用pow(x,2)。我的观点并不孤立。那些使用pow(x,2)的程序员通常在代码审查中受到严厉批评。


4
没有一个专门的乘方运算符(这并不难实现),但更复杂类型的运算符(如向量和矩阵)可以弥补这一缺陷。不进行优化会相当低效(在科学应用中绝对不能这样做)。如果你的代码在进行优化时无法工作,那么就是错误的代码。有些事情是你必须依赖的(如编译器的有效性)。但我同意不依赖于高级别优化特性,因为不是所有编译器都支持(比如上述的 pow 优化)。 - Christian Rau
@christian 我很贪心。我想要全部,运算符重载和一个 pow 运算符! - David Heffernan
1
@Christian:你刚刚触及了另一个痛点。在科学编程中,C++对向量和矩阵的支持非常糟糕。首先,std::vector不是一个向量。而且,对于一些顽固的Fortran程序员(或更重要的是,禁止使用C/C++的顽固项目经理),缺乏专用的指数运算符太难接受了。 - David Hammen
1
@David C++支持向量和矩阵,可以通过编写自己的类(或使用现有库)来实现,并且可以使用完全可用的运算符(并且使用表达式模板几乎没有额外成本)。但是,是的,没有人想要使用std::vector(甚至不是std::valarray)进行高性能数学向量的计算。 - Christian Rau
1
编写自己的类与C++支持矩阵和向量截然不同。去找死忠的Fortran程序员聊一聊吧。缺乏一个指数运算符是他们看不起C/C++的第二个原因。原因#1是缺乏对矩阵和向量的支持。原因#3是语言提供的极度稀疏的数学库。 - David Hammen
显示剩余3条评论

12

在C++11中,只有在需要在constexpr中使用时,才比std::pow(x,2)更好的情况是使用x * x

constexpr double  mySqr( double x )
{
      return x * x ;
}

我们可以看到std::pow并未标记为constexpr,因此无法在constexpr函数中使用。

从性能角度来看,将以下代码放入godbolt中会显示这些函数:

#include <cmath>

double  mySqr( double x )
{
      return x * x ;
}

double  mySqr2( double x )
{
      return std::pow( x, 2.0 );
}

生成相同的汇编:

mySqr(double):
    mulsd   %xmm0, %xmm0    # x, D.4289
    ret
mySqr2(double):
    mulsd   %xmm0, %xmm0    # x, D.4292
    ret

我们应该期望任何现代编译器都能产生类似的结果。

值得注意的是,目前 gcc将pow视为constexpr,这也在此处进行了介绍,但这是一种不符合规范的扩展,不能依赖它,并且在后续版本的gcc中可能会发生改变。


不错,非常好。我不知道那个网站。 - Sebastian Mach
@phresnel 我发现这对于快速了解编译器将进行哪些优化非常有帮助,并且对于回答优化问题非常有帮助。 - Shafik Yaghmour

8

x * x编译后将始终简化为乘法。pow(x, 2)可能会被优化为相同的形式,但不保证。如果没有被优化,那么它很可能使用缓慢的一般幂计算方法。因此,如果性能是您关心的问题,您应该始终选择x * x


6

我的看法:

  • 代码可读性
  • 代码健壮性 - 更容易更改为pow(x, 6),可能会实现特定处理器的浮点机制等。
  • 性能 - 如果有更聪明和更快的计算方法(使用汇编或某种特殊技巧),pow将会使用它。你不会。。:)

干杯


6
你关于 pow(x,6) 的论点不太现实。而且编译器也可以优化 x*x! - David Heffernan
1
@David 我同意它比另一个弱,但这两者的组合足够强大。优化论点对于两种情况都是正确的,我认为这是一种品味问题(实际上整个问题都是如此),哪个更好。我倾向于“不要做别人已经做过的事”的一般原则。 - Hertzel Guinness
1
如果编程语言内置了指数运算符,比如Fortran,那么这个讨论就没有意义了。 - David Heffernan
@David Heffernan:你触及了科学程序员在使用C/C++进行科学编程方面的一个敏感问题。请查看我的回答以获取更多信息。 - David Hammen
2
我也对可读性提出质疑。在任何大型方程式中,pow都很糟糕。如果认为运算符重载有助于提高可读性,那么应该认为pow是有害的。 - David Heffernan
2
@David Heffernan:在我看来,pow(x,2)并不比x*x更糟糕。至少它给出了一些分组;如果你有可读性问题,你应该将计算拆分,常量也会胜出:const float x = pow(x,2) / sqrt(y) -> const float num = pow(x,2), denom = sqrt(y), x = num/denom;。当然最好的是,但这是C++。 - Sebastian Mach

1
我可能会选择std::pow(x, 2),因为它可以使我的代码重构更容易。并且一旦代码被优化,这将毫无影响。
现在,这两种方法并不完全相同。这是我的测试代码:
#include<cmath>

double square_explicit(double x) {
  asm("### Square Explicit");
  return x * x;
}

double square_library(double x) {
  asm("### Square Library");  
  return std::pow(x, 2);
}
< p > asm("text"); 调用只是将注释写入汇编输出中,我使用(GCC 4.8.1 在 OS X 10.7.4 上)生成此输出:

g++ example.cpp -c -S -std=c++11 -O[0, 1, 2, or 3]

你不需要使用-std=c++11,我只是一直使用它。
首先,在调试时(没有优化),生成的汇编代码是不同的;这是相关部分:
# 4 "square.cpp" 1
    ### Square Explicit
# 0 "" 2
    movq    -8(%rbp), %rax
    movd    %rax, %xmm1
    mulsd   -8(%rbp), %xmm1
    movd    %xmm1, %rax
    movd    %rax, %xmm0
    popq    %rbp
LCFI2:
    ret
LFE236:
    .section __TEXT,__textcoal_nt,coalesced,pure_instructions
    .globl __ZSt3powIdiEN9__gnu_cxx11__promote_2IT_T0_NS0_9__promoteIS2_XsrSt12__is_integerIS2_E7__valueEE6__typeENS4_IS3_XsrS5_IS3_E7__valueEE6__typeEE6__typeES2_S3_
    .weak_definition __ZSt3powIdiEN9__gnu_cxx11__promote_2IT_T0_NS0_9__promoteIS2_XsrSt12__is_integerIS2_E7__valueEE6__typeENS4_IS3_XsrS5_IS3_E7__valueEE6__typeEE6__typeES2_S3_
__ZSt3powIdiEN9__gnu_cxx11__promote_2IT_T0_NS0_9__promoteIS2_XsrSt12__is_integerIS2_E7__valueEE6__typeENS4_IS3_XsrS5_IS3_E7__valueEE6__typeEE6__typeES2_S3_:
LFB238:
    pushq   %rbp
LCFI3:
    movq    %rsp, %rbp
LCFI4:
    subq    $16, %rsp
    movsd   %xmm0, -8(%rbp)
    movl    %edi, -12(%rbp)
    cvtsi2sd    -12(%rbp), %xmm2
    movd    %xmm2, %rax
    movq    -8(%rbp), %rdx
    movd    %rax, %xmm1
    movd    %rdx, %xmm0
    call    _pow
    movd    %xmm0, %rax
    movd    %rax, %xmm0
    leave
LCFI5:
    ret
LFE238:
    .text
    .globl __Z14square_libraryd
__Z14square_libraryd:
LFB237:
    pushq   %rbp
LCFI6:
    movq    %rsp, %rbp
LCFI7:
    subq    $16, %rsp
    movsd   %xmm0, -8(%rbp)
# 9 "square.cpp" 1
    ### Square Library
# 0 "" 2
    movq    -8(%rbp), %rax
    movl    $2, %edi
    movd    %rax, %xmm0
    call    __ZSt3powIdiEN9__gnu_cxx11__promote_2IT_T0_NS0_9__promoteIS2_XsrSt12__is_integerIS2_E7__valueEE6__typeENS4_IS3_XsrS5_IS3_E7__valueEE6__typeEE6__typeES2_S3_
    movd    %xmm0, %rax
    movd    %rax, %xmm0
    leave
LCFI8:
    ret

但是当您生成优化代码时(即使在GCC的最低优化级别,意味着-O1),代码仍然完全相同:

# 4 "square.cpp" 1
    ### Square Explicit
# 0 "" 2
    mulsd   %xmm0, %xmm0
    ret
LFE236:
    .globl __Z14square_libraryd
__Z14square_libraryd:
LFB237:
# 9 "square.cpp" 1
    ### Square Library
# 0 "" 2
    mulsd   %xmm0, %xmm0
    ret

所以,除非您关心未经优化的代码速度,否则并没有什么区别。
就像我说的:在我看来,std::pow(x, 2)更清晰地传达了您的意图,但这是一种偏好,而不是性能。
优化似乎即使针对更复杂的表达式也是适用的。例如:
double explicit_harder(double x) {
  asm("### Explicit, harder");
  return x * x - std::sin(x) * std::sin(x) / (1 - std::tan(x) * std::tan(x));
}

double implicit_harder(double x) {
  asm("### Library, harder");
  return std::pow(x, 2) - std::pow(std::sin(x), 2) / (1 - std::pow(std::tan(x), 2));
}

再次使用-O1(最低优化),汇编代码仍然相同:

# 14 "square.cpp" 1
    ### Explicit, harder
# 0 "" 2
    call    _sin
    movd    %xmm0, %rbp
    movd    %rbx, %xmm0
    call    _tan
    movd    %rbx, %xmm3
    mulsd   %xmm3, %xmm3
    movd    %rbp, %xmm1
    mulsd   %xmm1, %xmm1
    mulsd   %xmm0, %xmm0
    movsd   LC0(%rip), %xmm2
    subsd   %xmm0, %xmm2
    divsd   %xmm2, %xmm1
    subsd   %xmm1, %xmm3
    movapd  %xmm3, %xmm0
    addq    $8, %rsp
LCFI3:
    popq    %rbx
LCFI4:
    popq    %rbp
LCFI5:
    ret
LFE239:
    .globl __Z15implicit_harderd
__Z15implicit_harderd:
LFB240:
    pushq   %rbp
LCFI6:
    pushq   %rbx
LCFI7:
    subq    $8, %rsp
LCFI8:
    movd    %xmm0, %rbx
# 19 "square.cpp" 1
    ### Library, harder
# 0 "" 2
    call    _sin
    movd    %xmm0, %rbp
    movd    %rbx, %xmm0
    call    _tan
    movd    %rbx, %xmm3
    mulsd   %xmm3, %xmm3
    movd    %rbp, %xmm1
    mulsd   %xmm1, %xmm1
    mulsd   %xmm0, %xmm0
    movsd   LC0(%rip), %xmm2
    subsd   %xmm0, %xmm2
    divsd   %xmm2, %xmm1
    subsd   %xmm1, %xmm3
    movapd  %xmm3, %xmm0
    addq    $8, %rsp
LCFI9:
    popq    %rbx
LCFI10:
    popq    %rbp
LCFI11:
    ret

最后:使用x * x方法不需要include cmath,这会使您的编译速度稍微更快,其他条件相同。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接