我基本上观察到:
>>> 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是否触发条件,使用该子句都会更快。
else
if
我认为这与两者生成的不同字节码有关,但有人能够详细确认/解释吗?
编辑: 似乎不是每个人都能重现我的时间,所以我认为提供一些关于我的系统的信息可能很有用。我正在运行安装了默认 python 的 Ubuntu 11.10 64 位。python生成以下版本信息:
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
这是一个纯粹的猜测,我还没有找到一个简单的方法来检查它是否正确,但我有一个理论给你。
我尝试了您的代码并获得了相同的结果,但without_else()反复略慢于with_else():
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()所以全局名称查找稍微慢一些。
without_else
globals()
编辑 :具有 7 个或 8 个键的字典可能有 32 个插槽,因此在此基础上与以下without_else内容存在哈希冲突__builtins__:
__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**i是i从哈希值中使用的位数j。
2**i
i
j
对表的每个探测都可以找到以下之一:
插槽是空的,在这种情况下探测停止并且我们知道该值不在表中。
该插槽未使用,但在过去使用过,在这种情况下,我们将尝试按上面计算的下一个值。
插槽已满,但存储在表中的完整哈希值与我们正在查找的键的哈希值不同(这就是__builtins__vs的情况without_else)。
插槽已满,并且具有我们想要的哈希值,然后 Python 检查键和我们正在查找的对象是否是同一个对象(在这种情况下,它们将是因为可能是标识符的短字符串被实习,所以相同的标识符使用完全相同的字符串)。
最后,当插槽已满时,哈希完全匹配,但键不是相同的对象,然后 Python 才会尝试比较它们是否相等。这是相对较慢的,但在名称查找的情况下实际上不应该发生。