Che cosa è questo risultato Cprofile mi dice che ho bisogno di correzione?
-
29-09-2019 - |
Domanda
Vorrei migliorare le prestazioni di uno script Python e sono state usando cProfile
per generare un report di prestazioni:
python -m cProfile -o chrX.prof ./bgchr.py ...args...
Ho aperto questo file chrX.prof
con pstats
di Python e stampato le statistiche:
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)
Domanda: Cosa posso fare per operazioni join
, split
e write
per ridurre l'impatto apparente che hanno sulle prestazioni di questo script
Se è rilevante, ecco il codice sorgente completo per lo script in questione:
#!/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))
Modifica
Se io commento le istruzioni sys.stdout.write
nel primo condizionale di parseJarchLine()
, poi il mio tempo di esecuzione va dal 10,2 sec a 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
sta scrivendo stdout
davvero così costoso in Python?
Soluzione
ncalls
è rilevante solo nella misura in cui si confrontano i numeri contro altri conta come il numero di caratteri / campi / righe in un file può highligh anomalie; tottime
e cumtime
è ciò che conta davvero. cumtime
è il tempo trascorso nel / metodo funzione compreso il tempo trascorso in funzioni / metodi che chiama; tottime
è il tempo trascorso nella funzione / metodo di escluso il tempo trascorso nelle funzioni / metodi che chiama.
Trovo utile per ordinare le statistiche su tottime
e di nuovo su cumtime
, non su name
.
bgchar
sicuramente si riferisce all'esecuzione dello script e non è irrilevante come esso prende 8,9 secondi su 13.5; che 8,9 secondi non include il tempo nelle funzioni / metodi che chiama! Leggi con attenzione quello che dice @Lie Ryan sulla modularizzazione lo script in funzioni, e mettere in atto il suo consiglio. Allo stesso modo ciò che @jonesy dice.
string
è menzionato perché si import string
e utilizzarlo in un solo posto: string.find(elements[0], 'p')
. Su un'altra linea nell'output si noterà che string.find è stato chiamato solo una volta, quindi non è un problema di prestazioni in questo percorso di questo script. TUTTAVIA: si utilizzano i metodi str
ovunque. funzioni string
sono obsoleti e oggi sono implementati chiamando il metodo str
corrispondente. Si sarebbe meglio scrivere elements[0].find('p') == 0
per un esatto equivalente, ma più veloce, e avrebbe fatto piacere l'uso elements[0].startswith('p')
che permetterebbe di risparmiare i lettori chiedendo se che == 0
dovrebbe essere effettivamente == -1
.
I quattro metodi menzionati da @Bernd Petersohn occupano solo 3,7 secondi su un tempo di esecuzione totale di 13.541 secondi. Prima di preoccuparsi troppo quelli, modularizzare lo script in funzioni, eseguire nuovamente Cprofile, e ordinare le statistiche di tottime
.
Aggiornamento su domande rivisto con lo script modificato:
"" "Domanda: Cosa posso fare per unire, dividere le operazioni di scrittura e per ridurre l'impatto apparente che hanno sulle prestazioni di questo script" "
Eh? Coloro 3 complessivamente in 2,6 secondi su un totale di 13,8. La funzione parseJarchLine sta prendendo 8,5 secondi (che non comprende tempo impiegato da funzioni / metodi che chiama. assert(8.5 > 2.6)
Bernd ha già puntato a quello che si potrebbe considerare di fare con quelli. Si sono inutilmente dividendo la linea completamente unica per unire di nuovo durante la scrittura fuori. È necessario esaminare solo il primo elemento. Invece di fare elements = line.split('\t')
elements = line.split('\t', 1)
e sostituire '\t'.join(elements[1:])
da elements[1]
.
Ora diamo tuffo nel corpo di parseJarchLine. Il numero di impieghi nella sorgente e maniera degli usi della long
funzione incorporata sono sorprendenti. Anche sorprendente è il fatto che non è menzionato long
nell'output Cprofile.
Perché avete bisogno di long
a tutti? File di oltre 2 GB? OK, allora è necessario considerare che dal Python 2.2, int
overflow causa la promozione a long
invece di sollevare un'eccezione. È possibile usufruire di esecuzione più veloce di int
aritmetica. È inoltre necessario considerare che facendo long(x)
quando x
è già palesemente un long
è uno spreco di risorse.
Questa è la funzione parseJarchLine con la rimozione dei rifiuti variazione marcata [1] e cambiando-to-int notevoli cambiamenti [2]. Buona idea: le modifiche apportate in piccoli passi, re-test, ri-profilo
.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
Aggiorna dopo domanda su sys.stdout.write
Se la dichiarazione che si ha commentato fuori era qualcosa di simile a quello originale:
sys.stdout.write("%s\t%ld\t%ld\t%s\n" % (chromosome, start, lastEnd, '\t'.join(elements[1:])))
Quindi la tua domanda è ... interessante. Provarein questo modo:
payload = "%s\t%ld\t%ld\t%s\n" % (chromosome, start, lastEnd, '\t'.join(elements[1:]))
sys.stdout.write(payload)
Ora come commento le istruzioni sys.stdout.write
...
A proposito, qualcuno ha menzionato in un commento di rompere questo in più di una scrittura ... avete considerato questo? Il numero di byte, in media, negli elementi [1:]? In cromosoma?
=== cambio di argomento: Mi preoccupa che si inizializza lastEnd
a ""
piuttosto che a zero, e che nessuno ha commentato su di esso. In qualsiasi modo, si dovrebbe risolvere questo problema, che permette una semplificazione piuttosto drastica, più l'aggiunta di suggestioni degli altri:
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)
Ora sto allo stesso preoccupato per i due variabili globali lastEnd
e pLength
- la funzione parseJarchLine ora è così piccolo che può essere ripiegato nel corpo del suo chiamante suola, extractData
, che consente di risparmiare due variabili globali, e un funzione gazillion chiama. Si potrebbe anche risparmiare un gazillion le ricerche di sys.stdout.write
mettendo write = sys.stdout.write
una volta la parte anteriore del extractData
e l'utilizzo che, invece.
A proposito, i test di script per Python 2.5 o superiore; Hai provato profiling su 2.5 e 2.6?
Altri suggerimenti
Questa uscita sarà più utile se il codice è più modulare come Lie Ryan ha dichiarato. Tuttavia, un paio di cose che si può prendere dall'uscita e solo guardando il codice sorgente:
Si sta facendo un sacco di paragoni che non sono effettivamente necessarie in Python. Ad esempio, invece di:
if len(entryText) > 0:
Si può solo scrivere:
if entryText:
Un elenco Esamina vuoti a False in Python. Lo stesso vale per una stringa vuota, che anche di test per nel codice, e la modifica sarebbe anche rendere il codice un po 'più breve e più leggibile, così invece di questo:
for line in metadataLines:
if line == '':
break
else:
metadataList.append(line)
Si può semplicemente fare:
for line in metadataLines:
if line:
metadataList.append(line)
Ci sono diversi altri problemi con questo codice sia in termini di organizzazione e prestazioni. È possibile assegnare le variabili più volte per la stessa cosa invece di creare un'istanza di un oggetto, una volta e facendo tutti gli accessi per l'oggetto, per esempio. In questo modo si ridurrebbe il numero di incarichi, e anche il numero di variabili globali. Io non voglio sembrare eccessivamente critico, ma questo codice non sembra essere scritto con prestazioni in mente.
Le voci rilevanti per un eventuale ottimizzazione sono quelli con alti valori di ncalls e tottime . bgchr:4(<module>)
e <string>:1(<module>)
probabilmente si riferiscono alla esecuzione del corpo del modulo e non sono rilevanti qui.
Ovviamente, il problema delle prestazioni viene dalla elaborazione delle stringhe. Questo dovrebbe forse essere ridotto. I punti caldi sono split
, join
e sys.stdout.write
. bz2.decompress
sembra anche essere costoso.
Vi suggerisco di provare il seguente:
- I dati principale sembra consistere di scheda separata valori CSV. Provate, se esegue lettore CSV migliori.
- sys.stdout è la linea tamponato e lavato ogni volta che una nuova riga viene scritta. Prendere in considerazione la scrittura su un file con un buffer di dimensioni maggiori.
- Invece di unire elementi prima di scrivere fuori, li scrivere in sequenza nel file di output. Si può anche considerare l'utilizzo di scrittore CSV.
- Invece di decompressione dei dati contemporaneamente in una singola stringa, utilizzare un oggetto BZ2File e passare che al lettore CSV.
Sembra che il corpo del ciclo che decomprime i dati in realtà viene richiamato solo una volta. Forse si trova un modo per evitare il dataHandle.read(size)
chiamata, che produce una stringa enorme che viene poi decompresso, e di lavorare con il file oggetto direttamente.
Addendum: BZ2File probabilmente non è applicabile nel tuo caso, perché richiede un argomento filename. Quello che vi serve è qualcosa di simile a una vista oggetto file con limite di lettura integrata, paragonabile a ZipExtFile ma utilizzando BZ2Decompressor per la decompressione.
Il mio punto principale è che il codice dovrebbe essere cambiato per effettuare un'elaborazione più iterativo di dati, invece di slurping in nel suo insieme e la divisione di nuovo in seguito.