为什么提前回报比其他回报慢?

这是我几天前回答的后续问题。 编辑:这个问题的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] 

…换言之:无论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 

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

我尝试了你的代码,并得到相同的结果, with_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] 

考虑到字节码是相同的,唯一的区别是函数的名字。 特别是时间testing对全球名称进行查询。 尝试重命名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,其模数大小减less(32)意味着它被存储在表的#8槽中。

without_else散列到505688136减less模32是8,所以有碰撞。 为了解决这个Python计算:

从…开始:

 j = hash % 32 perturb = hash 

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

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

这给了它17作为下一个索引。 幸运的是,这个循环只能重复一次。 散列表大小是2的幂,所以2**i是散列表的大小, i是从散列值j使用的比特数。

表中的每个探针都可以find其中的一个:

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

  • 该槽没有使用,但在过去使用,在这种情况下,我们去尝试按上面计算的下一个值。

  • 该槽已满,但存储在表中的完整散列值与我们正在查找的关键字的散列值不同(这就是__builtins__ vs without_else的情况)。

  • 槽已满,并且具有我们想要的散列值,然后Python检查我们正在查找的键和对象是否是相同的对象(在这种情况下,它们将是因为可能是标识符的短string被实现了相同的标识符使用完全相同的string)。

  • 最后,当插槽已满,哈希值完全匹配,但键不是相同的对象,那么只有这样,Python才会尝试比较它们的相等性。 这是比较慢,但在名称查找的情况下不应该实际发生。