这个cProfile结果告诉我需要修复什么?

16 投票
3 回答
19139 浏览
提问于 2025-04-16 05:15

我想提高一个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真的那么耗性能吗?

3 个回答

-1

要优化的地方主要是那些ncallstottime值很高的条目。像bgchr:4(<module>)<string>:1(<module>)可能是指你模块的执行过程,这里不需要关注。

显然,你的性能问题出在字符串处理上。也许可以减少这部分的工作。主要的瓶颈在于splitjoinsys.stdout.write。另外,bz2.decompress似乎也很耗时。

我建议你尝试以下方法:

  • 你的主要数据看起来是用制表符分隔的CSV值。可以试试用CSV读取器,看看效果是否更好。
  • sys.stdout是行缓冲的,每次写入换行符时都会刷新。可以考虑写入一个缓冲区更大的文件。
  • 在写入输出文件之前,不要把元素连接在一起,而是依次写入。
  • 不要一次性把数据解压成一个字符串,可以使用BZ2File对象,然后把它传给CSV读取器。

看起来实际解压数据的循环体只调用了一次。也许你可以找到一种方法,避免调用dataHandle.read(size),这个调用会生成一个巨大的字符串,然后再解压,而是直接操作文件对象。

补充说明:在你的情况下,BZ2File可能不适用,因为它需要一个文件名参数。你需要的是一种类似文件对象的视图,带有集成的读取限制,类似于ZipExtFile,但使用BZ2Decompressor进行解压。

我想表达的主要观点是,你的代码应该改为更迭代地处理数据,而不是一次性把所有数据读入,然后再分开处理。

2

如果你的代码更模块化,就像Lie Ryan说的那样,这个输出会更有用。不过,从输出结果和源代码中你可以学到一些东西:

在Python中,你做了很多其实不必要的比较。比如,原本你可以这样写:

if len(entryText) > 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)

这段代码在组织和性能方面还有其他几个问题。比如,你多次给变量赋值为同一个东西,而不是只创建一次对象实例,然后对这个对象进行所有的访问。这样做可以减少赋值的次数,也能减少全局变量的数量。我不想显得过于苛刻,但这段代码看起来并没有考虑到性能问题。

30

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 排序统计数据。

在问题修改后更新:

"""问题:我该如何处理 join、split 和 write 操作,以减少它们对这个脚本性能的影响?"""

嗯?这三者加起来占用了总时间 13.8 秒中的 2.6 秒。你的 parseJarchLine 函数占用了 8.5 秒(这不包括它调用的函数或方法所花费的时间)。assert(8.5 > 2.6)

Bernd 已经指出了你可能考虑的解决方案。你不必要完全拆分这一行,然后再在写出时重新组合。你只需要检查第一个元素。可以把 elements = line.split('\t') 改成 elements = line.split('\t', 1),并把 '\t'.join(elements[1:]) 替换为 elements[1]

现在我们来深入分析 parseJarchLine 的主体。long 这个内置函数的使用次数和使用方式令人惊讶。而且在 cProfile 的输出中并没有提到 long

你为什么需要 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 中,这样可以节省两个全局变量和大量的函数调用。你还可以通过在 extractData 的开头写 write = sys.stdout.write 来节省大量对 sys.stdout.write 的查找。

顺便说一下,脚本测试的是 Python 2.5 或更高版本;你尝试过在 2.5 和 2.6 上进行性能分析吗?

撰写回答