为什么提早归还比其他要慢?


179

这是我几天前给出的答案的后续问题。编辑:该问题的OP似乎已经使用了我发布给他的代码来问同样的问题,但是我没有意识到。道歉。提供的答案是不同的!

我基本上观察到:

>>> 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]

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

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

编辑:似乎不是每个人都可以重现我的时间安排,所以我认为在我的系统上提供一些信息可能有用。我正在运行安装了默认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        

1
所以我现在找不到一个相同的问题。他们检查了生成的字节码,这又是另一步骤。观察到的差异非常取决于测试仪(机器,SO ..),有时仅发现非常小的差异。
joaquin 2011年

3
在3.x上,两者都产生相同的字节码LOAD_CONST(None); RETURN_VALUE,但在末尾有一些无法到达的代码(-但如上所述,从未达到)with_else。我高度怀疑死代码会使功能更快。有人可以提供dis2.7吗?

4
我无法重现此内容。使用else和的功能False最慢(152ns)。第二快的是True没有else(143ns),而其他两个基本相同(137ns和138ns)。我没有使用默认参数,而是%timeit在iPython中对其进行了测量。
rplnt 2011年

我无法重现的时刻,有时with_else更快,有时这是without_else版本,它看起来像他们对我很相似...
塞德里克朱利安

1
增加了拆卸结果。我使用的是Ubuntu 11.10、64位,Python 2.7版本-与@mac相同的配置。我也同意这with_else显然更快。
克里斯·摩根

Answers:


387

这纯粹是猜测,我还没有找到一种简单的方法来检查它是否正确,但是我有一个理论适合您。

我尝试了您的代码并获得了相同的结果,但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_else与其他内容发生了哈希冲突,globals()因此全局名称查找稍微慢一些。

编辑:具有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的幂2**ii是哈希值使用的位数j

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

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

  • 该广告位尚未使用,但已在过去使用过,在这种情况下,我们尝试使用如上计算的下一个值。

  • 插槽已满,但是存储在表中的完整哈希值与我们要查找的键的哈希值不同(在__builtins__vs 的情况下就是这样without_else)。

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

  • 最终,当插槽已满时,哈希值完全匹配,但是键不是同一对象,然后Python才会尝试比较它们是否相等。这相对较慢,但实际上不应该进行名称查找。


9
@Chris,字符串的长度不应该太大。第一次对字符串进行哈希处理时,将花费与字符串长度成比例的时间,但随后将计算出的哈希值缓存在字符串对象中,因此后续哈希为O(1)。
邓肯,

1
嗯,我还不知道缓存,但这是有道理的
克里斯·埃伯

9
迷人!我可以叫你夏洛克吗?;)无论如何,我希望我不会忘记在问题获得资格后立即给您一些额外的奖励。
Voo

4
@mac,不完全是。我将添加一些有关哈希解析的信息(将其压缩到注释中,但是比我想象的更有趣)。
邓肯

6
@Duncan-非常感谢您花时间说明哈希过程。一流的答案!:)
mac
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.