有什么更有效的?使用pow平方或仅将其与自身相乘?


119

这两种方法中的哪一种在C中更有效?以及如何:

pow(x,3)

x*x*x // etc?

9
x整数还是浮点数?
马修·弗拉申

6
您可以尝试编写执行上述两个操作的程序,并使用概要分析库确定执行时间。就执行时间而言,这将为您提供一个很好的答案。
J.波尔费尔

3
当您说有效时,您是指时间或空间(即内存使用)吗?
J.波尔费尔

4
@sheepsimulator:+1是为了节省我(再次)指出编写快速测试将给您确定的答案所需的时间,比您从SO中获得含糊或不正确的答案要快。
我的正确观点

5
@kirill_igum如果这些是不是bug的浮点值,则浮点算术不是关联的。
effeffe

Answers:


82

我使用以下代码测试了x*x*...vs 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中,yes x*x*x将比更快pow(x, 3),因为没有pow(double, int)重载。在C ++中,大致相同。(假设我的测试方法正确)。


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

即使using namespace std发出了指令,如果to的第二个参数pow是an int,那么std::pow(double, int)重载from <cmath>也会被调用,而不是::pow(double, double)from <math.h>

此测试代码确认该行为:

#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;
}

1
这是否意味着插入“ using namespace std”会选择C选项,这将对运行时有害?
Andreas

在两个定时循环中,pow计算可能只发生一次。gcc -O2应该可以毫无问题地将循环不变表达式提升到循环之外。因此,您只是在测试编译器将加常数循环变成乘法的性能,或者只是优化加常数循环。有一个原因使您的循环在指数= 1与指数= 5时具有相同的速度,即使是写版本也是如此。
彼得·科德斯

2
我在Godbolt上进行了尝试(注释掉了计时,因为Godbolt没有安装Boost。出乎意料的是std::pow,除非您使用,否则实际上会调用8次循环(对于指数> 2)-fno-math-errno。然后,它可以像我想的那样将pow调用拉出循环。我想既然errno将是一个全球性的,线程安全的需要,它叫战俘到可能设置errno多次... EXP = 1和EXP = 2是快,因为战俘电话与刚刚吊出循环-O3。(有- ffast-math,它也在循环外进行8之和。)
Peter Cordes

在意识到自己正在使用的“ godbolt”会话中使用-ffast-math之前,我投下了反对票。即使没有这些,testpow <1>和testpow <2>也会被破坏,因为它们与pow循环中悬挂的调用内联,因此存在一个很大的缺陷。另外,由于所有测试都在相同的时间内进行,因此您似乎主要是在测试FP添加的延迟。您可能希望test5比慢一些test1,但事实并非如此。使用多个累加器将分裂依赖链并隐藏等待时间。
彼得·科德斯

@PeterCordes,你五年前在哪里?:-)我将尝试通过应用pow不断变化的值来固定基准(以防止重复出现的pow表达式被悬挂)。
Emile Cormier

30

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

如果速度很重要(以后),不要问,而要测量。(在此之前,请测量是否对此进行优化实际上是否会产生任何明显的变化。)在此之前,编写代码以使其最容易阅读。

编辑
只是为了弄清楚(尽管已经应该这样做):突破性的加速通常来自诸如使用更好的算法改善数据的局部性减少动态内存的使用预先计算结果之类的事情。它们很少来自微观优化单个函数调用,并且它们在哪里进行,它们在很少的地方进行,只有通过仔细(且费时)的分析才能发现,而通过非直觉的分析常常会加速它们的运行。事物(例如插入noop 声明),而对一个平台的优化有时对另一个平台则是悲观的(这就是为什么您需要衡量而不是询问的原因,因为我们不完全了解/拥有您的环境)。

让我再次强调这一点:即使在这样的事情重要的几个应用程序,他们不事在他们使用最多的地方,而这是非常不可能的,你会发现,他们通过查看代码问题的地方。您确实确实需要首先确定热点,因为否则优化代码只会浪费时间

即使单个操作(例如计算某个值的平方)占用了应用程序执行时间的10%(这种情况很少见,IME),即使进行优化也节省了该操作所需的50%的时间(哪个IME是甚至更少),您仍然使应用程序仅减少5%的时间
您的用户将需要秒表来注意到这一点。(我想在大多数情况下,大多数用户都不会注意到加速低于20%的情况。就是您需要找到的四个此类地点。)


43
这可能是正确的问题。也许他不是在考虑自己的实际项目,而只是对语言/编译器的工作方式感兴趣……
Andreas Rejbrand 2010年

137
Stackoverflow应该有一个插入标准免责声明的按钮:“我已经知道过早的优化是邪恶的,但是出于学术目的我在问这个优化问题,或者我已经将该行/代码块确定为瓶颈”。
Emile Cormier 2010年

39
我认为可读性不是这里的问题。写x * x与pow(x,2)似乎都很清楚。
KillianDS 2010年

41
过多使用粗体和斜体,不容易引起眼睛不适。
stagas 2010年

24
我不同意这个答案。询问性能是一个有效的问题。您有时可以达到的最佳性能是一个有效的要求,并且通常是某人使用c ++而不是另一种语言的原因。测量并非总是一个好主意。我可能会用10个项目来测量气泡排序和快速排序并更快地找到气泡排序,因为我没有背景知道项目的数量非常重要,而后来发现1,000,000个项目则是一个非常糟糕的选择。
jcoder

17

x*xx*x*x将比快pow,因为pow必须处理一般情况,而是x*x特定的。同样,您可以取消函数调用等。

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


7
在决定测试之前,我一直在想同样的事情。我只是在定时循环中测试了x*x*xvs两倍std::pow(double base, int exponent),却看不到统计上有意义的性能差异。
Emile Cormier 2010年

2
确保编译器没有对其进行优化。
Ponkadoodle

1
@Emile:检查编译器生成的代码。优化器有时会做一些棘手的(而且不太明显)的事情。还要检查各种优化级别的性能:例如-O0,-O1,-O2和-O3。
只是我的正确观点2010年

2
您不能假定广义函数较慢。有时情况恰恰相反,因为更简单的代码更易于编译器优化。
凌晨

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 :: pow的时间长40倍左右) (x,2)情况)。在所有情况下,手动扩展的效果都更好-特别是对于Power 3情况(速度提高了60倍)。如果在紧密循环中以大于2的整数幂运行std :: pow(),绝对值得牢记...


4

最有效的方法是考虑乘法的指数增长。检查以下代码中的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;
}

2

如果指数恒定且较小,则将其展开,以最大程度减少乘法次数。(例如,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并让探查器告诉您这是(毫无疑问)在浪费时间-您可以通过明智地执行此步骤来消除此步骤。)


0

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

诀窍在于,对于小型系统而言确实如此,但是随着系统规模的扩大,M*G/d2/sqrt(d2)效率会提高,而且我不明白为什么系统规模会影响此结果,因为对不同数据重复操作不会。好像随着系统的发展有可能的优化,但是对于pow

在此处输入图片说明

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.