这是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
答案 0 :(得分:368)
这是一个纯粹的猜测,我还没有想出一个简单的方法来检查它是否正确,但我有一个理论适合你。
我尝试了您的代码并获得相同的结果,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才会尝试 比较他们的平等。这是相对缓慢的,但在 名称查找的情况实际上不应该发生。