【问题标题】:Why does removing the else slow down my code?为什么删除 else 会减慢我的代码速度?
【发布时间】:2012-01-02 11:20:57
【问题描述】:

考虑以下函数:

def fact1(n):
    if n < 2:
        return 1
    else:
        return n * fact1(n-1)

def fact2(n):
    if n < 2:
        return 1
    return n * fact2(n-1)

它们应该是等价的。但存在性能差异:

>>> T(lambda : fact1(1)).repeat(number=10000000)
[2.5754408836364746, 2.5710129737854004, 2.5678811073303223]
>>> T(lambda : fact2(1)).repeat(number=10000000)
[2.8432059288024902, 2.834425926208496, 2.8364310264587402]

没有else 的版本慢了10%。这是相当重要的。为什么?

【问题讨论】:

  • 你做了多少次测试?
  • @M4tt4n 呃....repeat(number=10000000)。 @Cat Plus Plus 是的,但是找出事情为什么会这样运作很有趣,对吧?
  • @GabiPurcaru:如果它导致专注于微优化,则不会。这是不健康的,完全是浪费时间。我真的不知道为什么人们会赞成这样的问题。
  • @GabiPurcaru 我赞成这类问题,因为它们奖励那些真正想了解他们的代码实际作用的人。研究时间差异和代码生成的人通常最终对语言有深刻的理解。
  • @CatPlusPlus 我真的不知道为什么人们会否决这样的问题。

标签: python performance recursion


【解决方案1】:

对我来说,它们的速度几乎相同:(Debian 上的 Python 2.6.6)

In [4]: %timeit fact1(1)
10000000 loops, best of 3: 151 ns per loop

In [5]: %timeit fact2(1)
10000000 loops, best of 3: 154 ns per loop

字节码也很相似:

In [6]: dis.dis(fact1)
  2           0 LOAD_FAST                0 (n)
              3 LOAD_CONST               1 (2)
              6 COMPARE_OP               0 (<)
              9 JUMP_IF_FALSE            5 (to 17)
             12 POP_TOP             

  3          13 LOAD_CONST               2 (1)
             16 RETURN_VALUE        
        >>   17 POP_TOP             

  5          18 LOAD_FAST                0 (n)
             21 LOAD_GLOBAL              0 (fact)
             24 LOAD_FAST                0 (n)
             27 LOAD_CONST               2 (1)
             30 BINARY_SUBTRACT     
             31 CALL_FUNCTION            1
             34 BINARY_MULTIPLY     
             35 RETURN_VALUE        
             36 LOAD_CONST               0 (None)
             39 RETURN_VALUE        

In [7]: dis.dis(fact2)
  2           0 LOAD_FAST                0 (n)
              3 LOAD_CONST               1 (2)
              6 COMPARE_OP               0 (<)
              9 JUMP_IF_FALSE            5 (to 17)
             12 POP_TOP             

  3          13 LOAD_CONST               2 (1)
             16 RETURN_VALUE        
        >>   17 POP_TOP             

  4          18 LOAD_FAST                0 (n)
             21 LOAD_GLOBAL              0 (fact)
             24 LOAD_FAST                0 (n)
             27 LOAD_CONST               2 (1)
             30 BINARY_SUBTRACT     
             31 CALL_FUNCTION            1
             34 BINARY_MULTIPLY     
             35 RETURN_VALUE        

唯一的区别是带有else 的版本包含在控制到达函数体末尾时返回None 的代码。

【讨论】:

  • 我在 Python 2.7 和 3.2 上进行了相同的计时 - 结果与您在 2.6 上找到的结果几乎相同。
  • 这很奇怪,我在另一台机器上计时,并没有得到太大的不同。
  • 那么为什么它会更快呢?我自己也很感兴趣。
  • @the_drow:差异并不显着。如果我再测量一次,结果很可能是相反的。
【解决方案2】:

这里发生的是fact2 与您的模块全局变量中的__name__ 存在哈希冲突。这使得全局fact2 的查找变得稍微慢了一点。

>>> [(k, hash(k) % 32) for k in globals().keys() ]
[('__builtins__', 8), ('__package__', 15), ('fact2', 25), ('__name__', 25), ('fact1', 26), ('__doc__', 29)]

即与Why is early return slower than else? 的答案相同,只是与__builtins__ 存在哈希冲突

【讨论】:

    【解决方案3】:

    我质疑时间安排。这两个函数不会递归到自己身上。 fact1 和 fact2 都调用了未显示的 fact。

    一旦修复,反汇编(在 Py2.6 和 Py2.7 中)显示两者都在运行相同的操作码,除了递归到函数的名称。名称的选择会触发时间上的微小差异,因为 fact1 可能会在没有名称冲突的情况下插入模块字典,而 *fact2) 可能具有与模块中的另一个名称冲突的哈希值。

    换句话说,您在时间上看到的任何差异都不是由于选择是否存在 else 子句:-)

    【讨论】:

    • 不仅如此,论据不是在每种情况下都是1吗?所以它只是返回 1?
    • @pessimopoppotamus - 是的,我为您发布的代码中出现了复制和粘贴错误! :) 如果能通过在my answer 上添加评论来通知我,那就太好了,这样我今天就没有问同样的问题了! :)
    猜你喜欢
    • 2018-11-30
    • 2019-01-03
    • 2021-06-15
    • 1970-01-01
    • 1970-01-01
    • 2018-01-02
    • 2017-01-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多