【问题标题】:Why is early return slower than else?为什么提早返回比其他方法慢?
【发布时间】:2012-01-06 10:53:37
【问题描述】:

这是an answer I gave a few days back 的后续问题。 编辑:似乎该问题的 OP 已经使用我发布给他的代码询问 the same question,但我不知道。道歉。但是提供的答案不同!

我基本上观察到:

>>> def without_else(param=False):
...     if param:
...         return 1
...     return 0
>>> def with_else(param=False):
...     if param:
...         return 1
...     else:
...         return 0
>>> from timeit import Timer as T
>>> T(lambda : without_else()).repeat()
[0.3011460304260254, 0.2866089344024658, 0.2871549129486084]
>>> T(lambda : with_else()).repeat()
[0.27536892890930176, 0.2693932056427002, 0.27011704444885254]
>>> T(lambda : without_else(True)).repeat()
[0.3383951187133789, 0.32756996154785156, 0.3279120922088623]
>>> T(lambda : with_else(True)).repeat()
[0.3305950164794922, 0.32186388969421387, 0.3209099769592285]

...或者换句话说:无论if 条件是否被触发,使用else 子句会更快。

我认为这与两者生成的不同字节码有关,但有人能够详细确认/解释吗?

编辑: 似乎不是每个人都能重现我的计时,所以我认为提供一些关于我的系统的信息可能会很有用。我正在运行安装了默认 python 的 Ubuntu 11.10 64 位。 python 生成以下版本信息:

Python 2.7.2+ (default, Oct  4 2011, 20:06:09) 
[GCC 4.6.1] on linux2

以下是 Python 2.7 中的反汇编结果:

>>> dis.dis(without_else)
  2           0 LOAD_FAST                0 (param)
              3 POP_JUMP_IF_FALSE       10

  3           6 LOAD_CONST               1 (1)
              9 RETURN_VALUE        

  4     >>   10 LOAD_CONST               2 (0)
             13 RETURN_VALUE        
>>> dis.dis(with_else)
  2           0 LOAD_FAST                0 (param)
              3 POP_JUMP_IF_FALSE       10

  3           6 LOAD_CONST               1 (1)
              9 RETURN_VALUE        

  5     >>   10 LOAD_CONST               2 (0)
             13 RETURN_VALUE        
             14 LOAD_CONST               0 (None)
             17 RETURN_VALUE        

【问题讨论】:

  • 我现在找不到关于 SO 的相同问题。他们检查了生成的字节码,还有一个额外的步骤。观察到的差异非常依赖于测试人员(机器、SO..),有时只发现非常非常小的差异。
  • 在 3.x 上,两者都生成 相同 字节码,但在 with_else 末尾会保留一些无法访问的代码(LOAD_CONST(None); RETURN_VALUE - 但如前所述,从未达到)。我非常怀疑死代码会使函数更快。有人可以在 2.7 上提供dis 吗?
  • 我无法重现这一点。 elseFalse 的函数是最慢的(152ns)。第二快的是True,没有else(143ns),另外两个基本相同(137ns和138ns)。我没有使用默认参数,而是在 iPython 中用%timeit 测量它。
  • 我无法重现这些时间,有时 with_else 更快,有时这是 without_else 版本,看起来它们对我来说非常相似......
  • 增加了反汇编的结果。我正在使用 Ubuntu 11.10、64 位、库存 Python 2.7 - 与 @mac 相同的配置。我也同意with_else 明显更快。

标签: python optimization python-2.7


【解决方案1】:

这是一个纯粹的猜测,我还没有想出一个简单的方法来检查它是否正确,但我有一个理论给你。

我尝试了您的代码并得到了相同的结果,without_else() 反复比with_else() 稍慢:

>>> T(lambda : without_else()).repeat()
[0.42015745017874906, 0.3188967452567226, 0.31984281521812363]
>>> T(lambda : with_else()).repeat()
[0.36009842032996175, 0.28962249392031936, 0.2927151355828528]
>>> T(lambda : without_else(True)).repeat()
[0.31709728471076915, 0.3172671387005721, 0.3285821242644147]
>>> T(lambda : with_else(True)).repeat()
[0.30939889008243426, 0.3035132258429485, 0.3046679117038593]

考虑到字节码是相同的,唯一的区别是函数的名称。特别是时序测试会查找全局名称。尝试重命名without_else(),差异就消失了:

>>> def no_else(param=False):
    if param:
        return 1
    return 0

>>> T(lambda : no_else()).repeat()
[0.3359846013948413, 0.29025818923918223, 0.2921801513879245]
>>> T(lambda : no_else(True)).repeat()
[0.3810395594970828, 0.2969634408842694, 0.2960104566362247]

我的猜测是 without_elseglobals() 中的其他内容存在哈希冲突,因此全局名称查找速度稍慢。

编辑:有 7 个或 8 个键的字典可能有 32 个插槽,因此在此基础上 without_else__builtins__ 发生哈希冲突:

>>> [(k, hash(k) % 32) for k in globals().keys() ]
[('__builtins__', 8), ('with_else', 9), ('__package__', 15), ('without_else', 8), ('T', 21), ('__name__', 25), ('no_else', 28), ('__doc__', 29)]

澄清散列的工作原理:

__builtins__ 散列到 -1196389688 以减少表大小的模数 (32) 意味着它存储在表的 #8 槽中。

without_else 散列到 505688136,将模 32 减少为 8,因此存在冲突。要解决这个 Python 计算:

开始于:

j = hash % 32
perturb = hash

重复此操作,直到找到空闲槽:

j = (5*j) + 1 + perturb;
perturb >>= 5;
use j % 2**i as the next table index;

这使它 17 用作下一个索引。幸运的是,这是免费的,所以循环只重复一次。哈希表大小是 2 的幂,所以 2**i 是哈希表的大小,i 是哈希值 j 中使用的位数。

对表的每个探测都可以找到以下之一:

  • 插槽是空的,在这种情况下探测停止并且我们知道 值不在表中。

  • 该插槽未使用,但过去曾使用过,在这种情况下我们去尝试 如上计算的下一个值。

  • 槽已满,但表中存储的完整哈希值未满 与我们正在寻找的密钥的哈希相同(这就是 在__builtins__ vs without_else 的情况下发生)。

  • 槽满了,正好有我们想要的哈希值,然后是Python 检查我们正在查找的键和对象是否是 相同的对象(在这种情况下它们将是因为短字符串 可能是标识符被实习所以相同的标识符使用 完全相同的字符串)。

  • 最后当slot满的时候,hash完全匹配,但是key 不是相同的对象,然后只有这样 Python 才会尝试 比较它们是否相等。这相对较慢,但在 实际上不应发生名称查找的情况。

【讨论】:

  • @Chris,字符串的长度不应该很重要。第一次对字符串进行哈希处理时,所花费的时间与字符串长度成正比,但计算出的哈希值会缓存在字符串对象中,因此后续哈希值是 O(1)。
  • 啊,好吧,我不知道缓存,但这是有道理的
  • 令人着迷!我可以叫你夏洛克吗? ;) 无论如何,只要问题符合条件,我希望我不会忘记给你一些额外的奖励积分。
  • @mac,不完全是。我将添加一些关于哈希解析的内容(本来打算将它挤进评论中,但它比我想象的更有趣)。
  • @Duncan - 非常感谢您花时间说明哈希过程。一流的答案! :)
猜你喜欢
  • 2014-01-18
  • 1970-01-01
  • 2011-09-18
  • 1970-01-01
  • 2019-09-07
  • 1970-01-01
  • 2014-01-21
  • 1970-01-01
  • 2022-06-13
相关资源
最近更新 更多