Python 为什么收款台这么慢?
我试图解决Rosalind的一个基本问题,计算给定序列中的核苷酸,并返回列表中的结果。对于那些不熟悉生物信息学的人来说,这只是计算字符串中4个不同字符('A'、'C'、'G'、'T')的出现次数 我认为Python 为什么收款台这么慢?,python,performance,collections,counter,bioinformatics,matplotlib,Python,Performance,Collections,Counter,Bioinformatics,Matplotlib,我试图解决Rosalind的一个基本问题,计算给定序列中的核苷酸,并返回列表中的结果。对于那些不熟悉生物信息学的人来说,这只是计算字符串中4个不同字符('A'、'C'、'G'、'T')的出现次数 我认为collections.Counter是最快的方法(首先是因为它们声称具有高性能,其次是因为我看到很多人使用它来解决这个特定问题) 但令我惊讶的是,这种方法是最慢的 我比较了三种不同的方法,使用timeit并运行两种类型的实验: 运行长序列几次 多次运行短序列 这是我的密码: import t
collections.Counter
是最快的方法(首先是因为它们声称具有高性能,其次是因为我看到很多人使用它来解决这个特定问题)
但令我惊讶的是,这种方法是最慢的
我比较了三种不同的方法,使用timeit
并运行两种类型的实验:
- 运行长序列几次
- 多次运行短序列
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)
结果:
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的处理方法是什么
- 这不是因为
收集。计数器速度慢,实际上相当快,但它是一个通用工具,计数字符只是许多应用程序中的一个
另一方面,str.count
只对字符串中的字符进行计数,并且针对其唯一的任务进行了大量优化
这意味着str.count
可以处理底层的C-char
数组,同时它可以避免在迭代过程中创建新的(或查找现有的)长度为1的python字符串(这是for
和计数器所做的)
只是为了给这句话添加更多的上下文
字符串存储为包装为python对象的C数组。str.count
知道字符串是一个连续数组,因此将要合并的字符转换为C-“字符”,然后在本机C代码中迭代数组并检查是否相等,最后包装并返回找到的出现次数
另一方面,for
和Counter
使用python迭代协议。字符串的每个字符都将被包装为python对象,然后在python中进行(哈希和)比较
因此,经济放缓是因为:
- 每个字符都必须转换为Python对象(这是性能损失的主要原因)
- 循环是用Python完成的(不适用于Python3.x中的
计数器,因为它是用C重写的)
- 每次比较都必须在Python中完成(而不仅仅是在C中比较数字-字符由数字表示)
- 计数器需要散列值,循环需要索引列表
请注意,减速的原因类似于关于的问题
我做了一些额外的基准测试来找出在哪一点集合。计数器比str.count
更受欢迎。为此,我创建了包含不同数量的唯一字符的随机字符串,并绘制了性能图:
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)
使用以下公式绘制结果:
Python 3.5的结果
Python3.6的结果非常相似,所以我没有明确列出它们
因此,如果要计算80个不同的字符,计数器
会变得更快/更具可比性,因为它只遍历字符串一次,而不像str.count那样遍历多次。这将弱依赖于字符串的长度(但测试显示只有非常微弱的差异+/-2%)
Python2.7的结果
在Python-2.7集合中。Counter
是使用Python(而不是C)实现的,速度要慢得多。str.count
和计数器
的盈亏平衡点只能通过外推来估计,因为即使有100个不同的字符,str.count
仍然要快6倍。这里的时差很容易解释。这一切都归结为Python中运行的内容以及作为本机代码运行的内容。后者总是更快,因为它不会带来大量的评估开销
这就是为什么调用str.count()
四次比其他任何调用都快的原因。尽管这会将字符串迭代四次,但这些循环在本机代码中运行str.count
是用C实现的,因此开销很小,速度很快。这真的很难克服,特别是当任务如此简单时(只寻找简单的字符相等)
第二种方法(收集数组中的计数)实际上是以下方法的一个性能较差的版本:
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,即
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
您的遇到了问题
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
%%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)
%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