在Python中哪个更快:x**.5 还是 math.sqrt(x)?

248

我已经思考了一段时间。正如标题所说,哪个更快,实际函数还是简单地提高到一半的幂次方?

更新

这不是过早优化的问题。这只是一个关于底层代码如何工作的问题。Python代码的理论是什么?

我给Guido van Rossum发送了一封电子邮件,因为我真的想知道这些方法的区别。

我的电子邮件:

有至少3种方法可以在Python中进行平方根:math.sqrt、'**'运算符和pow(x,.5)。我只是好奇每种方法的实现方式有什么区别。就效率而言,哪种更好?

他的回复:

pow和**是等效的;math.sqrt对于复数无效,并链接到C sqrt()函数。至于哪一个更快,我不知道...


120
太棒了,Guido回复邮件。 - Evan Fosmark
3
Evan,我很惊讶我收到了回复。 - Nope
14
我认为这不是一个糟糕的问题。例如,x * x比x ** 2 快10倍。在这种情况下,可读性是个取舍,那为什么不选择更快的方式呢? - TM.
12
Casey,我支持你对“过早优化”的看法。 :) 在我看来,你的问题并不像是在过早优化:没有风险会导致任何变体破坏你的代码。这更多是关于当你选择pow()而不是math.sqrt()时更好地知道你所做的事情(以执行时间为衡量标准)。 - Eric O. Lebigot
10
这不是过早优化,而是避免过早悲观化(参考文献28,《C++编程标准》,A. Alexandrescu)。如果math.sqrt是一个更优化的例程(就像它一样),并且表达意图更清晰,那么它应该始终优先于x ** .5。了解您编写的内容并选择更快且提供更多代码清晰度的替代方案并不是过早优化。如果这样做,您需要同样好地论证为什么会选择其他替代方案。 - swalog
显示剩余2条评论
15个回答

122

math.sqrt(x)x**0.5快得多。

import math
N = 1000000
%%timeit
for i in range(N):
    z=i**.5

10 次循环,3 次中的最佳结果:每个循环 156 毫秒

%%timeit
for i in range(N):
    z=math.sqrt(i)

每个循环跑完耗时91.1毫秒,已经循环了10次,取最优结果。

使用的是 Python 3.6.9(笔记本)。


1
我已经在codepad.org上运行了3次,并且每一次都发现a()比b()快得多。 - Paige Ruten
16
标准的 timeit 模块是你的好帮手。它能避免在测量执行时间时常见的陷阱! - Eric O. Lebigot
1
这是您的脚本结果:zoltan@host:$ python2.5 p.py 用时0.183226秒 用时0.155829秒 zoltan@host:$ python2.4 p.py 用时0.181142秒 用时0.153742秒 zoltan@host:~$ python2.6 p.py 用时0.157436秒 用时0.093905秒目标系统:Ubuntu Linux CPU:Intel(R) Core(TM)2 Duo CPU T9600 @ 2.80GHz正如您所看到的,我得到了不同的结果。根据这个,您的答案并不通用。 - zoli2k
3
Codepad是一个很棒的服务,但在计时性能方面却很糟糕。我的意思是,在任何给定的时刻,谁知道服务器会有多忙碌呢?每次运行都有可能得到非常不同的结果。 - adamJLev
1
我已经在Linux上为py32、py31、py30、py27、py26、pypy、jython、py25和py24解释器添加了x**.5与sqrt(x)的性能比较。https://gist.github.com/783011 - jfs
显示剩余6条评论

27
  • 优化的第一条规则是:不要优化
  • 第二条规则是:还不要优化

以下是一些时间数据(Python 2.5.2,Windows):

$ python -mtimeit -s"from math import sqrt; x = 123" "x**.5"
1000000 loops, best of 3: 0.445 usec per loop

$ python -mtimeit -s"from math import sqrt; x = 123" "sqrt(x)"
1000000 loops, best of 3: 0.574 usec per loop

$ python -mtimeit -s"import math; x = 123" "math.sqrt(x)"
1000000 loops, best of 3: 0.727 usec per loop

这个测试结果显示,x**.5sqrt(x) 稍微快一些。

但是对于 Python 3.0 来说,结果则相反:

$ \Python30\python -mtimeit -s"from math import sqrt; x = 123" "x**.5"
1000000 loops, best of 3: 0.803 usec per loop

$ \Python30\python -mtimeit -s"from math import sqrt; x = 123" "sqrt(x)"
1000000 loops, best of 3: 0.695 usec per loop

$ \Python30\python -mtimeit -s"import math; x = 123" "math.sqrt(x)"
1000000 loops, best of 3: 0.761 usec per loop

math.sqrt(x)在另一台机器上(Ubuntu、Python 2.6 和 3.1)始终比x**.5更快:

$ python -mtimeit -s"from math import sqrt; x = 123" "x**.5"
10000000 loops, best of 3: 0.173 usec per loop
$ python -mtimeit -s"from math import sqrt; x = 123" "sqrt(x)"
10000000 loops, best of 3: 0.115 usec per loop
$ python -mtimeit -s"import math; x = 123" "math.sqrt(x)"
10000000 loops, best of 3: 0.158 usec per loop
$ python3.1 -mtimeit -s"from math import sqrt; x = 123" "x**.5"
10000000 loops, best of 3: 0.194 usec per loop
$ python3.1 -mtimeit -s"from math import sqrt; x = 123" "sqrt(x)"
10000000 loops, best of 3: 0.123 usec per loop
$ python3.1 -mtimeit -s"import math; x = 123" "math.sqrt(x)"
10000000 loops, best of 3: 0.157 usec per loop

4
为什么我很难遵守这个规则? - Quinten C
"过早优化是万恶之源" 谬论 - JDB
@JDB 我不明白它如何适用于我的答案,我的答案主要是关于“先测量”:只有在测量表明优化对你的情况有必要时才进行优化。 - jfs
@JDB 虽然[与答案无关],但你链接的标题是错误的——阅读Knuth的完整引用以了解原因:“程序员浪费了大量时间思考或担心他们程序中非关键部分的速度,而这些效率尝试实际上在调试和维护时具有强烈的负面影响。我们应该忘记小的效率,大约97%的时间:过早的优化是万恶之源。然而,在关键的3%机会中,我们不应该放弃。” https://wiki.c2.com/?PrematureOptimization - jfs
1
你的帖子开头直接写着“优化的第一条规则:不要去做”。太多程序员已经相信任何优化都是邪恶的,正如我所链接的文章所阐述的那样,太多高级工程师甚至不理解各种算法和数据结构的基本成本,无法做出关于它们如何影响性能(无论是CPU周期还是内存管理)的良好判断。 - JDB
显示剩余4条评论

18
在这些微基准测试中,math.sqrt 的速度会较慢,因为需要在 math 命名空间中查找 sqrt。你可以用 from math import sqrt 稍微改善它。
 from math import sqrt

即使如此,通过在timeit中运行几个变量,展示了x**.5略微(4-5%)的性能优势。

有趣的是,执行

 import math
 sqrt = math.sqrt

我们进一步加速它,使其速度差异在1%以内,并且统计显着性非常小。


我会重申Kibbee的话,并说这可能是过早的优化。


2
在程序的本地命名空间中定义sqrt能够加速程序运行的原因可能是由于方法解析顺序:编译器首先检查函数是否在您的代码中定义,然后在任何导入中查找,因此如果其在本地定义,则每次查找所需的时间更少。 - Rotem Shalev

11

您实际上执行了多少个平方根?您是否试图在Python中编写一些3D图形引擎?如果不是,为什么要选择晦涩难懂的代码而不是易于阅读的代码呢?在我能想象到的任何应用程序中,时间差异都将小于任何人都能注意到的。我真的不是要贬低您的问题,但似乎您在过早优化时走得太远了。


23
我并不觉得我在进行过早优化。这更像是一个简单的问题,需要决定从两种不同方法中选择哪个平均来说会更快。 - Nope
2
Kibbee:这绝对是一个合理的问题,但我和你一样感到失望的是,在 Stack Overflow 上有很多问题暗示提问者正在进行各种过早的优化。这绝对是每种语言被问到的问题中占很大比例的。 - Eli Courtwright
但这真的有什么关系吗?这不会对任何程序产生绝对影响。 - DGM
4
math.sqrt(x) 和 x ** 0.5 哪个更易读?我认为两者都很明显是求平方根的意思... 至少如果你熟悉 Python 的话。不要因为你不熟悉 Python 就将 ** 等标准 Python 运算符称作“神秘”。 - TM.
6
我认为“**”运算符并不难懂。但是,将某个数的0.5次方作为获取其平方根的方法可能对那些不常关注数学的人来说有点难以理解。请注意,我的翻译尽力保留原意,使之更通俗易懂,但不添加任何解释性内容。 - Kibbee
18
如果他正在用Python制作一个3D引擎,那该怎么办? - Chris Burt-Brown

8
在Python 2.6中,(float).__pow__()函数使用C pow()函数,math.sqrt()函数使用C sqrt()函数。
在glibc编译器中,pow(x,y)的实现非常复杂,并且对于各种特殊情况进行了优化。例如,调用C pow(x,0.5)只需调用sqrt()函数。
使用.**或math.sqrt的速度差异是由于围绕C函数使用的包装器以及速度强烈依赖于系统上使用的优化标志/C编译器所致。
编辑:以下是Claudiu算法在我的计算机上的结果。我得到了不同的结果:
zoltan@host:~$ python2.4 p.py 
Took 0.173994 seconds
Took 0.158991 seconds
zoltan@host:~$ python2.5 p.py 
Took 0.182321 seconds
Took 0.155394 seconds
zoltan@host:~$ python2.6 p.py 
Took 0.166766 seconds
Took 0.097018 seconds

4

有人评论了Quake 3中的“快速牛顿-拉夫逊平方根”算法...我用ctypes实现了它,但与本地版本相比速度非常慢。我将尝试一些优化和替代实现。

from ctypes import c_float, c_long, byref, POINTER, cast

def sqrt(num):
 xhalf = 0.5*num
 x = c_float(num)
 i = cast(byref(x), POINTER(c_long)).contents.value
 i = c_long(0x5f375a86 - (i>>1))
 x = cast(byref(i), POINTER(c_float)).contents.value

 x = x*(1.5-xhalf*x*x)
 x = x*(1.5-xhalf*x*x)
 return x * num

这里有另一种使用struct的方法,比ctypes版本快约3.6倍,但仍然比C慢10倍。

from struct import pack, unpack

def sqrt_struct(num):
 xhalf = 0.5*num
 i = unpack('L', pack('f', 28.0))[0]
 i = 0x5f375a86 - (i>>1)
 x = unpack('f', pack('L', i))[0]

 x = x*(1.5-xhalf*x*x)
 x = x*(1.5-xhalf*x*x)
 return x * num

4

最有可能是 math.sqrt(x),因为它被优化用于平方根运算。

基准测试将会给你想要的答案。


4

就我看来(请参见Jim的答案)。在我的机器上,运行Python 2.5:

PS C:\> python -m timeit -n 100000 10000**.5
100000 loops, best of 3: 0.0543 usec per loop
PS C:\> python -m timeit -n 100000 -s "import math" math.sqrt(10000)
100000 loops, best of 3: 0.162 usec per loop
PS C:\> python -m timeit -n 100000 -s "from math import sqrt" sqrt(10000)
100000 loops, best of 3: 0.0541 usec per loop

4

使用Claudiu的代码,在我的计算机上即使使用"from math import sqrt",x**.5也更快,但使用psyco.full()时sqrt(x)变得更快,至少快了200%。


3
优化Python代码的目标应该是可读性。为此,我认为最好明确使用sqrt函数。尽管如此,让我们仍然调查一下性能。
我已经更新了Claudiu的Python 3代码,并且还使计算不可能被优化掉(这是未来一个好的Python编译器可能会做的事情)。
from sys import version
from time import time
from math import sqrt, pi, e

print(version)

N = 1_000_000

def timeit1():
  z = N * e
  s = time()
  for n in range(N):
    z += (n * pi) ** .5 - z ** .5
  print (f"Took {(time() - s):.4f} seconds to calculate {z}")

def timeit2():
  z = N * e
  s = time()
  for n in range(N):
    z += sqrt(n * pi) - sqrt(z)
  print (f"Took {(time() - s):.4f} seconds to calculate {z}")

def timeit3(arg=sqrt):
  z = N * e
  s = time()
  for n in range(N):
    z += arg(n * pi) - arg(z)
  print (f"Took {(time() - s):.4f} seconds to calculate {z}")

timeit1()
timeit2()
timeit3()

结果因情况而异,但样本输出如下:
3.6.6 (default, Jul 19 2018, 14:25:17) 
[GCC 8.1.1 20180712 (Red Hat 8.1.1-5)]
Took 0.3747 seconds to calculate 3130485.5713865166
Took 0.2899 seconds to calculate 3130485.5713865166
Took 0.2635 seconds to calculate 3130485.5713865166

还有一份较新的产出:

3.7.4 (default, Jul  9 2019, 16:48:28) 
[GCC 8.3.1 20190223 (Red Hat 8.3.1-2)]
Took 0.2583 seconds to calculate 3130485.5713865166
Took 0.1612 seconds to calculate 3130485.5713865166
Took 0.1563 seconds to calculate 3130485.5713865166

尝试一下:


我认为 **0.5 更易读;在编写数学表达式时,如果有可用的数学运算符,我宁愿使用数学运算符而不是函数。我之所以使用 **,是因为我写 -1 而不是 neg(1),或者写 a + b 而不是 add(a, b) 的原因相同。 - itub

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