【问题标题】:Which is faster in Python: x**.5 or math.sqrt(x)?Python 中哪个更快:x**.5 或 math.sqrt(x)?
【发布时间】:2010-09-24 12:41:58
【问题描述】:

我一直在想这个问题。正如标题所说,哪个更快,实际功能还是简单地提高到一半?

更新

这不是过早优化的问题。这只是底层代码如何实际工作的问题。 Python 代码的工作原理是什么?

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

我的邮箱:

在 Python 中至少有 3 种求平方根的方法:math.sqrt、 '**' 运算符和 pow(x,.5)。我只是好奇 其中每一项的实施。当谈到效率时 更好吗?

他的回应:

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

【问题讨论】:

  • Guido 回复电子邮件真是太棒了。
  • 埃文,我很惊讶我收到了回复
  • 我不认为这是一个坏问题。例如,x * x 比 x ** 2 快了整整 10 倍。在这种情况下,可读性是一个折腾,那么为什么不采用快速的方式呢?
  • 凯西,我和你一起讨论“过早的优化”问题。 :) 在我看来,您的问题不像是过早的优化:任何变体都不会破坏您的代码。当您选择 pow() 而不是 math.sqrt() 时,更多的是更好地了解您所做的事情(就执行时间而言)。
  • 这不是过早的优化,而是避免过早的悲观化(参考编号 28,C++ 编码标准,A.Alexandrescu)。如果math.sqrt 是一个更优化的例程(事实上)并且更清楚地表达了意图,那么它应该始终优于x**.5。知道您编写的内容并不是过早的优化,而是选择了更快且代码更清晰的替代方案。如果是这样,您需要同样充分地论证为什么您会选择其他替代方案。

标签: python performance


【解决方案1】:

很可能是 math.sqrt(x),因为它针对平方根进行了优化。

基准测试将为您提供您正在寻找的答案。

【讨论】:

    【解决方案2】:

    你真正执行了多少平方根?您是否正在尝试用 Python 编写一些 3D 图形引擎?如果不是,那为什么要使用晦涩难懂的代码而不是易于阅读的代码呢?在我能预见的任何应用程序中,时间差将小于任何人所能注意到的。我真的不想放下你的问题,但似乎你在过早的优化方面走得太远了。

    【讨论】:

    • 我真的不觉得我在做一个过早的优化。这更像是一个简单的问题,即从 2 种不同的方法中做出决定,平均而言,哪种方法会更快。
    • Kibbee:这绝对是一个有效的问题,但我与您一样对 Stack Overflow 上暗示提问者正在执行各种过早优化的问题数量感到沮丧。这绝对是每种语言所问问题的很大一部分。
    • math.sqrt(x) 比 x ** 0.5 更容易阅读吗?我认为它们显然都是平方根......至少如果你熟悉python的话。不要仅仅因为你不熟悉 python,就称 ** 之类的标准 python 运算符为“神秘”。
    • 我认为 ** 运算符并不神秘。我认为将某些东西提高到指数 0.5 作为获得平方根的一种方法,对于那些跟不上数学的人来说有点神秘。
    • 如果他正在用 Python 制作 3D 引擎怎么办?
    【解决方案3】:

    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)
    

    10 个循环,3 个循环中的最佳:每个循环 91.1 毫秒

    使用 Python 3.6.9 (notebook)。

    【讨论】:

    • 我现在在 codepad.org 上运行了 3 次,所有 3 次 a() 都比 b() 快得多。
    • 标准的 timeit 模块是你的朋友。它避免了测量执行时间时的常见陷阱!​​
    • 以下是脚本的结果: 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 如你所见,我得到了不同的结果.根据这个,你的答案不是通用的。
    • Codepad 是一项很棒的服务,但对于计时性能来说却很糟糕,我的意思是谁知道服务器在特定时刻会有多忙。每次运行都可能产生非常不同的结果
    • 我为 Linux 上的 py32、py31、py30、py27、py26、pypy、jython、py25、py24 解释器添加了 x**.5 与 sqrt(x) 的性能比较。 gist.github.com/783011
    【解决方案4】:

    在这些微基准测试中,math.sqrt 会更慢,因为在数学命名空间中查找 sqrt 需要一点时间。你可以用

    稍微改进一下
     from math import sqrt
    

    尽管如此,通过 timeit 运行一些变体,x**.5 显示出轻微 (4-5%) 的性能优势

    有趣的是,做

     import math
     sqrt = math.sqrt
    

    加快速度,速度差异在 1% 以内,几乎没有统计学意义。


    我会重复 Kibbee,并说这可能是一个过早的优化。

    【讨论】:

    • 在程序的本地命名空间中定义 sqrt 的原因可能会加快它的速度,这可能是因为方法解析顺序:编译器首先检查函数是否在您的代码中定义,然后在任何导入中,因此,如果它是在本地定义的,则每次查找所需的时间会更少
    【解决方案5】:
    • 优化的第一条规则:不要这样做
    • 第二条规则:不要这样做,但是

    以下是一些时间安排(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
    

    【讨论】:

    • 为什么我很难遵守这条规则?
    【解决方案6】:

    对于它的价值(见吉姆的回答)。在我的机器上,运行 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
    

    【讨论】:

      【解决方案7】:

      使用 Claudiu 的代码,在我的机器上,即使使用“从数学导入 sqrt”x**.5 更快,但使用 psyco.full() sqrt(x) 变得更快,至少快 200%

      【讨论】:

        【解决方案8】:

        Claudiu 的结果与我的不同。我在旧 P4 2.4Ghz 机器上的 Ubuntu 上使用 Python 2.6...这是我的结果:

        >>> timeit1()
        Took 0.564911 seconds
        >>> timeit2()
        Took 0.403087 seconds
        >>> timeit1()
        Took 0.604713 seconds
        >>> timeit2()
        Took 0.387749 seconds
        >>> timeit1()
        Took 0.587829 seconds
        >>> timeit2()
        Took 0.379381 seconds
        

        sqrt 对我来说始终更快......即使 Codepad.org NOW 似乎也同意 sqrt 在本地环境中更快(http://codepad.org/6trzcM3j)。目前,键盘似乎正在运行 Python 2.5。也许他们在 Claudiu 第一次回答时使用的是 2.4 或更早版本?

        事实上,即使使用 math.sqrt(i) 代替 arg(i),我仍然可以更好地使用 sqrt。在这种情况下,timeit2() 在我的机器上花费了 0.53 到 0.55 秒,这仍然比 timeit1 中的 0.56-0.60 数字要好。

        我想说,在现代 Python 上,使用 math.sqrt 并肯定将其带到本地上下文中,可以使用 somevar=math.sqrt 或 from math import sqrt。

        【讨论】:

          【解决方案9】:

          在 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
          

          【讨论】:

            【解决方案10】:

            有人评论了 Quake 3 中的“快速 Newton-Raphson 平方根”...我使用 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 的 1/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
            

            【讨论】:

              【解决方案11】:

              如果你进入 math.py 并将函数“sqrt”复制到你的程序中会更快。您的程序需要一些时间才能找到 math.py,然后打开它,找到您要查找的函数,然后将其带回您的程序。如果即使使用“查找”步骤该功能也更快,那么该功能本身必须非常快。可能会将您的时间缩短一半。总结:

              1. 转到 math.py
              2. 找到函数“sqrt”
              3. 复制它
              4. 将函数作为 sqrt finder 粘贴到您的程序中。
              5. 计时。

              【讨论】:

              • 那不行;见stackoverflow.com/q/18857355/3004881。还要注意原始问题中的引用,它说它是一个 C 函数的链接。另外,复制函数的源代码和from math import sqrt有什么不同?
              • 不会的,我这么说只是为了弄清楚调用这两个函数有什么区别。
              【解决方案12】:

              我最近解决的问题SQRMINSUM 需要在大型数据集上重复计算平方根。在我进行其他优化之前,我的history 中最旧的 2 次提交仅通过将 **0.5 替换为 sqrt() 不同,从而将 PyPy 中的运行时间从 3.74 秒减少到 0.51 秒。这几乎是 Claudiu 测量的 400% 的巨大改进的两倍。

              【讨论】:

                【解决方案13】:

                要优化的 Python 风格是可读性。为此,我认为明确使用 sqrt 函数是最好的。话虽如此,还是让我们研究一下性能。

                我为 Python 3 更新了 Claudiu 的代码,并且还使得优化计算变得不可能(一个好的 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
                

                Try it yourself.

                【讨论】:

                • 我发现 **0.5 更具可读性;在编写数学表达式时,如果可用,我宁愿在整个过程中使用数学运算符,而不是函数。我使用 ** 的原因与我写 -1 而不是 neg(1) 或 a + b 而不是 add(a, b) 的原因相同。
                【解决方案14】:

                当然,如果处理文字并需要一个常量值,Python 运行时可以在编译时预先计算该值,如果它是用运算符编写的 - 在这种情况下不需要分析每个版本:

                In [77]: dis.dis(a)                                                                                                                       
                  2           0 LOAD_CONST               1 (1.4142135623730951)
                              2 RETURN_VALUE
                
                In [78]: def a(): 
                    ...:     return 2 ** 0.5 
                    ...:                                                                                                                                  
                
                In [79]: import dis                                                                                                                       
                
                In [80]: dis.dis(a)                                                                                                                       
                  2           0 LOAD_CONST               1 (1.4142135623730951)
                              2 RETURN_VALUE
                
                

                【讨论】:

                  猜你喜欢
                  • 2014-01-13
                  • 2010-10-29
                  • 2016-03-16
                  • 2011-09-22
                  • 1970-01-01
                  • 2019-07-15
                  • 1970-01-01
                  • 2011-03-14
                  • 1970-01-01
                  相关资源
                  最近更新 更多