为何提前返回比else慢?
这是我之前在一个问题上给出的回答的后续提问。补充:看起来那个问题的提问者已经用我发给他的代码去问了同样的问题,但我之前并不知道。对此我表示歉意。不过提供的答案是不同的!
我观察到的主要内容是:
>>> 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
语句的速度都是更快的。
我猜这可能和两者生成的字节码不同有关,但有没有人能确认或详细解释一下呢?
补充:似乎并不是每个人都能复现我的时间测试结果,所以我觉得提供一些关于我系统的信息可能会有帮助。我正在运行的是64位的Ubuntu 11.10,安装的是默认的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
1 个回答
这只是我的一个猜测,我还没找到简单的方法来验证它是否正确,但我有个理论可以分享给你。
我试过你的代码,结果是一样的,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
和全局命名空间中的其他东西发生了哈希冲突,所以全局名称查找稍微慢了一点。
编辑: 一个有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__
和without_else
的情况)。槽位是满的,并且恰好有我们想要的哈希值,这时Python会检查键和我们查找的对象是否是同一个对象(在这种情况下,它们是相同的,因为短字符串作为标识符会被存储,所以相同的标识符使用的是完全相同的字符串)。
最后,当槽位满了,哈希值完全匹配,但键不是同一个对象时,Python才会尝试比较它们是否相等。这相对较慢,但在名称查找的情况下其实不应该发生。