什么更有效率?使用 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*x
或x*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^5
是y*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::pow
与 x*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
,那么将调用来自<cmath>
的std::pow(double, int)
重载,而不是来自<math.h>
的::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 添加的延迟,因为所有测试都在相同的时间内运行。你会期望test5
比test1
慢,但事实并非如此。使用多个累加器会拆分依赖链并隐藏延迟。
@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
生成tmp1
,tmp1/d2
生成tmp2
,sqrt(d2)
生成tmp3
,tmp2/tmp3
生成结果。只需致电pow
即可跳过其中一个临时工。因此,它实际上与有关 C 函数的问题无关。尝试使用numba
或numexpr
来帮助避免这种开销以上是关于什么更有效率?使用 pow 平方还是仅将其与自身相乘?的主要内容,如果未能解决你的问题,请参考以下文章
是否可以仅将通用应用程序上传到应用商店后才能将其更改为iphone?
从键盘读入两个实数,编程计算并输出它们的平方和,要求使用数学函数pow(x,y)计算平方值,输出结果保留2位小数。