Brian Kernighan的用于在python中计数的算法与用于在字符串中计数的内置函数相比,在性能上的巨大差异令我感到惊讶。
在我看来,将字符串转换为字符串然后计数是一个坏主意。
现在,似乎不好的主意是循环而不是在寻找性能时使用内置函数。
import random
x = random.randint(0,1<<1000000)
def count_ones(number):
c = 0
while(number !=0 ):
number = number&(number-1)
c = c + 1
return c
%timeit bin(x).count("1")
%timeit count_ones(x)
5.09 ms ± 20 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
25 s ± 544 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
答案 0 :(得分:1)
Kernighan的算法在适合ALU的数据上效果最好,在现代硬件上通常为64位。对于更长的数字,由于每次迭代都在数字的整个长度上进行计算,因此该算法在数字的长度上变为二次方。可以手动优化该算法,因为很明显,一旦借位停止传播,按位与的结果就不会改变。
即使进行了这样的优化,我们仍然处在画家史莱米尔的境界。计算是二次方的,因为每次扫描总是在同一位置有效地开始扫描,每次扫描都越来越远。
无论如何,即使是复杂的优化器也发现优化和Python的bignum实现没有复杂的优化器,这将非常令人惊讶。例如,它不会将减量与按位和相融合。
显然可以按位执行并在bignum上完成,因此写这样的尝试很诱人:
number &= number - 1
希望可以执行就地操作。但这与CPython并没有任何区别。 CPython的大数是不可变的,因此就地突变是不可能的。
简而言之,Python将为每次迭代创建两个新的百万位数字。花了一段时间并不奇怪。而是令人惊讶的是,仅需25秒。取决于版本和平台,CPython bignum操作以15位或30位为单位执行。 (对适合30位的整数的计算略有优化。但是64位远远超出了该范围,因此不要期望将数字限制为64位以避免大数目的开销。)假设您的平台使用30位单元,则运行对于预期的50万次迭代,该算法意味着执行超过660亿个单字计算(一个字大数相减和按位运算((1000000/30 = 33334)个字),再加上一百万个130KB内存分配。在25秒内完成操作完全不算破。这证明了现代CPU的速度有多快,同时也警告说在数字变得非常大之前不注意使用二次算法是多么容易。