当前位置: 首页 > 知识库问答 >
问题:

为什么收款台这么慢?

钱经赋
2023-03-14

我试图解决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的处理方法是什么?


  • 共有3个答案

    宗政颖逸
    2023-03-14

    正如其他人已经指出的,您正在比较相当具体的代码和相当一般的代码。

    考虑到在你感兴趣的字符上拼写循环的琐碎的事情已经为你买了一个因子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
    

    您的method 1()是最快的,但不是最有效的,因为输入完全是针对您正在检查的每个字符循环的,因此不会利用这样一个事实,即一旦分配了字符,您可以轻松地短路循环到其中一个角色类。

    不幸的是,Python没有提供一个快速的方法来利用你的问题的特定条件。然而,您可以使用Cython来实现这一点,然后您将能够超越您的method 1()

    %%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
    
    皇甫树
    2023-03-14

    这里的时差很容易解释。这一切都归结为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中迭代字符串来工作的每个解决方案都会变慢的主要原因。

    集合也存在同样的问题。计数器.它是在Python中实现的,所以即使它非常高效和灵活,它也面临着同样的问题,那就是它在速度方面从来都不接近本机。

    龚宏壮
    2023-03-14

    这并不是因为collections.Counter速度慢,实际上相当快,但它是一个通用工具,计数字符只是许多应用程序中的一个。

    另一方面,str.count只对字符串中的字符进行计数,并且针对其唯一的任务进行了大量优化。

    这意味着str.count可以处理底层的C-char数组,同时它可以避免在迭代过程中创建新的(或查找现有的)长度为1的python字符串(这是for计数器所做的)。

    只是为了给这句话增加一些背景。

    字符串存储为C数组,包装为python对象。str.count知道字符串是一个连续数组,因此将您想要的字符转换为C-字符,然后在本机C代码中遍历数组并检查是否相等,最后换行并返回发现的出现次数。

    另一方面,forCounter使用python迭代协议。字符串的每个字符都将被包装为python对象,然后在python中进行(哈希和)比较。

    因此,经济放缓是因为:

    • 每个字符都必须转换为Python对象(这是性能损失的主要原因)
    • 循环是用Python完成的(不适用于Python3.x中的计数器,因为它是用C重写的)
    • 每次比较都必须在Python中完成(而不仅仅是在C中比较数字-字符由数字表示)
    • 计数器需要散列值,循环需要索引列表

    请注意,速度减慢的原因类似于关于Python的数组为什么速度慢的问题?。

    我做了一些额外的基准测试来找出在哪个点上集合。计数器优于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)
    

    使用matplotlib绘制结果:

    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()
    

    Python3.6的结果非常相似,所以我没有明确列出它们。

    因此,如果要计算80个不同的字符,计数器会变得更快/更具可比性,因为它只遍历字符串一次,而不像str.count那样遍历多次。这将弱依赖于字符串的长度(但测试显示只有非常微弱的差异/-2%)。

    在Python-2.7集合中。Counter是使用Python(而不是C)实现的,速度要慢得多。str.countCounter的盈亏平衡点只能通过外推来估计,因为即使有100个不同的字符,str.count仍然快6倍。

     类似资料:
    • 问题内容: 这是所有编程语言所共有的吗?在进行多次打印后再执行println似乎更快,但是将所有内容移动到字符串中并仅进行打印似乎最快。为什么? 编辑:例如,Java可以在不到一秒钟的时间内找到所有高达100万的质数- 但要进行打印,然后在自己的println中将它们全部输出可能需要几分钟!最多可打印100亿小时! 例如: 问题答案: 速度并不慢,而是由主机操作系统提供的与控制台连接的基础。 您可

    • 问题内容: 我对此感到困惑 现在让我们来看看numpy: 神圣的CPU周期蝙蝠侠! 使用改进,但恕我直言仍然不够 numpy.version.version =‘1.5.1’ 如果您想知道在第一个示例中是否跳过了列表创建以进行优化,则不是: 问题答案: Numpy已针对大量数据进行了优化。给它一个很小的3长度数组,毫不奇怪,它的性能很差。 考虑单独的测试 输出是 似乎是数组的归零一直花费在nump

    • 问题内容: Magento通常这么慢吗? 这是我的第一次使用体验,管理面板只需花一些时间即可加载和保存更改。这是带有测试数据的默认安装。 托管该服务器的服务器可超快地服务于其他非Magento站点。Magento使它如此缓慢的PHP代码有什么用,该如何解决? 问题答案: 我只是切身参与优化Magento的性能,但这是系统速度如此缓慢的一些原因 Magento的某些部分使用在MySQL之上实现的EA

    • 问题内容: 在有人质疑使用的事实之前,我先说一下,出于内存和性能的原因,我需要在特定的应用程序中使用它。[1] 因此,到目前为止,我一直使用并假定这是最有效的方法。但是,自古以来我就注意到它是软件的瓶颈。[2] 然后,就在最近,我试图用一个巨大的映射替换,在该映射中放置/获取字符串,以便每次获得唯一的实例。我以为这会慢一些…但是事实恰恰相反!它快得多了!通过推送/轮询地图(实现完全相同)来替换,可

    • 问题内容: IDLE是我最喜欢的Python编辑器。它提供了非常漂亮和直观的Python shell,它对单元测试和调试非常有用,并且还提供了一个简洁的调试器。 但是,在IDLE下执行的代码异常缓慢。 疯狂是指慢 三个数量级 : 重击 需要0.052秒, 闲 需要: 大约慢了2000倍。 有什么想法或想法可以改善这一点吗?我想这与后台调试器有关,但是我不确定。 亚当 问题答案: 问题是文本输出而不

    • ChatGPT为什么这么火?