2011-11-25 59 views
151

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

...或者换句话说:具有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

+3

在3.x上,它们都产生了**相同的**字节码,保存了一些无法访问的代码('LOAD_CONST(None); RETURN_VALUE' - 但如前所述,它从来没有达到)在'with_else'结尾。我非常怀疑死代码使得函数更快。有人可以在2.7上提供“dis”吗? – delnan

+4

我无法重现这一点。带有'else'和'False'的函数是其中最慢的(152ns)。第二快的是没有“其他”(143ns)的“True”,另外两个基本相同(137ns和138ns)。我没有使用默认参数,并使用iPython中的'%timeit'来测量它。 – rplnt

回答

329

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

我想你的代码,并得到相同的结果的,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**i是散列表的大小,i是从散列值j使用的位数。

每个探测到表中可以找到其中的一个:

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

  • 槽未使用,但在过去使用,在这种情况下,我们去尝试 按照上述计算的下一个值。

  • 插槽已满,但存储在表中的全部哈希值不 一样,我们正在寻找关键的哈希(这就是 发生在__builtins__ VS without_else的情况下)。

  • 插槽已满,具有正是我们想要的哈希值,那么Python 检查是否的关键,我们正在查找的对象是 同一对象(在这种情况下,他们会因为短字符串 可能是标识符被实施,因此完全相同的标识符使用 完全相同的字符串)。

  • 最后,当槽满,散列完全匹配,但 是不是同一对象,然后才把钥匙将Python的尝试 比较它们是否相等。这是比较慢的,但在名称查找的情况下不应该实际发生。

+50

辉煌的侦探工作,很好的答案! –

+0

考虑到哈希码函数线性缩放,单独使用名称的长度可能是一个因素 –

+9

@Chris,字符串的长度不应该是重要的,第一次哈希一个字符串需要时间与字符串长度成正比,但随后计算的散列被缓存在字符串对象中,所以后面的散列是O(1)。 – Duncan