用pow(x,2)代替x*x有什么好处吗

Is there any advantage to using pow(x,2) instead of x*x, with x double?

本文关键字:什么 pow 代替      更新时间:2023-10-16

使用这个代码有什么好处吗

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

而不是这个?

double x;
double square = x*x;

我更喜欢x*x,从我的实现(Microsoft)来看,我发现pow没有任何优势,因为对于特定的方形情况,x*x比pow更简单。

有没有什么特殊情况下pow更优越?

FWIW,带有MacOS X 10.6上的gcc-4.2和-O3编译器标志,

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)永远不会比普通乘法更优化。

std::pow时更具表现力,x*xx*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来强度减少,那么就不会增加相关的大小。此外,程序大小永远不应该决定代码的表达能力

math.h相比,cmath的另一个优点是,使用cmath,您可以为每个浮点类型获得std::pow重载,而使用math.h,您可以在全局命名空间中获得powpowf等,因此cmath提高了代码的适应性,尤其是在编写模板时。

一般来说:更喜欢表达清晰的代码,而不是基于可疑性能和二进制大小的合理代码

另请参见Knuth:

"我们应该忘记小效率,比如说97%的时间:过早优化是万恶之源"

和Jackson:

程序优化的第一条规则:不要做。程序优化的第二条规则(仅供专家使用!):还不要做。

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

这个问题涉及到大多数C和C++实现在科学编程方面的一个关键弱点。从Fortran转换到C大约二十年后,再到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(和其他语言)需要对所有的小整数幂"做正确的事情",其中small是-12到12之间的任何整数。(如果编译器不能"做正确的事情",那么它就是不兼容的。)此外,它们必须在关闭优化的情况下这样做
  • 许多Fortran编译器也知道如何提取一些有理根,而不必求助于简单的方法

依赖高优化级别来"做正确的事情"存在问题。我曾为多个组织工作,这些组织禁止在安全关键软件中使用优化。在这里损失了1000万美元,那里损失了1亿美元之后,内存可能会很长(几十年长),这一切都是由于某些优化编译器中的错误。

IMHO,应该永远不要在C或C++中使用pow(x,2)。我并不是唯一一个持这种观点的人。使用pow(x,2)的程序员通常会在代码评审期间获得大量时间。

在C++11中,有一种情况是使用x * x比使用std::pow(x,2)有优势,而这种情况是您需要在constexpr中使用它:

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的后续版本中更改。

x * x将始终编译为简单乘法。pow(x, 2)很可能(但决不能保证)会被优化到相同的水平。如果它没有得到优化,它可能会使用缓慢的通用提升来增强数学例程。因此,如果您关心性能,您应该始终支持x * x

IMHO:

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

干杯

我可能会选择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);
}

asm("text");调用只是将注释写入程序集输出,我使用(OSX 10.7.4上的GCC 4.8.1)生成:

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方法不需要includecmath,这将使您的编译速度稍微快一点——在其他条件相同的情况下。