Python 为什么收款台这么慢?

Python 为什么收款台这么慢?,python,performance,collections,counter,bioinformatics,matplotlib,Python,Performance,Collections,Counter,Bioinformatics,Matplotlib,我试图解决Rosalind的一个基本问题,计算给定序列中的核苷酸,并返回列表中的结果。对于那些不熟悉生物信息学的人来说,这只是计算字符串中4个不同字符('A'、'C'、'G'、'T')的出现次数 我认为collections.Counter是最快的方法(首先是因为它们声称具有高性能,其次是因为我看到很多人使用它来解决这个特定问题) 但令我惊讶的是,这种方法是最慢的 我比较了三种不同的方法,使用timeit并运行两种类型的实验: 运行长序列几次 多次运行短序列 这是我的密码: import t

我试图解决Rosalind的一个基本问题,计算给定序列中的核苷酸,并返回列表中的结果。对于那些不熟悉生物信息学的人来说,这只是计算字符串中4个不同字符('A'、'C'、'G'、'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