Was ist das cProfile Ergebnis mir zu sagen, ich repariert werden muss?
-
29-09-2019 - |
Frage
Ich möchte die Leistung eines Python-Skript verbessern und wurden mit cProfile
einen Leistungsbericht zu generieren:
python -m cProfile -o chrX.prof ./bgchr.py ...args...
Ich öffnete diese chrX.prof
Datei mit Python pstats
und ausgedruckte Statistik:
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)
Frage: Was kann ich über join
, split
und write
Operationen die offensichtliche Auswirkungen reduzieren sie auf die Leistung dieses Skript
Wenn es relevant ist, ist hier die vollständige Quellcode für das Skript in Frage:
#!/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))
Bearbeiten
Wenn ich die sys.stdout.write
Anweisung in dem ersten bedingten von parseJarchLine()
kommentieren, dann meiner Laufzeit geht von 10,2 sec bis 4,8 sec:
# 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
Ist Schreiben wirklich stdout
so teuer in Python?
Lösung
ncalls
ist nur relevant, in dem Maße, dass die Zahlen gegen andere zählt wie die Anzahl der Zeichen / Felder / Zeilen in einer Datei zu vergleichen können Anomalien Highligh; tottime
und cumtime
ist das, was wirklich zählt. cumtime
ist die Zeit, in der Funktion / Methode verbrachte mit die verbrachte Zeit in den Funktionen / Methoden, die es nennt; tottime
ist die Zeit, in der Funktion / Methode ausgegeben ohne die verbrachte Zeit in den Funktionen / Methoden, die es nennt.
Ich finde es hilfreich, die Statistiken über tottime
zu sortieren und wieder auf cumtime
, nicht auf name
.
bgchar
auf jeden Fall bezieht sich auf die Ausführung des Skripts und ist nicht unerheblich, da es 8,9 Sekunden aus 13,5 aufgreift; dass 8,9 Sekunden Zeit in den Funktionen / Methoden beinhalten NICHT, dass es nennt! Lesen Sie sorgfältig, was @Lie Ryan sagt über Ihr Skript in Funktionen Modularisierung und seine Ratschläge umzusetzen. Ebenso was @jonesy sagt.
string
wird erwähnt, weil Sie import string
und verwenden Sie es in nur einem Ort: string.find(elements[0], 'p')
. Bei einer anderen Zeile in der Ausgabe werden Sie feststellen, dass string.find nur einmal aufgerufen wurde, so dass es in diesem Laufe dieses Skripts keine Performance-Problem ist. ABER: Sie verwenden überall sonst str
Methoden. string
Funktionen werden heute als veraltet und werden durch Aufruf der entsprechenden str
Methode implementiert. Sie wäre besser elements[0].find('p') == 0
für eine exakte, aber schneller äquivalent zu schreiben, und könnte zu verwenden elements[0].startswith('p')
mag die Leser sparen würde fragen, ob das == 0
tatsächlich == -1
sein sollte.
Die vier genannten Methoden von @Bernd Peter nehmen nur 3,7 Sekunden bei einer Gesamtausführungszeit von 13,541 Sekunden. Vor zu viel über diejenigen Sorgen, modularisieren Ihr Skript in Funktionen, laufen cProfile wieder, und sortieren die Statistiken von tottime
.
Update nach Frage mit geändertenen Skript überarbeitet:
„“ „Frage: Was kann ich tun kommen über, Split und Schreiboperationen die offensichtlichen Auswirkungen sie auf der Leistung dieses Skripts zu reduzieren“ "
Hä? Diejenigen 3 nehmen zusammen 2,6 Sekunden aus dem insgesamt 13,8. Ihre parseJarchLine Funktion nimmt 8,5 Sekunden (die Zeit nicht durch Funktionen / Methoden genommen nicht enthalten, dass es nennt. assert(8.5 > 2.6)
Bernd hat bereits darauf Sie an, was Sie sich anschauen sollten mit denen zu tun. Sie spalten sich unnötig die Linie vollständig nur um es wieder zu kommen, wenn es auszuschreiben. Sie müssen nur das erste Element prüfen. Statt elements = line.split('\t')
tun elements = line.split('\t', 1)
und ersetzen '\t'.join(elements[1:])
durch elements[1]
.
Nun des Tauchgangs in den Körper parseJarchLine lassen. Die Anzahl der Verwendungen in der Quelle und Weise der Nutzung der long
eingebaute Funktion erstaunlich sind. Auch erstaunlicher ist die Tatsache, dass long
nicht im cProfile Ausgabe erwähnt.
Warum brauchen Sie überhaupt long
? Dateien über 2 GB? OK, dann müssen Sie bedenken, dass seit Python 2.2, int
Überlauf Promotion long
verursacht stattdessen eine Ausnahme zu heben. Sie können die Vorteile der schnelleren Ausführung von arithmetischen int
nehmen. Sie müssen auch bedenken, dass long(x)
tun, wenn x
ist bereits nachweislich ein long
ist eine Verschwendung von Ressourcen.
Hier ist die parseJarchLine Funktion mit dem Entfernen von Abfall deutlichen Veränderungen [1] und Wechsel-to-int ändert gekennzeichnet [2]. Gute Idee: Bearbeite in kleinen Schritten, Re-Test, Re-Profil
.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
Update nach Frage zu sys.stdout.write
Wenn die Aussage, dass Sie kommentierte, war so etwas wie das Original:
sys.stdout.write("%s\t%ld\t%ld\t%s\n" % (chromosome, start, lastEnd, '\t'.join(elements[1:])))
Dann ist Ihre Frage ... interessant. Versuchenfolgt aus:
payload = "%s\t%ld\t%ld\t%s\n" % (chromosome, start, lastEnd, '\t'.join(elements[1:]))
sys.stdout.write(payload)
Jetzt Kommentar aus der sys.stdout.write
Aussage ...
Durch die Art und Weise, jemand in einem Kommentar erwähnt über das Einbrechen in mehr als ein Schreib ... haben Sie dies in Betracht gezogen? Wie viele Bytes im Durchschnitt in den Elementen [1:]? In Chromosom?
=== Themenwechsel: Es macht mir Sorgen, dass Sie lastEnd
zu ""
initialisieren, anstatt auf Null, und dass niemand hat es kommentiert. Jede Art und Weise, sollten Sie dieses Problem beheben, die eine ziemlich drastische Vereinfachung ermöglicht sowie das Hinzufügen in anderen Vorschlägen:
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)
Jetzt bin ich besorgt in ähnlicher Weise über die beiden globalen Variablen lastEnd
und pLength
- die parseJarchLine Funktion ist jetzt so klein, dass es wieder in den Körper ihres einzigen Anrufer, extractData
gefaltet werden kann, die zwei globalen Variablen speichert, und eine gazillion Funktion aufruft. Sie könnten auch speichern Sie eine Unmenge Lookups von sys.stdout.write
von write = sys.stdout.write
einmal auf der Vorderseite extractData
setzen und die Verwendung dieser statt.
BTW, die Skript-Tests für Python 2.5 oder besser; Haben Sie versucht, Profilieren auf 2.5 und 2.6?
Andere Tipps
Diese Ausgabe wird nützlicher sein, wenn Ihr Code modular aufgebaut ist, wie Lie Ryan erklärt hat. Doch ein paar Dinge, die Sie aus der Ausgabe abholen und nur auf dem Quellcode suchen:
Sie tun eine Menge Vergleiche, die nicht wirklich notwendig, in Python sind. Zum Beispiel statt:
if len(entryText) > 0:
Sie können einfach schreiben:
if entryText:
Eine leere Liste auswertet auf False in Python. Gleiches gilt für eine leere Zeichenfolge, die Sie auch Test für in Ihrem Code und das Ändern es auch der Code eine kürzere Bit machen würde und besser lesbar, so dass anstelle dieses:
for line in metadataLines:
if line == '':
break
else:
metadataList.append(line)
Sie können einfach tun:
for line in metadataLines:
if line:
metadataList.append(line)
Es gibt einige andere Probleme mit diesem Code sowohl in Bezug auf Organisation und Leistung. Sie ordnen Variablen mehrmals auf die gleiche Sache, anstatt nur einmal eine Objektinstanz zu schaffen und alle Zugriffe auf das Objekt zu tun, zum Beispiel. Dadurch würde die Anzahl der Aufgaben, und auch die Anzahl der globalen Variablen reduzieren. Ich will nicht übermäßig kritisch klingen, aber dieser Code scheint nicht mit Leistung im Verstand geschrieben werden.
Die Einträge relevant für mögliche Optimierung sind solche mit hohen Werten für ncalls und tottime . bgchr:4(<module>)
und <string>:1(<module>)
wahrscheinlich auf die Ausführung Ihres Modulkörper beziehen und sind hier nicht relevant.
Offensichtlich Ihr Leistungsproblem kommt aus String-Verarbeitung. Dies sollte vielleicht reduziert werden. Die Hot Spots sind split
, join
und sys.stdout.write
. bz2.decompress
scheint auch teuer zu sein.
Ich schlage vor, Sie versuchen, die folgenden:
- Ihre Hauptdaten scheint von Tabulatoren getrennte CSV-Werte zu bestehen. Testen Sie, wenn CSV-Leser eine bessere Leistung.
- sys.stdout wird Zeile gepuffert und gespült jedes Mal, wenn ein Newline geschrieben. Betrachten Sie das Schreiben in eine Datei mit einer größeren Puffergröße.
- Anstelle von Verbindungselementen, bevor sie sie schreibt, schreibt sie nacheinander in die Ausgabedatei. Sie können sich auch CSV-Writer.
- Anstatt die Daten sofort in einem einzigen String Dekomprimieren, verwenden Sie ein BZ2File Objekt und übergeben, daß in der CSV-Leser.
Es scheint, dass der Schleifenkörper, die Daten tatsächlich dekomprimiert nur einmal aufgerufen wird. Vielleicht finden Sie einen Weg, um den Anruf dataHandle.read(size)
zu vermeiden, die eine große Zeichenfolge erzeugt, die dann dekomprimiert wird, und die Arbeit mit dem Dateiobjekt direkt an.
Nachtrag: BZ2File ist wahrscheinlich in Ihrem Fall nicht anwendbar, weil es einen Dateinamen als Argument benötigt. Was Sie brauchen, ist so etwas wie ein Dateiobjekt Ansicht mit integrierten Lesegrenze, vergleichbar mit ZipExtFile aber mit BZ2Decompressor für die Dekompression.
Meine hier Hauptsache ist, dass der Code geändert werden soll, um eine iterative Verarbeitung Ihrer Daten durchzuführen, statt sie in als Ganze von Schlürfen und Spaltung wieder danach.