疑惑
在普遍的认知中,位运算是比取模运算快的。然而在使用Python进行简单的测试中,却意外发现位运算居然比取模慢,比如下面的代码:
>>> timeit.timeit('[i for i in range(1000) if i % 2]', number=1000)
0.07428571907803416
>>> timeit.timeit('[i for i in range(1000) if i & 1]', number=1000)
0.10028700600378215
计算1000以内的奇数,位运算居然比取模慢?
思考
这其中到底发生了什么?通过资料搜集,最终在stackoverflow上找到了线索。首先,Python中的int跟C语言中的int是有区别的,它们是对象Object。在底层实现上,使用的是一个数字列表,这也是Python中int不限制大小的原因。有兴趣可以了解一下CPython对int的定义。
其次,针对single limb integer,*、/、%三种运算有优化;而位运算则是针对任意精度的。
single limb integer这个说法笔者也是第一次接触,解释如下:a "limb" is a base-2^30 "digit", stored as a binary integer in a uint32_t or equivalent. CPython stores big integers in chunks of that size. Just like how you can add decimal numbers on paper one digit at a time, using base-2^30 digits allows CPython to get lots of work done with each primitive add operation the CPU does. (Using base 2^32 chunks would allow using add / add-with-carry in asm to take advantage of hardware support for big-integers on most architectures, but that's not what CPython does. 2^30 has some advantages, though, esp. for pure C without asm.)
可以简单理解成,小于2^30的数,为single limb integer。
到目前为止,导致文章开头诡异表现的原因已经分析得差不多了。让我们使用大于2^30的数,验证一下。
# coding: utf-8
import timeit
def fun_1(n):
return [i for i in range(2 ** n, 2 ** n + 1000) if i % 2]
def fun_2(n):
return [i for i in range(2 ** n, 2 ** n + 1000) if i & 1]
print('{}\t{}\t{}'.format('n', 'modulo', 'bit_operation'))
for n in range(25, 45):
fun_1_time = timeit.timeit(
'fun_1(n)',
setup='from __main__ import n, fun_1',
number=1000
)
fun_2_time = timeit.timeit(
'fun_2(n)',
setup='from __main__ import n, fun_2',
number=1000
)
print('{}\t{:f}\t{:f}'.format(
n,
fun_1_time,
fun_2_time,
))
输出结果如下:
n modulo bit_operation
25 0.070913 0.091450
26 0.075533 0.091473
27 0.069695 0.096567
28 0.076114 0.100146
29 0.077445 0.091671
30 0.119914 0.091599
31 0.118146 0.098614
32 0.131388 0.098873
33 0.121872 0.102023
34 0.114846 0.092842
35 0.119020 0.091165
36 0.118940 0.098211
37 0.123240 0.099836
38 0.128368 0.092894
39 0.112679 0.091436
40 0.123808 0.100071
41 0.114746 0.091711
42 0.113422 0.091883
43 0.118400 0.091734
44 0.113491 0.091707
很明显的发现,位运算在 2^25 和 2^44 的表现很稳定,也从侧面验证了位运算是针对所有精度实现的。而取模则呈线性增长,而且笔者猜测跟limb的数目有关。
总结
使用Python的话,对于 2^30 以内的数字操作,并没有必要使用位运算来提高效率,直接使用*、/、%即可。