【问题标题】:What is this cProfile result telling me I need to fix?这个 cProfile 结果告诉我我需要修复什么?
【发布时间】:2011-04-23 07:59:24
【问题描述】:

我想提高 Python 脚本的性能,并一直在使用 cProfile 生成性能报告:

python -m cProfile -o chrX.prof ./bgchr.py ...args...

我用 Python 的 pstats 打开了这个 chrX.prof 文件并打印了统计数据:

Python 2.7 (r27:82500, Oct  5 2010, 00:24:22) 
[GCC 4.1.2 20080704 (Red Hat 4.1.2-44)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import pstats
>>> p = pstats.Stats('chrX.prof')
>>> p.sort_stats('name')
>>> p.print_stats()                                                                                                                                                                                                                        
Sun Oct 10 00:37:30 2010    chrX.prof                                                                                                                                                                                                      

         8760583 function calls in 13.780 CPU seconds                                                                                                                                                                                      

   Ordered by: function name                                                                                                                                                                                                               

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)                                                                                                                                                                    
        1    0.000    0.000    0.000    0.000 {_locale.setlocale}                                                                                                                                                                          
        1    1.128    1.128    1.128    1.128 {bz2.decompress}                                                                                                                                                                             
        1    0.002    0.002   13.780   13.780 {execfile}                                                                                                                                                                                   
  1750678    0.300    0.000    0.300    0.000 {len}                                                                                                                                                                                        
       48    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}                                                                                                                                                          
        1    0.000    0.000    0.000    0.000 {method 'close' of 'file' objects}                                                                                                                                                           
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}                                                                                                                                             
  1750676    0.496    0.000    0.496    0.000 {method 'join' of 'str' objects}                                                                                                                                                             
        1    0.007    0.007    0.007    0.007 {method 'read' of 'file' objects}                                                                                                                                                            
        1    0.000    0.000    0.000    0.000 {method 'readlines' of 'file' objects}                                                                                                                                                       
        1    0.034    0.034    0.034    0.034 {method 'rstrip' of 'str' objects}                                                                                                                                                           
       23    0.000    0.000    0.000    0.000 {method 'seek' of 'file' objects}                                                                                                                                                            
  1757785    1.230    0.000    1.230    0.000 {method 'split' of 'str' objects}                                                                                                                                                            
        1    0.000    0.000    0.000    0.000 {method 'startswith' of 'str' objects}                                                                                                                                                       
  1750676    0.872    0.000    0.872    0.000 {method 'write' of 'file' objects}                                                                                                                                                           
        1    0.007    0.007   13.778   13.778 ./bgchr:3(<module>)                                                                                                                                                                          
        1    0.000    0.000   13.780   13.780 <string>:1(<module>)                                                                                                                                                                         
        1    0.001    0.001    0.001    0.001 {open}                                                                                                                                                                                       
        1    0.000    0.000    0.000    0.000 {sys.exit}                                                                                                                                                                                   
        1    0.000    0.000    0.000    0.000 ./bgchr:36(checkCommandLineInputs)                                                                                                                                                           
        1    0.000    0.000    0.000    0.000 ./bgchr:27(checkInstallation)                                                                                                                                                                
        1    1.131    1.131   13.701   13.701 ./bgchr:97(extractData)                                                                                                                                                                      
        1    0.003    0.003    0.007    0.007 ./bgchr:55(extractMetadata)                                                                                                                                                                  
        1    0.064    0.064   13.771   13.771 ./bgchr:5(main)                                                                                                                                                                              
  1750677    8.504    0.000   11.196    0.000 ./bgchr:122(parseJarchLine)                                                                                                                                                                  
        1    0.000    0.000    0.000    0.000 ./bgchr:72(parseMetadata)                                                                                                                                                                    
        1    0.000    0.000    0.000    0.000 /home/areynolds/proj/tools/lib/python2.7/locale.py:517(setlocale) 

问题:对于 joinsplitwrite 操作,我可以做些什么来减少它们对该脚本性能的明显影响?

如果相关,这里是相关脚本的完整源代码:

#!/usr/bin/env python

import sys, os, time, bz2, locale

def main(*args):
    # Constants
    global metadataRequiredFileSize
    metadataRequiredFileSize = 8192
    requiredVersion = (2,5)

    # Prep
    global whichChromosome
    whichChromosome = "all"
    checkInstallation(requiredVersion)
    checkCommandLineInputs()
    extractMetadata()
    parseMetadata()
    if whichChromosome == "--list":
        listMetadata()
        sys.exit(0)

    # Extract
    extractData()   
    return 0

def checkInstallation(rv):
    currentVersion = sys.version_info
    if currentVersion[0] == rv[0] and currentVersion[1] >= rv[1]:
        pass
    else:
        sys.stderr.write( "\n\t[%s] - Error: Your Python interpreter must be %d.%d or greater (within major version %d)\n" % (sys.argv[0], rv[0], rv[1], rv[0]) )
        sys.exit(-1)
    return

def checkCommandLineInputs():
    cmdName = sys.argv[0]
    argvLength = len(sys.argv[1:])
    if (argvLength == 0) or (argvLength > 2):
        sys.stderr.write( "\n\t[%s] - Usage: %s [<chromosome> | --list] <bjarch-file>\n\n" % (cmdName, cmdName) )
        sys.exit(-1)
    else:   
        global inFile
        global whichChromosome
        if argvLength == 1:
            inFile = sys.argv[1]
        elif argvLength == 2:
            whichChromosome = sys.argv[1]
            inFile = sys.argv[2]
        if inFile == "-" or inFile == "--list":
            sys.stderr.write( "\n\t[%s] - Usage: %s [<chromosome> | --list] <bjarch-file>\n\n" % (cmdName, cmdName) )
            sys.exit(-1)
    return

def extractMetadata():
    global metadataList
    global dataHandle
    metadataList = []
    dataHandle = open(inFile, 'rb')
    try:
        for data in dataHandle.readlines(metadataRequiredFileSize):     
            metadataLine = data
            metadataLines = metadataLine.split('\n')
            for line in metadataLines:      
                if line:
                    metadataList.append(line)
    except IOError:
        sys.stderr.write( "\n\t[%s] - Error: Could not extract metadata from %s\n\n" % (sys.argv[0], inFile) )
        sys.exit(-1)
    return

def parseMetadata():
    global metadataList
    global metadata
    metadata = []
    if not metadataList: # equivalent to "if len(metadataList) > 0"
        sys.stderr.write( "\n\t[%s] - Error: No metadata in %s\n\n" % (sys.argv[0], inFile) )
        sys.exit(-1)
    for entryText in metadataList:
        if entryText: # equivalent to "if len(entryText) > 0"
            entry = entryText.split('\t')
            filename = entry[0]
            chromosome = entry[0].split('.')[0]
            size = entry[1]
            entryDict = { 'chromosome':chromosome, 'filename':filename, 'size':size }
            metadata.append(entryDict)
    return

def listMetadata():
    for index in metadata:
        chromosome = index['chromosome']
        filename = index['filename']
        size = long(index['size'])
        sys.stdout.write( "%s\t%s\t%ld" % (chromosome, filename, size) )
    return

def extractData():
    global dataHandle
    global pLength
    global lastEnd
    locale.setlocale(locale.LC_ALL, 'POSIX')
    dataHandle.seek(metadataRequiredFileSize, 0) # move cursor past metadata
    for index in metadata:
        chromosome = index['chromosome']
        size = long(index['size'])
        pLength = 0L
        lastEnd = ""
        if whichChromosome == "all" or whichChromosome == index['chromosome']:
            dataStream = dataHandle.read(size)
            uncompressedData = bz2.decompress(dataStream)
            lines = uncompressedData.rstrip().split('\n')
            for line in lines:
                parseJarchLine(chromosome, line)
            if whichChromosome == chromosome:
                break
        else:
            dataHandle.seek(size, 1) # move cursor past chromosome chunk

    dataHandle.close()
    return

def parseJarchLine(chromosome, line):
    global pLength
    global lastEnd
    elements = line.split('\t')
    if len(elements) > 1:
        if lastEnd:
            start = long(lastEnd) + long(elements[0])
            lastEnd = long(start + pLength)
            sys.stdout.write("%s\t%ld\t%ld\t%s\n" % (chromosome, start, lastEnd, '\t'.join(elements[1:])))
        else:
            lastEnd = long(elements[0]) + long(pLength)
            sys.stdout.write("%s\t%ld\t%ld\t%s\n" % (chromosome, long(elements[0]), lastEnd, '\t'.join(elements[1:])))
    else:
        if elements[0].startswith('p'):
            pLength = long(elements[0][1:])
        else:
            start = long(long(lastEnd) + long(elements[0]))
            lastEnd = long(start + pLength)
            sys.stdout.write("%s\t%ld\t%ld\n" % (chromosome, start, lastEnd))               
    return

if __name__ == '__main__':
    sys.exit(main(*sys.argv))

编辑

如果我在parseJarchLine() 的第一个条件中注释掉sys.stdout.write 语句,那么我的运行时间将从10.2 秒变为4.8 秒:

# with first conditional's "sys.stdout.write" enabled
$ time ./bgchr chrX test.bjarch > /dev/null
real    0m10.186s                                                                                                                                                                                        
user    0m9.917s                                                                                                                                                                                         
sys 0m0.160s  

# after first conditional's "sys.stdout.write" is commented out                                                                                                                                                                                           
$ time ./bgchr chrX test.bjarch > /dev/null
real    0m4.808s                                                                                                                                                                                         
user    0m4.561s                                                                                                                                                                                         
sys 0m0.156s

用 Python 写信给stdout 真的那么贵吗?

【问题讨论】:

  • 将代码分解为小函数。 Python 的 cProfile 对于编写为一大块的代码几乎毫无用处,因为它是一个函数级分析器,而不是逐行分析器。同时,如果将所有内容都放在 main() 函数中,则速度会有所提高,因为在 Python 中访问全局变量比访问局部变量要慢。
  • @Lie Ryan:看看数字!这些足够详细,可以显示需要优化的地方。访问全局变量这里不相关,bgchr:4()和:1()的时间对应总执行时间。
  • @Bernd Petersohn:请考虑您弄错的可能性。看我的回答。
  • 我已经替换了源代码和生成的 cProfile 分析结果。如果您有时间看一看并提供建议,我将不胜感激。谢谢。
  • @Alex Reynolds:在parseJarchLine() 中,仅在第一个选项卡处拆分行就足够了:elements = line.split('\t', 1)。这将使以下联接过时:将表达式 '\t'.join(elements[1:] 替换为 elements[1]。此外,如果第二部分可能变得很大,如果不将其集成到格式字符串中,性能可能会进一步提高。相反,使用对sys.stdout.write 的单独调用来输出第一部分、第二部分和最后的换行符。

标签: python performance profiling profile cprofile


【解决方案1】:

与可能的优化相关的条目是那些 ncallstottime 值较高的条目。 bgchr:4(&lt;module&gt;)&lt;string&gt;:1(&lt;module&gt;) 可能是指你的模块体的执行,这里不相关。

显然,您的性能问题来自字符串处理。这也许应该减少。热点是splitjoinsys.stdout.writebz2.decompress 似乎也很昂贵。

我建议您尝试以下方法:

  • 您的主要数据似乎由制表符分隔的 CSV 值组成。试试看,如果 CSV 阅读器性能更好。
  • sys.stdout 在每次写入换行符时都会进行行缓冲和刷新。考虑写入具有较大缓冲区大小的文件。
  • 不要在写出元素之前加入元素,而是将它们按顺序写入输出文件。您也可以考虑使用 CSV 编写器。
  • 不要立即将数据解压缩为单个字符串,而是使用 BZ2File 对象并将其传递给 CSV 阅读器。

实际上解压缩数据的循环体似乎只调用了一次。也许你找到了一种方法来避免调用dataHandle.read(size),它会产生一个巨大的字符串,然后解压缩,并直接使用文件对象。

附录: BZ2File 可能不适用于您的情况,因为它需要文件名参数。您需要的是具有集成读取限制的文件对象视图,与 ZipExtFile 相当,但使用 BZ2Decompressor 进行解压缩。

我的主要观点是,应该更改您的代码以对数据执行更多迭代处理,而不是将其作为一个整体并在之后再次拆分。

【讨论】:

    【解决方案2】:

    如果您的代码像 Lie Ryan 所说的那样更加模块化,那么这个输出将会更加有用。但是,您可以从输出中获取一些内容并查看源代码:

    您正在做很多在 Python 中实际上不需要的比较。例如,而不是:

    if len(entryText) &gt; 0:

    你可以写:

    if entryText:

    空列表在 Python 中的计算结果为 False。空字符串也是如此,您也可以在代码中对其进行测试,并且更改它也会使代码更短且更具可读性,所以不要这样:

       for line in metadataLines:      
            if line == '':
                break
            else:
                metadataList.append(line)
    

    你可以这样做:

    for line in metadataLines:
        if line:
           metadataList.append(line)
    

    此代码在组织和性能方面还有其他几个问题。例如,您将变量多次分配给同一事物,而不是仅创建一次对象实例并对该对象进行所有访问。这样做会减少分配的数量,以及全局变量的数量。我不想听起来过于挑剔,但这段代码似乎没有考虑到性能。

    【讨论】:

    • 我已经替换了源代码和生成的 cProfile 分析结果。如果您有时间看一看并提供建议,我将不胜感激。谢谢。
    • 首先,我认为各种人在这里提出的建议通常都是很好的建议。其次,我仍然认为全局变量的广泛使用将对解析大量数据的东西产生影响——将适当的函数放入一个类中,这几乎完全消失了。此外,将事物包装在一个类中可以更容易地尝试诸如线程和多处理之类的解决方案(在这种情况下,我更喜欢多处理而不是线程,fwiw)。显示示例输入可能会让您获得更多反馈。
    • if line == '': break; else: metadataList.append(line)if line: metadataList.append(line) 不同
    【解决方案3】:

    ncalls 仅在将数字与其他计数(例如文件中的字符/字段/行数)进行比较时可能会突出异常; tottimecumtime 才是真正重要的。 cumtime 是函数/方法花费的时间包括它调用的函数/方法花费的时间; tottime 是函数/方法花费的时间不包括它调用的函数/方法花费的时间。

    我发现对tottimecumtime 上的统计数据进行排序很有帮助,而不是name

    bgchar肯定指的是脚本的执行,并不是无关紧要的,因为它占用了 13.5 中的 8.9 秒; 8.9 秒不包括它调用的函数/方法中的时间!仔细阅读@Lie Ryan 所说的关于将脚本模块化为函数的内容,并实施他的建议。就像@jonesy 所说的一样。

    string 被提及是因为你 import string 并且只在一个地方使用它:string.find(elements[0], 'p')。在输出的另一行,您会注意到 string.find 只调用了一次,因此在此脚本的运行中这不是性能问题。但是:您在其他任何地方都使用str 方法。 string 函数现在已被弃用,并通过调用相应的 str 方法来实现。您最好写 elements[0].find('p') == 0 以获得精确但更快的等价物,并且可能喜欢使用 elements[0].startswith('p') 这将让读者不知道 == 0 是否实际上应该是 == -1

    @Bernd Petersohn 提到的四种方法只占用了 13.541 秒的总执行时间中的 3.7 秒。在过多担心这些之前,请将您的脚本模块化为函数,再次运行 cProfile,然后按 tottime 对统计信息进行排序。

    使用更改的脚本修改问题后更新:

    """问题:关于加入、拆分和写入操作,我可以做些什么来减少它们对该脚本性能的明显影响?""

    嗯?这 3 个加起来总共需要 13.8 秒中的 2.6 秒。您的 parseJarchLine 函数需要 8.5 秒(其中不包括它调用的函数/方法所花费的时间。assert(8.5 &gt; 2.6)

    Bernd 已经向您指出了您可能会考虑如何处理这些内容。您不必要地完全拆分该行,只是在写出时再次将其连接起来。您只需要检查第一个元素。代替 elements = line.split('\t') 执行 elements = line.split('\t', 1) 并将 '\t'.join(elements[1:]) 替换为 elements[1]

    现在让我们深入了解 parseJarchLine 的主体。 long 内置函数的来源和使用方式的使用次数是惊人的。同样令人惊讶的是,long 没有在 cProfile 输出中提及。

    你为什么需要long?超过 2 Gb 的文件?好的,那么您需要考虑,由于 Python 2.2,int 溢出会导致提升到 long 而不是引发异常。您可以利用int 算术的更快执行。您还需要考虑,当x 已经被证明是long 时执行long(x) 是一种资源浪费。

    这是 parseJarchLine 函数,其中删除浪费更改标记为 [1],更改为 int 更改标记为 [2]。好主意:小步进行更改,重新测试,重新配置。

    def parseJarchLine(chromosome, line):
        global pLength
        global lastEnd
        elements = line.split('\t')
        if len(elements) > 1:
            if lastEnd != "":
                start = long(lastEnd) + long(elements[0])
                # [1] start = lastEnd + long(elements[0])
                # [2] start = lastEnd + int(elements[0])
                lastEnd = long(start + pLength)
                # [1] lastEnd = start + pLength
                sys.stdout.write("%s\t%ld\t%ld\t%s\n" % (chromosome, start, lastEnd, '\t'.join(elements[1:])))
            else:
                lastEnd = long(elements[0]) + long(pLength)
                # [1] lastEnd = long(elements[0]) + pLength
                # [2] lastEnd = int(elements[0]) + pLength
                sys.stdout.write("%s\t%ld\t%ld\t%s\n" % (chromosome, long(elements[0]), lastEnd, '\t'.join(elements[1:])))
        else:
            if elements[0].startswith('p'):
                pLength = long(elements[0][1:])
                # [2] pLength = int(elements[0][1:])
            else:
                start = long(long(lastEnd) + long(elements[0]))
                # [1] start = lastEnd + long(elements[0])
                # [2] start = lastEnd + int(elements[0])
                lastEnd = long(start + pLength)
                # [1] lastEnd = start + pLength
                sys.stdout.write("%s\t%ld\t%ld\n" % (chromosome, start, lastEnd))               
        return
    

    关于sys.stdout.write的问题后更新

    如果您注释掉的语句与原来的语句相似:

    sys.stdout.write("%s\t%ld\t%ld\t%s\n" % (chromosome, start, lastEnd, '\t'.join(elements[1:])))
    

    那么你的问题……很有趣。试试这个:

    payload = "%s\t%ld\t%ld\t%s\n" % (chromosome, start, lastEnd, '\t'.join(elements[1:]))
    sys.stdout.write(payload)
    

    现在注释掉sys.stdout.write 声明...

    顺便说一句,有人在评论中提到将其分成多个写作……您考虑过吗? elements[1:] 中平均有多少字节?在染色体中?

    === 主题更改:我担心您将lastEnd 初始化为"" 而不是为零,并且没有人对此发表评论。无论如何,您应该解决这个问题,这样可以进行相当大的简化并添加其他人的建议:

    def parseJarchLine(chromosome, line):
        global pLength
        global lastEnd
        elements = line.split('\t', 1)
        if elements[0][0] == 'p':
            pLength = int(elements[0][1:])
            return
        start = lastEnd + int(elements[0])
        lastEnd = start + pLength
        sys.stdout.write("%s\t%ld\t%ld" % (chromosome, start, lastEnd))
        if elements[1:]:
            sys.stdout.write(elements[1])
        sys.stdout.write(\n)
    

    现在我同样担心lastEndpLength 这两个全局变量—— parseJarchLine 函数现在非常小,可以折叠回其唯一调用者extractData 的主体中,这样可以节省两个全局变量,以及无数个函数调用。您还可以通过将write = sys.stdout.write 放在extractData 前面并改用它来节省大量的sys.stdout.write 查找。

    顺便说一句,脚本测试 Python 2.5 或更高版本;您是否尝试过在 2.5 和 2.6 上进行分析?

    【讨论】:

    • 我同意对数字的解释。但我也查看了代码:主要工作显然是在最后一个标记为“提取数据”的部分中完成的。我看到有很多必须是大量数据的字符串创建。这就是我认为需要优化的地方。如果将该代码放入函数中,您可能不会获得更多信息量。是的,我个人会以不同的方式构造这段代码。
    • 我已经替换了源代码和生成的 cProfile 分析结果。如果您有时间看一看并提供建议,我将不胜感激。谢谢。
    • 感谢您的有益建议。我不是 Python 专家,也不知道 int 类型会根据其他语言的工作自动提升为 long。但是,您建议的更改似乎只缩短了 0.8 秒。我的脚本所需的时间仍然是csh/awk 解决方案的两倍(不允许使用基于seek 的随机访问,如Python,应该更慢)。除非有其他特定于 Python 语言的优化和技巧(仍然允许写入标准输出),否则我想我现在可能不得不研究基于 C 的解决方案。
    • 我开始在 Python 2.5.2 下运行它,它同样慢 - 我去 Python 2.7 尝试获得任何速度增强。在过去的几天里,我一直在编写这个脚本的基于 C 的等价物,结果是闪电般的快,相比之下,尽管存在一些 bzip 困难,但我仍在努力。稍后我可能会发布代码以进行比较。
    • @Alex Reynolds:你测试过 John Machin 的最后一个建议吗?我认为您的脚本的一个主要问题是它维护了不必要的大内存占用。在extractData() 中,您首先读取一条染色体的未压缩数据块。然后创建第二个解压缩数据字符串。它包含大约 175 万行。在这里很容易达到 1 GB。然后创建一个包含这 175 万行的列表。最好将未压缩的数据包装在 StringIO 中并遍历其行。您还应该使用del 提前释放压缩数据。
    猜你喜欢
    • 1970-01-01
    • 2021-11-09
    • 1970-01-01
    • 1970-01-01
    • 2015-08-29
    • 2012-12-22
    • 2021-08-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多