Why is Collections.counter so slow?
我正在尝试解决Rosalind的基本问题,即计算给定序列中的核苷酸,并将结果返回到列表中。对于那些不熟悉生物信息学的人来说,它只是计算字符串中4个不同字符('A','C','G','T')的出现次数。
我期望
但令我惊讶的是,这种方法是最慢的!
我比较了三种不同的方法,分别使用
- 长时间运行几次
- 短时间运行很多次。
这是我的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | import timeit from collections import Counter # Method1: using count def method1(seq): return [seq.count('A'), seq.count('C'), seq.count('G'), seq.count('T')] # method 2: using a loop def method2(seq): r = [0, 0, 0, 0] for i in seq: if i == 'A': r[0] += 1 elif i == 'C': r[1] += 1 elif i == 'G': r[2] += 1 else: r[3] += 1 return r # method 3: using Collections.counter def method3(seq): counter = Counter(seq) return [counter['A'], counter['C'], counter['G'], counter['T']] if __name__ == '__main__': # Long dummy sequence long_seq = 'ACAGCATGCA' * 10000000 # Short dummy sequence short_seq = 'ACAGCATGCA' * 1000 # Test 1: Running a long sequence once print timeit.timeit("method1(long_seq)", setup='from __main__ import method1, long_seq', number=1) print timeit.timeit("method2(long_seq)", setup='from __main__ import method2, long_seq', number=1) print timeit.timeit("method3(long_seq)", setup='from __main__ import method3, long_seq', number=1) # Test2: Running a short sequence lots of times print timeit.timeit("method1(short_seq)", setup='from __main__ import method1, short_seq', number=10000) print timeit.timeit("method2(short_seq)", setup='from __main__ import method2, short_seq', number=10000) print timeit.timeit("method3(short_seq)", setup='from __main__ import method3, short_seq', number=10000) |
结果:
1 2 3 4 5 6 7 8 9 | Test1: Method1: 0.224009990692 Method2: 13.7929501534 Method3: 18.9483819008 Test2: Method1: 0.224207878113 Method2: 13.8520510197 Method3: 18.9861831665 |
在两个实验中,方法1比方法2和3快得多!
所以我有一系列问题:
-
我是在做错什么,还是确实比其他两种方法慢?有人可以运行相同的代码并共享结果吗?
-
如果我的结果正确,(也许应该是另一个问题),有没有比使用方法1更快的方法来解决此问题?
-
如果
count 更快,那么collections.Counter 怎么处理?
这不是因为
另一方面,
这意味着
只是为该语句添加更多上下文。
字符串存储为包装为python对象的C数组。
另一方面,
因此,减速是因为:
- 每个字符都必须转换为Python对象(这是性能下降的主要原因)
-
循环是在Python中完成的(不适用于python 3.x中的
Counter ,因为它是用C重写的) - 每个比较都必须在Python中完成(而不仅仅是比较C中的数字-字符由数字表示)
- 计数器需要散列值,而循环需要索引列表。
请注意,速度降低的原因类似于关于Python的阵列为什么速度慢的问题。
我做了一些其他的基准测试,以找出在什么时候
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | from collections import Counter import random import string characters = string.printable # 100 different printable characters results_counter = [] results_count = [] nchars = [] for i in range(1, 110, 10): chars = characters[:i] string = ''.join(random.choice(chars) for _ in range(10000)) res1 = %timeit -o Counter(string) res2 = %timeit -o {char: string.count(char) for char in chars} nchars.append(len(chars)) results_counter.append(res1) results_count.append(res2) |
并使用matplotlib绘制结果:
1 2 3 4 5 6 7 8 9 | import matplotlib.pyplot as plt plt.figure() plt.plot(nchars, [i.best * 1000 for i in results_counter], label="Counter", c='black') plt.plot(nchars, [i.best * 1000 for i in results_count], label="str.count", c='red') plt.xlabel('number of different characters') plt.ylabel('time to count the chars in a string of length 10000 [ms]') plt.legend() |
Python 3.5的结果
Python 3.6的结果非常相似,因此我没有明确列出它们。
因此,如果您要计算80个不同的字符,
Python 2.7的结果
在Python-2.7中,
这里的时差很容易解释。这一切都归结为在Python中运行的内容以及以本机代码运行的内容。后者将始终更快,因为它没有很多评估开销。
现在,这已经是为什么四次调用
第二种收集数组中计数的方法实际上是以下方法的性能较低的版本:
1 2 3 4 5 6 7 8 9 10 11 12 | def method4 (seq): a, c, g, t = 0, 0, 0, 0 for i in seq: if i == 'A': a += 1 elif i == 'C': c += 1 elif i == 'G': g += 1 else: t += 1 return [a, c, g, t] |
这里,所有四个值都是单独的变量,因此更新它们非常快。实际上,这比更改列表项要快一点。
但是,这里的总体性能"问题"是这会在Python中迭代字符串。因此,这将创建一个字符串迭代器,然后将每个字符分别生成为实际的字符串对象。这会产生很大的开销,这也是每个通过迭代Python中的字符串而起作用的解决方案都会变慢的主要原因。
正如其他人已经指出的那样,您正在将相当具体的代码与相当一般的代码进行比较。
考虑一下,将您感兴趣的字符拼出一个循环这样的琐碎小事已经为您买到了因子2,即
1 2 3 4 5 6 7 8 9 10 11 12 13 | def char_counter(text, chars='ACGT'): return [text.count(char) for char in chars] %timeit method1(short_seq) # 100000 loops, best of 3: 18.8 μs per loop %timeit char_counter(short_seq) # 10000 loops, best of 3: 40.8 μs per loop %timeit method1(long_seq) # 10 loops, best of 3: 172 ms per loop %timeit char_counter(long_seq) # 1 loop, best of 3: 374 ms per loop |
您的
不幸的是,Python没有提供一种快速的方法来利用问题的特定条件。
但是,您可以为此使用Cython,然后您就可以胜过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | %%cython -c-O3 -c-march=native -a #cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True, infer_types=True import numpy as np cdef void _count_acgt( const unsigned char[::1] text, unsigned long len_text, unsigned long[::1] counts): for i in range(len_text): if text[i] == b'A': counts[0] += 1 elif text[i] == b'C': counts[1] += 1 elif text[i] == b'G': counts[2] += 1 else: counts[3] += 1 cpdef ascii_count_acgt(text): counts = np.zeros(4, dtype=np.uint64) bin_text = text.encode() return _count_acgt(bin_text, len(bin_text), counts) |
1 2 3 4 | %timeit ascii_count_acgt(short_seq) # 100000 loops, best of 3: 12.6 μs per loop %timeit ascii_count_acgt(long_seq) # 10 loops, best of 3: 140 ms per loop |