Qu'est-ce que ce résultat cprofile me disant que je dois fixer?
-
29-09-2019 - |
Question
Je voudrais améliorer les performances d'un script Python et ont été à l'aide cProfile
pour générer un rapport de performance:
python -m cProfile -o chrX.prof ./bgchr.py ...args...
J'ai ouvert ce fichier chrX.prof
avec pstats
de Python et d'imprimer les statistiques:
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)
Question: Que puis-je faire des opérations join
, split
et write
pour réduire l'impact apparent qu'ils ont sur les performances de ce script
Si elle est pertinente, voici le code source complet du script en question:
#!/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))
EDIT
Si je commente la déclaration de sys.stdout.write
dans la première condition de parseJarchLine()
, puis mon exécution passe de 10,2 sec à 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
est en train d'écrire à stdout
vraiment si cher que ça en Python?
La solution
ncalls
est pertinent que dans la mesure où l'on compare les numéros contre d'autres chiffres tels que le nombre de caractères / champs / lignes dans un fichier peut highligh anomalies; tottime
et cumtime
est ce qui importe vraiment. cumtime
est le temps passé dans la fonction / méthode dont le temps passé dans les fonctions / méthodes qu'il appelle; tottime
est le temps passé dans la fonction / méthode sauf le temps passé dans les fonctions / méthodes qu'il appelle.
Je trouve utile de trier les statistiques sur tottime
et encore sur cumtime
, pas name
.
bgchar
définitivement fait référence à l'exécution du script et n'est pas inutile car il faut 8,9 secondes sur 13,5; que 8,9 secondes ne contient pas de temps dans les fonctions / méthodes qu'il appelle! Lisez attentivement ce @Lie Ryan dit à propos modularisation votre script en fonctions, et mettre en œuvre ses conseils. De même ce que dit @jonesy.
string
est mentionné parce que vous import string
et l'utiliser en un seul endroit: string.find(elements[0], 'p')
. Sur une autre ligne dans la sortie, vous remarquerez que string.find a été appelé une seule fois, il est donc pas un problème de performance dans cette série de ce script. CEPENDANT: Vous utilisez des méthodes de str
partout ailleurs. fonctions string
sont dépréciées de nos jours et sont mises en œuvre en appelant la méthode de str
correspondante. Vous seriez mieux écrire elements[0].find('p') == 0
pour une exacte mais plus rapide équivalent, et peut-être que l'utilisation elements[0].startswith('p')
qui sauverait les lecteurs se demandant si cette == 0
devrait effectivement être == -1
.
Les quatre méthodes mentionnées par @Bernd Petersohn prennent seulement 3,7 secondes d'un temps d'exécution total de 13.541 secondes. Avant de se préoccuper trop de ceux-ci, modularisation votre script en fonctions, exécutez cprofile à nouveau, et trier les statistiques par tottime
.
Mise à jour après la question révisée avec le script modifié:
« » « Question: Que puis-je joindre à propos, les opérations de division et d'écriture pour réduire l'impact apparent qu'ils ont sur les performances de ce script » "
Huh? Les 3 prennent ensemble 2,6 secondes sur le total de 13,8. Votre fonction parseJarchLine prend 8,5 secondes (qui ne comprend pas le temps pris par des fonctions / méthodes qu'il appelle. assert(8.5 > 2.6)
Bernd vous a déjà fait ce que vous pourriez envisager de faire avec ceux-ci. Vous fractionnez inutilement la ligne complètement seulement pour le joindre à nouveau lors de l'écriture dehors. Vous devez vérifier que le premier élément. Au lieu de faire elements = line.split('\t')
elements = line.split('\t', 1)
et le remplacer par '\t'.join(elements[1:])
elements[1]
.
Maintenant, nous allons plonger dans le corps de parseJarchLine. Le nombre d'utilisations de la source et la manière des utilisations du long
fonction intégrée sont étonnantes. Aussi étonnant est le fait que long
ne figure pas dans la sortie cprofile.
Pourquoi avez-vous besoin long
du tout? Les fichiers de plus de 2 Go? OK, alors vous devez considérer que depuis Python 2.2, débordement de int
provoque la promotion de long
au lieu de soulever une exception. Vous pouvez profiter de l'exécution plus rapide de l'arithmétique int
. Vous devez également considérer que faire long(x)
lorsque x
est déjà manifestement un long
est un gaspillage de ressources.
Voici la fonction parseJarchLine avec l'élimination des déchets des changements marqués [1] et en changeant à des changements marqués int [2]. Bonne idée: apporter des changements dans les petites étapes, re-test, reprofiler
.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
Mise à jour après question sur sys.stdout.write
Si la déclaration que vous avez dit sur était quelque chose comme l'original:
sys.stdout.write("%s\t%ld\t%ld\t%s\n" % (chromosome, start, lastEnd, '\t'.join(elements[1:])))
Alors, votre question est ... intéressante. Essayerceci:
payload = "%s\t%ld\t%ld\t%s\n" % (chromosome, start, lastEnd, '\t'.join(elements[1:]))
sys.stdout.write(payload)
maintenant commentaire sur la déclaration de sys.stdout.write
...
Par ailleurs, quelqu'un a mentionné dans un commentaire au sujet de briser ce dans plus d'une écriture ... avez-vous pensé cela? Combien d'octets en moyenne dans les éléments [1:]? Dans le chromosome?
=== changement de sujet: Il me inquiète que vous initialisez lastEnd
à ""
plutôt que de zéro, et que personne n'a commenté à ce sujet. De toute façon, vous devez résoudre ce problème, ce qui permet une simplification assez radicale, plus l'ajout dans les suggestions des autres:
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)
Maintenant, je suis inquiet de la même sur les deux variables globales et lastEnd
pLength
- la fonction parseJarchLine est si petit qu'il peut être replié dans le corps de son unique interlocuteur, extractData
, qui enregistre deux variables globales, et appelle la fonction gazillion. Vous pourriez également enregistrer un gazillion de recherches sys.stdout.write
en mettant write = sys.stdout.write
une fois l'avant de extractData
et en utilisant cette place.
BTW, les tests de script pour Python 2.5 ou mieux; Avez-vous essayé de profilage sur 2.5 et 2.6?
Autres conseils
Cette sortie va être plus utile si votre code est plus modulaire Lie Ryan a déclaré. Cependant, un certain nombre de choses que vous pouvez chercher à la sortie et juste regarder le code source:
Vous êtes en train de faire beaucoup de comparaisons qui ne sont pas réellement nécessaires en Python. Par exemple, au lieu de:
if len(entryText) > 0:
Vous pouvez simplement écrire:
if entryText:
Une liste vide est évaluée à faux en Python. La même chose est vrai pour une chaîne vide, que vous avez test dans votre code, et le changer serait également le code un peu plus court et plus lisible, donc au lieu de ceci:
for line in metadataLines:
if line == '':
break
else:
metadataList.append(line)
Vous pouvez simplement faire:
for line in metadataLines:
if line:
metadataList.append(line)
Il y a plusieurs autres problèmes avec ce code en termes de l'organisation et de la performance. Vous attribuez des variables plusieurs fois à la même chose au lieu de simplement créer une instance d'objet une fois et de faire tous les accès sur l'objet, par exemple. Faire cela réduirait le nombre de missions, ainsi que le nombre de variables globales. Je ne veux pas paraître trop critique, mais ce code ne semble pas être écrit avec des performances à l'esprit.
Les entrées pertinentes pour l'optimisation possibles sont ceux avec des valeurs élevées pour ncalls et tottime . bgchr:4(<module>)
et <string>:1(<module>)
font probablement référence à l'exécution de votre corps de module et ne sont pas pertinentes en l'espèce.
De toute évidence, votre problème de performance provient de traitement de chaîne. Cela devrait peut-être réduit. Les points chauds sont split
, join
et sys.stdout.write
. bz2.decompress
semble aussi être coûteux.
Je vous suggère d'essayer ce qui suit:
- Vos données principale semble consister en valeurs séparées par des tabulations CSV. Essayez, si le lecteur CSV fonctionne mieux.
- sys.stdout est en ligne et tamponne rincée à chaque fois qu'un saut de ligne est écrite. Pensez à écrire dans un fichier avec une plus grande mémoire tampon.
- Au lieu de se joindre à des éléments avant de les écrire sur, les écrire de manière séquentielle dans le fichier de sortie. Vous pouvez également envisager d'utiliser l'écrivain CSV.
- Au lieu de décompresser les données à la fois en une seule chaîne, utilisez un objet BZ2File et de transmettre au lecteur que CSV.
Il semble que le corps de boucle qui décompresse les données ne fait invoqué qu'une seule fois. Peut-être vous trouver un moyen d'éviter l'dataHandle.read(size)
d'appel, qui produit une grande chaîne qui est ensuite décompressé, et de travailler avec l'objet fichier directement.
Addendum: BZ2File est probablement pas applicable dans votre cas, car il nécessite un argument de nom de fichier. Qu'est-ce que vous avez besoin est quelque chose comme une vue de l'objet fichier avec limite de lecture intégrée, comparable à ZipExtFile mais en utilisant BZ2Decompressor pour la décompression.
Mon point principal est que votre code doit être modifié pour effectuer un traitement plus itérative de vos données au lieu de siphonage dans son ensemble et le fractionnement encore par la suite.