¿Cuál es este resultado cprofile me dice que tengo que corregir?
-
29-09-2019 - |
Pregunta
Me gustaría mejorar el rendimiento de un script en Python y han estado utilizando cProfile
para generar un informe de rendimiento:
python -m cProfile -o chrX.prof ./bgchr.py ...args...
Abrí este archivo chrX.prof
con pstats
de Python y se imprimen las estadísticas:
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)
Pregunta: ¿Qué puedo hacer yo acerca de las operaciones join
, split
y write
para reducir la aparente impacto que tienen en el desempeño de esta secuencia de comandos
Si es relevante, aquí está el código fuente completo a la escritura en cuestión:
#!/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))
Editar
Si comento hacia fuera la declaración sys.stdout.write
en el primer condicional de parseJarchLine()
, entonces mi tiempo de ejecución pasa de 10,2 seg a 4,8 seg:
# 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á escribiendo a stdout
realmente tan caro en Python?
Solución
ncalls
es relevante sólo en la medida en que la comparación de los números contra otros recuentos como el número de chars / campos / líneas en un archivo puede Highligh anomalías; tottime
y cumtime
es lo que realmente importa. cumtime
es el tiempo de permanencia en la función / método incluir el tiempo de permanencia en las funciones / métodos que se llama; tottime
es el tiempo de permanencia en la función / método excluyendo el tiempo de permanencia en las funciones / métodos que llama.
Me resulta muy útil para ordenar las estadísticas sobre tottime
y otra vez en cumtime
, no en name
.
bgchar
definitivamente se refiere a la ejecución del script y no es irrelevante, ya que ocupa 8,9 segundos de cada 13,5; que 8,9 segundos no incluye el tiempo en las funciones / métodos que llama! Leer cuidadosamente lo @Lie Ryan dice acerca de la modularización de su script en funciones, y poner en práctica su consejo. Del mismo modo lo @jonesy dice.
string
se menciona porque import string
y utilizarlo en un solo lugar: string.find(elements[0], 'p')
. En otra línea en la salida se dará cuenta de que string.find se llama sólo una vez, así que no es un problema de rendimiento en esta carrera de este guión. Sin embargo: Se utilizan los métodos str
en cualquier otro lugar. string
funciones están en desuso hoy en día y se implementan mediante una llamada al método str
correspondiente. Que sería mejor escribir elements[0].find('p') == 0
para una exacta pero más rápido equivalentes, y puede ser que les gusta usar elements[0].startswith('p')
lo que ahorraría a los lectores preguntando si == 0
que debería ser en realidad == -1
.
Los cuatro métodos mencionados por @Bernd Petersohn ocupan sólo 3,7 segundos de un tiempo de ejecución total de 13.541 segundos. Antes de preocuparse demasiado acerca de ellos, modularizar su script en funciones, ejecutar cprofile de nuevo, y ordenar las estadísticas de todas tottime
.
Actualizar tras pregunta revisada con el cambio de secuencia de comandos:
"" "Pregunta: ¿Qué puedo hacer con respecto a unirse, las operaciones de división y de escritura para reducir la aparente impacto que tienen en el desempeño de este script" "
¿Eh? Los 3 juntos toman 2,6 segundos, de un total de 13,8. Su función parseJarchLine está llevando a 8,5 segundos (que no incluye el tiempo empleado por funciones / métodos que llama. assert(8.5 > 2.6)
Bernd ya le ha señalado en lo que se podría considerar hacer con ellos. Usted está innecesariamente la división de la línea completamente única para unirse de nuevo al escribir a cabo. Hay que fijarse sólo el primer elemento. En lugar de hacer elements = line.split('\t')
elements = line.split('\t', 1)
y reemplazar '\t'.join(elements[1:])
por elements[1]
.
Ahora vamos a bucear en el cuerpo de parseJarchLine. El número de usos en la fuente y la forma de los usos de la long
función integrada son sorprendentes. También es sorprendente el hecho de que long
no se menciona en la salida cprofile.
¿Por qué necesita long
en absoluto? Archivos de más de 2 gb? OK, entonces debe tener en cuenta que desde Python 2.2, desbordamiento int
provoca el ascenso a long
en lugar de lanzar una excepción. Usted puede tomar ventaja de una ejecución más rápida de la aritmética int
. También es necesario tener en cuenta que haciendo long(x)
cuando x
ya es demostrable una long
es un desperdicio de recursos.
Aquí es la función parseJarchLine con residuos eliminación de cambios marcados [1] y cambiar-a-int cambios marcados [2]. Buena idea: Realizar cambios en pasos pequeños, repetición de la prueba, re-perfil
.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
Actualizar tras pregunta sobre sys.stdout.write
Si la declaración de que usted comentada se parecía a la original:
sys.stdout.write("%s\t%ld\t%ld\t%s\n" % (chromosome, start, lastEnd, '\t'.join(elements[1:])))
A continuación, su pregunta es ... interesante. Trataresto:
payload = "%s\t%ld\t%ld\t%s\n" % (chromosome, start, lastEnd, '\t'.join(elements[1:]))
sys.stdout.write(payload)
ahora Comentario a cabo la declaración sys.stdout.write
...
Por cierto, alguien mencionó en un comentario acerca de romper esto en más de una escritura ... ¿ha considerado esto? Cuántos bytes de media en elementos [1:]? En el cromosoma?
=== cambio de tema: Me preocupa que inicializar lastEnd
a ""
en lugar de a cero, y que nadie ha comentado sobre el mismo. Cualquier manera, usted debe solucionar este problema, lo que permite una simplificación drástica en vez más la adición de las sugerencias de los demás:
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)
Ahora estoy preocupado de manera similar sobre las dos variables globales lastEnd
y pLength
- la función parseJarchLine ahora es tan pequeño que se puede plegar de nuevo en el cuerpo de su llamador única, extractData
, lo que ahorra dos variables globales, y una función tropecientos llama. Usted también podría guardar un tropecientos búsquedas de sys.stdout.write
poniendo write = sys.stdout.write
una vez que la parte delantera de extractData
y usar en su lugar.
Por cierto, las pruebas de script para Python 2,5 o mejor; ¿Ha tratado de perfilar en 2.5 y 2.6?
Otros consejos
Esta salida va a ser más útil si su código es más modular como Lie Ryan ha declarado. Sin embargo, un par de cosas que usted puede recoger a partir de la salida y con sólo mirar el código fuente:
Usted está haciendo una gran cantidad de comparaciones que no son realmente necesarios en Python. Por ejemplo, en lugar de:
if len(entryText) > 0:
Se puede escribir:
if entryText:
Una lista vacía como resultado false en Python. Lo mismo es cierto para una cadena vacía, que también se prueba en su código, y cambiar también haría que el código un poco más corto y más fácil de leer, así que en vez de esto:
for line in metadataLines:
if line == '':
break
else:
metadataList.append(line)
Sólo puede hacer:
for line in metadataLines:
if line:
metadataList.append(line)
Hay varios otros problemas con este código en términos de organización y funcionamiento. Se asignan las variables varias veces a la misma cosa en lugar de sólo la creación de una instancia de objeto de una vez haciendo todos los accesos en el objeto, por ejemplo. Hacer esto reduciría el número de asignaciones, y también el número de variables globales. No quiero sonar demasiado crítico, pero este código no parece ser escritos con el rendimiento en mente.
Las entradas relevantes para su posible optimización son los que tienen altos valores de ncalls y tottime . bgchr:4(<module>)
y <string>:1(<module>)
probablemente se refieren a la ejecución de su cuerpo módulo y no son relevantes aquí.
Obviamente, el problema de rendimiento proviene de procesamiento de cadenas. Tal vez esto se debe reducir. Los puntos calientes son split
, join
y sys.stdout.write
. bz2.decompress
también parece ser costoso.
Le sugiero que pruebe lo siguiente:
- Sus datos principal parece consistir en la pestaña de valores separados CSV. Pruebe, si realiza el lector CSV mejor.
- sys.stdout es la línea tamponada y lava cada vez que un salto de línea está escrito. Que no escribe en un archivo con un tamaño de búfer mayor.
- En lugar de elementos de unión antes de escribir a cabo, escribir en forma secuencial para el archivo de salida. También puede considerar el uso de escritor CSV.
- En lugar de descomprimir los datos a la vez en una sola cadena, utilice un objeto BZ2File y pasar que al lector CSV.
Parece que el cuerpo del bucle que realmente descomprime datos sólo se invoca una vez. Tal vez a encontrar una manera de evitar la dataHandle.read(size)
llamada, que produce una enorme cadena que se descomprime a continuación, y para el trabajo con el objeto de archivo directamente.
Adición: BZ2File probablemente no es aplicable en su caso, ya que requiere un nombre de fichero. Lo que necesita es algo así como una vista de objetos de archivo con límite de lectura integrada, comparable a ZipExtFile pero utilizando BZ2Decompressor para la descompresión.
Mi punto principal aquí es que el código debe ser cambiado para realizar un procesamiento más iterativo de los datos en lugar de sorber en su conjunto y la división de nuevo después.