什么更有效率?使用 pow 平方还是仅将其与自身相乘?

Posted

技术标签:

【中文标题】什么更有效率?使用 pow 平方还是仅将其与自身相乘?【英文标题】:What is more efficient? Using pow to square or just multiply it with itself? 【发布时间】:2011-02-25 18:58:00 【问题描述】:

这两种方法中哪一种在 C 语言中更有效?怎么样:

pow(x,3)

对比

x*x*x // etc?

【问题讨论】:

x 是整数还是浮点数? 您可以尝试编写一个执行上述两个操作的程序,并使用分析库计算执行所需的时间。这将在执行时间方面为您提供一个很好的答案。 你说的高效,是指时间,还是空间(即内存使用)? @sheepsimulator:+1 为我节省了(再次)指出编写快速测试会给你一个明确的答案比你从 SO 得到一个可能含糊或不正确的答案更快的时间. @kirill_igum 如果这些浮点值不是错误,则浮点运算不是关联的。 【参考方案1】:

x*xx*x*x 将比pow 快,因为pow 必须处理一般情况,而x*x 是特定情况。此外,您可以省略函数调用等。

但是,如果您发现自己像这样进行微优化,则需要获取分析器并进行一些认真的分析。压倒性的可能性是您永远不会注意到两者之间的任何区别。

【讨论】:

在决定测试之前,我一直在想同样的事情。我刚刚在定时循环中测试了x*x*x 与双倍std::pow(double base, int exponent),并没有看到具有统计学意义的性能差异。 确保它没有被编译器优化掉。 @Emile:检查编译器生成的代码。优化器有时会做一些棘手(且不明显)的事情。还要检查各种优化级别的性能:例如 -O0、-O1、-O2 和 -O3。 你不能假设泛化函数更慢。有时情况正好相反,因为更简单的代码更容易让编译器优化。【参考方案2】:

这是一个错误的问题。正确的问题应该是:“对于我的代码的人类读者来说,哪一个更容易理解?”

如果速度很重要(稍后),不要问,而是衡量。 (在此之前,测量优化它是否真的会产生任何明显的差异。)在此之前,编写代码,使其最容易阅读。

编辑 只是为了说明这一点(尽管应该已经如此):突破性的加速通常来自诸如使用更好的算法改善数据的局部性减少动态内存的使用预计算结果等。 他们很少来自微优化的单个函数调用,而且他们在很少的地方这样做strong>,只有 小心 (且耗时)profiling 才能发现,而且往往比从来没有它们可以通过做一些非常不直观的事情来加速(比如插入noop 语句),一个平台的优化有时是另一个平台的悲观(这就是为什么你需要测量而不是询问,因为我们不'不完全了解/拥有你的环境)。

让我再次强调这一点:即使在少数应用程序中这些东西很重要,它们在大多数使用它们的地方都无关紧要,您不太可能非常通过查看代码找到重要的地方。您确实需要首先确定热点,否则优化代码只是 浪费时间

即使单个操作(例如计算某个值的平方)占用 10% 的应用程序执行时间(IME 非常少见),甚至如果优化它可以节省该操作所需的 50% 的时间(IME 更为罕见),您仍然让应用程序占用 时间仅减少 5%。 您的用户甚至需要一个秒表才能注意到这一点。 (我猜在大多数情况下,大多数用户都不会注意到任何低于 20% 的加速。是你需要找到的四个这样的地方。)

【讨论】:

这可能是一个正确的问题。也许他并没有考虑自己的实际项目,而只是对语言/编译器的工作方式感兴趣...... @Andreas:答案仍然是在他感兴趣的具体环境中进行测量。 我认为这里的可读性不是问题。写 x*x 和 pow(x,2) 看起来都很清楚。 我不完全同意这个答案。这是一个关于性能的有效问题。您可以实现的最佳性能有时是一个有效的要求,并且通常是有人使用 c++ 而不是另一种语言的原因。测量并不总是一个好主意。我可能会测量冒泡排序和快速排序,并用我的 10 个项目更快地找到冒泡排序,因为我没有背景知道项目的数量非常重要,后来发现我的 1,000,000 个项目是一个非常糟糕的选择。 这正是要问的问题。向经验丰富的开发人员索取知识可以揭示有关编译器优化/展开、宏、C/C++ 标准等方面的大量见解。OP 以及大多数观众已经知道“什么更容易阅读”的答案”。它们都很容易阅读。数学运算符(如幂函数)在科学编程中被大量使用,并且会在 N 较大的循环中占用大量 CPU 时间。这是编程中的标准情况。【参考方案3】:

如果指数是常数且很小,则将其展开,最小化乘法次数。 (例如,x^4 不是最佳的x*x*x*x,而是y*y,其中y=x*x。而x^5y*y*x,其中y=x*x。等等。)对于常数整数指数,只需写出已经优化的形式;对于小指数,这是一个标准优化,无论代码是否已被分析,都应该执行。在很大比例的情况下,优化后的表单会更快,基本上总是值得做的。

(如果您使用 Visual C++,std::pow(float,int) 执行我提到的优化,其中操作序列与指数的位模式相关。但我不保证编译器会为您展开循环,所以还是值得自己动手。)

[编辑] BTW pow 有一个(不)令人惊讶的倾向,会突然出现分析器结果。如果您不是绝对需要它(即指数很大或不是常数),并且您完全关心性能,那么最好写出最佳代码并等待分析器告诉您它(令人惊讶) 在进一步思考之前浪费时间。 (另一种方法是致电pow 并让分析器告诉您这(不出所料)是在浪费时间——您通过智能地完成这一步来省去这一步。)

【讨论】:

【参考方案4】:

2021 年更新

我已将基准代码修改如下:

std::chrono 用于计时测量而不是 boost 使用 C++11 <random> 代替 rand() 避免可能被吊出的重复操作。基本参数是不断变化的。

使用 GCC 10 -O2(以秒为单位)得到以下结果:

exp     c++ pow     c pow       x*x*x...
2       0.204243    1.39962     0.0902527   
3       1.36162     1.38291     0.107679    
4       1.37717     1.38197     0.106103    
5       1.3815      1.39139     0.117097

GCC 10 -O3 几乎与 GCC 10 -O2 相同。

使用 GCC 10 -O2 -ffast-math:

exp     c++ pow     c pow       x*x*x...
2       0.203625    1.4056      0.0913414   
3       0.11094     1.39938     0.108027    
4       0.201593    1.38618     0.101585    
5       0.102141    1.38212     0.10662

使用 GCC 10 -O3 -ffast-math:

exp     c++ pow     c pow       x*x*x...
2       0.0451995   1.175       0.0450497   
3       0.0470842   1.20226     0.051399    
4       0.0475239   1.18033     0.0473844   
5       0.0522424   1.16817     0.0522291

使用 Clang 12 -O2:

exp     c++ pow     c pow       x*x*x...
2       0.106242    0.105435    0.105533    
3       1.45909     1.4425      0.102235    
4       1.45629     1.44262     0.108861    
5       1.45837     1.44483     0.1116

Clang 12 -O3 几乎与 Clang 12 -O2 相同。

使用 Clang 12 -O2 -ffast-math:

exp     c++ pow     c pow       x*x*x...
2       0.0233731   0.0232457   0.0231076   
3       0.0271074   0.0266663   0.0278415   
4       0.026897    0.0270698   0.0268115   
5       0.0312481   0.0296402   0.029811    

Clang 12 -O3 -ffast-math 几乎与 Clang 12 -O2 -ffast-math 相同。

机器是 Linux 5.4.0-73-generic x86_64 上的 Intel Core i7-7700K。

结论:

使用 GCC 10(无 -ffast-math),x*x*x...总是更快 使用 GCC 10 -O2 -ffast-math,std::powx*x*x... 一样快奇数个指数 使用 GCC 10 -O3 -ffast-math,std::pow 在所有测试用例中的速度都与 x*x*x... 一样快,大约是 -O2 的两倍。 使用 GCC 10,C 的 pow(double, double) 总是慢得多 使用 Clang 12(无 -ffast-math),x*x*x... 对于大于 2 的指数更快 使用 Clang 12 -ffast-math,所有方法都会产生相似的结果 使用 Clang 12,pow(double, double)std::pow 对于积分指数一样快 编写基准测试而不让编译器超越你是困难

我最终会在我的机器上安装更新版本的 GCC,并会在我这样做时更新我的​​结果。

这是更新后的基准代码:

#include <cmath>
#include <chrono>
#include <iostream>
#include <random>

using Moment = std::chrono::high_resolution_clock::time_point;
using FloatSecs = std::chrono::duration<double>;

inline Moment now()

    return std::chrono::high_resolution_clock::now();


#define TEST(num, expression) \
double test##num(double b, long loops) \
 \
    double x = 0.0; \
\
    auto startTime = now(); \
    for (long i=0; i<loops; ++i) \
     \
        x += expression; \
        b += 1.0; \
     \
    auto elapsed = now() - startTime; \
    auto seconds = std::chrono::duration_cast<FloatSecs>(elapsed); \
    std::cout << seconds.count() << "\t"; \
    return x; \


TEST(2, b*b)
TEST(3, b*b*b)
TEST(4, b*b*b*b)
TEST(5, b*b*b*b*b)

template <int exponent>
double testCppPow(double base, long loops)

    double x = 0.0;

    auto startTime = now();
    for (long i=0; i<loops; ++i)
    
        x += std::pow(base, exponent);
        base += 1.0;
    
    auto elapsed = now() - startTime;

    auto seconds = std::chrono::duration_cast<FloatSecs>(elapsed); \
    std::cout << seconds.count() << "\t"; \

    return x;


double testCPow(double base, double exponent, long loops)

    double x = 0.0;

    auto startTime = now();
    for (long i=0; i<loops; ++i)
    
        x += ::pow(base, exponent);
        base += 1.0;
    
    auto elapsed = now() - startTime;

    auto seconds = std::chrono::duration_cast<FloatSecs>(elapsed); \
    std::cout << seconds.count() << "\t"; \

    return x;


int main()

    using std::cout;
    long loops = 100000000l;
    double x = 0;
    std::random_device rd;
    std::default_random_engine re(rd());
    std::uniform_real_distribution<double> dist(1.1, 1.2);
    cout << "exp\tc++ pow\tc pow\tx*x*x...";

    cout << "\n2\t";
    double b = dist(re);
    x += testCppPow<2>(b, loops);
    x += testCPow(b, 2.0, loops);
    x += test2(b, loops);

    cout << "\n3\t";
    b = dist(re);
    x += testCppPow<3>(b, loops);
    x += testCPow(b, 3.0, loops);
    x += test3(b, loops);

    cout << "\n4\t";
    b = dist(re);
    x += testCppPow<4>(b, loops);
    x += testCPow(b, 4.0, loops);
    x += test4(b, loops);

    cout << "\n5\t";
    b = dist(re);
    x += testCppPow<5>(b, loops);
    x += testCPow(b, 5.0, loops);
    x += test5(b, loops);

    std::cout << "\n" << x << "\n";


旧答案,2010 年

我使用以下代码测试了 x*x*...pow(x,i) 之间的性能差异小 i

#include <cstdlib>
#include <cmath>
#include <boost/date_time/posix_time/posix_time.hpp>

inline boost::posix_time::ptime now()

    return boost::posix_time::microsec_clock::local_time();


#define TEST(num, expression) \
double test##num(double b, long loops) \
 \
    double x = 0.0; \
\
    boost::posix_time::ptime startTime = now(); \
    for (long i=0; i<loops; ++i) \
     \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
     \
    boost::posix_time::time_duration elapsed = now() - startTime; \
\
    std::cout << elapsed << " "; \
\
    return x; \


TEST(1, b)
TEST(2, b*b)
TEST(3, b*b*b)
TEST(4, b*b*b*b)
TEST(5, b*b*b*b*b)

template <int exponent>
double testpow(double base, long loops)

    double x = 0.0;

    boost::posix_time::ptime startTime = now();
    for (long i=0; i<loops; ++i)
    
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
    
    boost::posix_time::time_duration elapsed = now() - startTime;

    std::cout << elapsed << " ";

    return x;


int main()

    using std::cout;
    long loops = 100000000l;
    double x = 0.0;
    cout << "1 ";
    x += testpow<1>(rand(), loops);
    x += test1(rand(), loops);

    cout << "\n2 ";
    x += testpow<2>(rand(), loops);
    x += test2(rand(), loops);

    cout << "\n3 ";
    x += testpow<3>(rand(), loops);
    x += test3(rand(), loops);

    cout << "\n4 ";
    x += testpow<4>(rand(), loops);
    x += test4(rand(), loops);

    cout << "\n5 ";
    x += testpow<5>(rand(), loops);
    x += test5(rand(), loops);
    cout << "\n" << x << "\n";

结果是:

1 00:00:01.126008 00:00:01.128338 
2 00:00:01.125832 00:00:01.127227 
3 00:00:01.125563 00:00:01.126590 
4 00:00:01.126289 00:00:01.126086 
5 00:00:01.126570 00:00:01.125930 
2.45829e+54

请注意,我会累积每次 pow 计算的结果,以确保编译器不会对其进行优化。

如果我使用std::pow(double, double) 版本和loops = 1000000l,我会得到:

1 00:00:00.011339 00:00:00.011262 
2 00:00:00.011259 00:00:00.011254 
3 00:00:00.975658 00:00:00.011254 
4 00:00:00.976427 00:00:00.011254 
5 00:00:00.973029 00:00:00.011254 
2.45829e+52

这是在运行 Ubuntu 9.10 64 位的 Intel Core Duo 上。使用带有 -o2 优化的 gcc 4.4.1 编译。

所以在 C 中,是的 x*x*x 会比 pow(x, 3) 快,因为没有 pow(double, int) 过载。在 C++ 中,情况大致相同。 (假设我的测试方法是正确的。)


这是对 An Markm 的评论的回应:

即使发出了using namespace std 指令,如果pow 的第二个参数是int,那么将调用来自&lt;cmath&gt;std::pow(double, int) 重载,而不是来自&lt;math.h&gt;::pow(double, double)

此测试代码确认该行为:

#include <iostream>

namespace foo


    double bar(double x, int i)
    
        std::cout << "foo::bar\n";
        return x*i;
    




double bar(double x, double y)

    std::cout << "::bar\n";
    return x*y;


using namespace foo;

int main()

    double a = bar(1.2, 3); // Prints "foo::bar"
    std::cout << a << "\n";
    return 0;

【讨论】:

这是否意味着插入“using namespace std”会选择 C ​​选项,这将不利于运行时? 在两个计时循环中,pow 计算可能只发生一次。 gcc -O2 将循环不变表达式提升到循环之外应该没有问题。因此,您只是在测试编译器在将 add-constant 循环转换为乘法方面的表现,或者只是优化 add-constant 循环。即使对于写出的版本,您的循环速度与指数 = 1 与指数 = 5 的速度相同是有原因的。 我在godbolt 上尝试过(由于没有安装 Boost,时间已被注释掉)。令人惊讶的是,它实际上确实调用了 std::pow 8*loops 次(对于指数 > 2),除非您使用 -fno-math-errno。然后它可以将 pow 调用拉出循环,就像我想的那样。我猜由于 errno 是一个全局的,线程安全要求它调用 pow 可能多次设置 errno ... exp=1 和 exp=2 很快,因为 pow 调用被提升到循环之外,只有-O3.. (with -ffast-math,它也在循环外进行 8 之和。) 在我意识到我在使用的 Godbolt 会话中启用了 -ffast-math 之前,我投了反对票。即使没有这个, testpow 和 testpow 也被破坏了,因为它们与循环外的 pow 调用内联,所以那里有一个很大的缺陷。此外,看起来您主要是在测试 FP 添加的延迟,因为所有测试都在相同的时间内运行。你会期望test5test1 慢,但事实并非如此。使用多个累加器会拆分依赖链并隐藏延迟。 @PeterCordes 我终于开始更新基准代码以避免循环中的重复计算。我相信你也会在这个版本中发现缺陷。 ;-)【参考方案5】:

我还想知道性能问题,并希望编译器会根据@EmileCormier 的回答对此进行优化。但是,我担心他展示的测试代码仍然允许编译器优化 std::pow() 调用,因为每次调用中都使用相同的值,这将允许编译器存储结果和在循环中重新使用它 - 这将解释所有情况下几乎相同的运行时间。所以我也去看了一下。

这是我使用的代码(test_pow.cpp):

#include <iostream>                                                                                                                                                                                                                       
#include <cmath>
#include <chrono>

class Timer 
  public:
    explicit Timer () : from (std::chrono::high_resolution_clock::now())  

    void start () 
      from = std::chrono::high_resolution_clock::now();
    

    double elapsed() const 
      return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - from).count() * 1.0e-6;
    

  private:
    std::chrono::high_resolution_clock::time_point from;
;

int main (int argc, char* argv[])

  double total;
  Timer timer;



  total = 0.0;
  timer.start();
  for (double i = 0.0; i < 1.0; i += 1e-8)
    total += std::pow (i,2);
  std::cout << "std::pow(i,2): " << timer.elapsed() << "s (result = " << total << ")\n";

  total = 0.0;
  timer.start();
  for (double i = 0.0; i < 1.0; i += 1e-8)
    total += i*i;
  std::cout << "i*i: " << timer.elapsed() << "s (result = " << total << ")\n";

  std::cout << "\n";

  total = 0.0;
  timer.start();
  for (double i = 0.0; i < 1.0; i += 1e-8)
    total += std::pow (i,3);
  std::cout << "std::pow(i,3): " << timer.elapsed() << "s (result = " << total << ")\n";

  total = 0.0;
  timer.start();
  for (double i = 0.0; i < 1.0; i += 1e-8)
    total += i*i*i;
  std::cout << "i*i*i: " << timer.elapsed() << "s (result = " << total << ")\n";


  return 0;

这是使用以下代码编译的:

g++ -std=c++11 [-O2] test_pow.cpp -o test_pow

基本上,区别在于 std::pow() 的参数是循环计数器。正如我所担心的那样,性能差异很明显。如果没有 -O2 标志,我的系统(Arch Linux 64 位、g++ 4.9.1、Intel i7-4930)上的结果是:

std::pow(i,2): 0.001105s (result = 3.33333e+07)
i*i: 0.000352s (result = 3.33333e+07)

std::pow(i,3): 0.006034s (result = 2.5e+07)
i*i*i: 0.000328s (result = 2.5e+07)

经过优化,结果同样惊人:

std::pow(i,2): 0.000155s (result = 3.33333e+07)
i*i: 0.000106s (result = 3.33333e+07)

std::pow(i,3): 0.006066s (result = 2.5e+07)
i*i*i: 9.7e-05s (result = 2.5e+07)

所以看起来编译器至少尝试优化 std::pow(x,2) 的情况,而不是 std::pow(x,3) 的情况(它需要的时间比 std 长约 40 倍::pow(x,2) 情况)。在所有情况下,手动扩展的性能都更好——尤其是 power 3 情况下(快 60 倍)。如果在紧密循环中以大于 2 的整数幂运行 std::pow(),这绝对值得牢记...

【讨论】:

【参考方案6】:

最有效的方法是考虑乘法的指数增长。检查此代码是否为 p^q:

template <typename T>
T expt(T p, unsigned q)
    T r =1;
    while (q != 0) 
        if (q % 2 == 1)     // if q is odd
            r *= p;
            q--;
        
        p *= p;
        q /= 2;
    
    return r;

【讨论】:

【参考方案7】:

我一直在忙于解决类似的问题,我对结果感到很困惑。我正在计算 n 体情况下牛顿引力的 x⁻³/²(从位于距离矢量 d 处的另一个质量 M 的物体经历的加速度):a = M G d*(d²)⁻³/²(其中 d² 是 d 的点(标量)乘积本身),我认为计算M*G*pow(d2, -1.5) 会比M*G/d2/sqrt(d2) 更简单

诀窍是小型系统确实如此,但是随着系统规模的扩大,M*G/d2/sqrt(d2) 变得更加高效,我不明白为什么系统的大小会影响这个结果,因为对不同的数据重复操作才不是。好像随着系统的增长有可能的优化,但pow是不可能的

【讨论】:

Numpy 一次作用于数组,因此每个单独的操作都会生成一个完整的中间数组(分配了大量内存)。因此M*G 生成tmp1tmp1/d2 生成tmp2sqrt(d2) 生成tmp3tmp2/tmp3 生成结果。只需致电pow 即可跳过其中一个临时工。因此,它实际上与有关 C 函数的问题无关。尝试使用numbanumexpr 来帮助避免这种开销

以上是关于什么更有效率?使用 pow 平方还是仅将其与自身相乘?的主要内容,如果未能解决你的问题,请参考以下文章

无法仅将USB连接更改为充电

是否可以仅将通用应用程序上传到应用商店后才能将其更改为iphone?

50-Pow(x,n)

从键盘读入两个实数,编程计算并输出它们的平方和,要求使用数学函数pow(x,y)计算平方值,输出结果保留2位小数。

从键盘读入两个实数,编程计算并输出它们的平方和,要求使用数学函数pow(x,y)计算平方值,输出结果保留2位小数。

LeetCode50. Pow(x, n)