我想提高Python脚本的性能并使用cProfile
生成性能报告:
python -m cProfile -o chrX.prof ./bgchr.py ...args...
我用Python的chrX.prof
打开了这个pstats
文件并打印出统计信息:
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)
问题:对于join
,split
和write
操作,我可以做些什么来减少它们对此脚本性能的明显影响?
如果相关,则以下是相关脚本的完整源代码:
#!/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))
修改
如果我在sys.stdout.write
的第一个条件中注释掉parseJarchLine()
语句,那么我的运行时间从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
写入stdout
在Python中真的那么贵吗?
答案 0 :(得分:27)
ncalls
仅与将数字与其他计数(例如文件中的字符/字段/行数)进行比较的程度相关,可能会出现高度异常; tottime
和cumtime
才是真正重要的。 cumtime
是在函数/方法中花费的时间,包括在它调用的函数/方法中花费的时间; tottime
是在函数/方法中花费的时间,不包括在它调用的函数/方法中花费的时间。
我发现在tottime
和cumtime
再次对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 > 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
算术的更快执行速度。您还需要考虑在long(x)
已证明x
是long
是浪费资源时执行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
。
这是parseJarchLine函数,其中删除废弃更改标记为[1],更改为int更改标记为[2]。好主意:小步改变,重新测试,重新分析。
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
现在注释掉lastEnd
声明......
顺便说一下,有人在评论中提到有关将其分成不止一个写的内容......你考虑过这个吗?元素[1:]中平均有多少字节?染色体?
===更改主题:我担心您将""
初始化为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)
而不是零,并且没有人对此进行评论。无论如何,你应该解决这个问题,这样可以进行相当大的简化,并添加其他人的建议:
lastEnd
现在我同样担心两个全局变量pLength
和extractData
- 现在parseJarchLine函数很小,可以折回到其唯一调用者的主体{{1} 1}},它保存了两个全局变量,以及大量的函数调用。您还可以通过sys.stdout.write
将write = sys.stdout.write
放在extractData
的前面并使用它来保存{{1}}的大量查找。
BTW,脚本测试Python 2.5或更高版本;你有没有尝试过分析2.5和2.6?
答案 1 :(得分: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)
此代码在组织和性能方面还存在其他几个问题。例如,您可以将变量多次分配给同一个事物,而不是仅创建一次对象实例并对该对象执行所有访问。这样做可以减少分配的数量,也可以减少全局变量的数量。我不想听起来过于批评,但这段代码似乎并没有考虑性能。
答案 2 :(得分:-1)
与可能的优化相关的条目是 ncalls 和 tottime 的值较高的条目。 bgchr:4(<module>)
和<string>:1(<module>)
可能是指您的模块正文的执行,并且与此无关。
显然,您的性能问题来自字符串处理。这或许应该减少。热点是split
,join
和sys.stdout.write
。 bz2.decompress
似乎也很昂贵。
我建议您尝试以下方法:
实际上解压缩数据的循环体似乎只被调用一次。也许你找到了一种方法来避免调用dataHandle.read(size)
,它产生一个巨大的字符串然后被解压缩,并直接使用文件对象。
附录: BZ2File可能不适用于您的情况,因为它需要一个文件名参数。您需要的是具有集成读取限制的文件对象视图,与ZipExtFile相当,但使用BZ2Decompressor进行解压缩。
我的主要观点是,您的代码应该被更改为对数据执行更多迭代处理,而不是整体地将其篡改,然后再将其拆分。