¿Qué es este resultado cProfile que me dice que necesito arreglarlo?

Me gustaría mejorar el rendimiento de un script de Python y he estado usando 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 e pstats 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() 1 0.000 0.000 13.780 13.780 :1() 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 para join , split y write operaciones para reducir el impacto aparente que tienen en el rendimiento de este script?

Si es relevante, aquí está el código fuente completo del script 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 [ | --list] \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 [ | --list] \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 la instrucción sys.stdout.write en el primer condicional de parseJarchLine() , mi tiempo de ejecución va de 10.2 a 4.8 segundos:

 # 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 

¿Escribir en stdout realmente es caro en Python?

ncalls es relevante solo en la medida en que la comparación de números con otros conteos, como el número de caracteres / campos / líneas en un archivo, puede resaltar anomalías; lo que realmente importa es el cumtime tottime y el tiempo de la cumtime . cumtime es el tiempo empleado en la función / método, incluido el tiempo empleado en las funciones / métodos a los que llama; tottime es el tiempo empleado en la función / método, excluyendo el tiempo empleado en las funciones / métodos que llama.

Me resulta útil ordenar las estadísticas en tiempo de tottime y nuevamente en tiempo de cumtime , no en name .

bgchar definitivamente se refiere a la ejecución del script y no es irrelevante, ya que toma 8.9 segundos de 13.5; ¡esos 8,9 segundos NO incluyen el tiempo en las funciones / métodos a los que llama! Lea detenidamente lo que @Lie Ryan dice acerca de modularizar su script en funciones, e implemente sus consejos. Igualmente lo que dice @jonesy.

Se menciona la string porque usted import string y la usa en un solo lugar: string.find(elements[0], 'p') . En otra línea en la salida, notará que string.find fue llamado solo una vez, por lo que no es un problema de rendimiento en esta ejecución de este script. SIN EMBARGO: usas los métodos str cualquier otro lugar. string funciones de string están en desuso hoy en día y se implementan llamando al método str correspondiente. Sería mejor escribir los elements[0].find('p') == 0 para un equivalente exacto pero más rápido, y le gustaría usar los elements[0].startswith('p') que ahorraría a los lectores que se preguntan si eso es == 0 realidad debería ser == -1 .

Los cuatro métodos mencionados por @Bernd Petersohn toman solo 3.7 segundos de un tiempo total de ejecución de 13.541 segundos. Antes de preocuparse demasiado por eso, modularice su script en funciones, ejecute cProfile de nuevo y ordene las estadísticas por tottime .

Actualizar después de la pregunta revisada con el script modificado:

“” “Pregunta: ¿Qué puedo hacer con respecto a las operaciones de unir, dividir y escribir para reducir el impacto aparente que tienen en el rendimiento de este script?” ”

Eh Esos 3 juntos toman 2.6 segundos del total de 13.8. La función parseJarchLine está tomando 8.5 segundos (lo que no incluye el tiempo empleado por las funciones / métodos a los que llama. assert(8.5 > 2.6)

Bernd ya te ha indicado lo que podrías considerar hacer con ellos. Usted está innecesariamente dividiendo la línea completamente solo para volver a unirla al escribirla. Necesitas inspeccionar solo el primer elemento. En lugar de elements = line.split('\t') do elements = line.split('\t', 1) y reemplaza '\t'.join(elements[1:]) por elements[1] .

Ahora vamos a sumergirnos en el cuerpo de parseJarchLine. El número de usos en la fuente y la forma de los usos de la función incorporada long es asombroso. También es sorprendente el hecho de que long no se menciona en la salida de cProfile.

¿Por qué necesitas long ? Archivos de más de 2 Gb? De acuerdo, entonces debe tener en cuenta que, dado que Python 2.2, el desbordamiento de int provoca que la promoción sea long lugar de generar una excepción. Puede aprovechar la ejecución más rápida de la aritmética int . También debe considerar que hacer long(x) cuando x ya es demostrablemente long es un desperdicio de recursos.

Aquí está la función parseJarchLine con los cambios de eliminación de residuos marcados [1] y los cambios de cambio a int marcados [2]. Buena idea: hacer cambios en pequeños pasos, volver a probar, volver a perfilar.

 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 después de la pregunta sobre sys.stdout.write

Si la statement que comentaste fue similar a la original:

 sys.stdout.write("%s\t%ld\t%ld\t%s\n" % (chromosome, start, lastEnd, '\t'.join(elements[1:]))) 

Entonces tu pregunta es … interesante. Prueba esto:

 payload = "%s\t%ld\t%ld\t%s\n" % (chromosome, start, lastEnd, '\t'.join(elements[1:])) sys.stdout.write(payload) 

Ahora comente la sentencia sys.stdout.write

Por cierto, alguien mencionado en un comentario acerca de dividir esto en más de una escritura … ¿has considerado esto? ¿Cuántos bytes en promedio en los elementos [1:]? ¿En el cromosoma?

=== cambio de tema: me preocupa que inicialice lastEnd a "" lugar de a cero, y que nadie haya comentado al respecto. De cualquier forma, deberías arreglar esto, lo que permite una simplificación bastante drástica y además agrega otras sugerencias:

 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 igualmente preocupado por las dos variables globales lastEnd y pLength : la función parseJarchLine ahora es tan pequeña que se puede volver a plegar en el cuerpo de su único llamador, extractData , que guarda dos variables globales, y una función de gazillion llama . También puede guardar un sys.stdout.write de búsquedas de sys.stdout.write colocando write = sys.stdout.write una vez en la parte frontal de extractData y usando eso en su lugar.

Por cierto, el script prueba para Python 2.5 o mejor; ¿Has probado perfilar en 2.5 y 2.6?

Esta salida será más útil si su código es más modular, como lo ha dicho Lie Ryan. Sin embargo, un par de cosas que puede recoger de la salida y solo mirando el código fuente:

Estás haciendo muchas comparaciones que no son realmente necesarias en Python. Por ejemplo, en lugar de:

if len(entryText) > 0:

Usted puede simplemente escribir:

if entryText:

Una lista vacía se evalúa como Falso en Python. Lo mismo es cierto para una cadena vacía, que también verificas en tu código, y cambiarla también hará que el código sea un poco más corto y más legible, así que en lugar de esto:

  for line in metadataLines: if line == '': break else: metadataList.append(line) 

Usted puede simplemente 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 rendimiento. Asigna variables varias veces a la misma cosa en lugar de solo crear una instancia de objeto una vez y hacer todos los accesos al objeto, por ejemplo. Hacer esto reduciría el número de asignaciones, y también el número de variables globales. No quiero parecer demasiado crítico, pero este código no parece estar escrito teniendo en cuenta el rendimiento.

Las entradas relevantes para una posible optimización son aquellas con valores altos para ncalls y tottime . bgchr:4() y :1() probablemente se refieran a la ejecución del cuerpo de su módulo y no son relevantes aquí.

Obviamente, su problema de rendimiento proviene del procesamiento de cadenas. Esto tal vez debería reducirse. Los puntos calientes son split , join y sys.stdout.write . bz2.decompress también parece ser costoso.

Te sugiero que pruebes lo siguiente:

  • Sus datos principales parecen consistir en valores CSV separados por tabulaciones. Pruébalo, si el lector CSV funciona mejor.
  • sys.stdout tiene búfer de línea y se vacía cada vez que se escribe una nueva línea. Considere escribir en un archivo con un tamaño de búfer más grande.
  • En lugar de unir elementos antes de escribirlos, escríbalos secuencialmente en 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, use un objeto BZ2File y páselo al lector CSV.

Parece que el cuerpo del bucle que realmente descomprime los datos solo se invoca una vez. Tal vez encuentre una manera de evitar la llamada dataHandle.read(size) , que produce una cadena enorme que luego se descomprime, y trabajar con el objeto de archivo directamente.

Anexo: BZ2File probablemente no sea aplicable en su caso, ya que requiere un argumento de nombre de archivo. Lo que necesita es algo así como una vista de objeto de archivo con límite de lectura integrado, comparable a ZipExtFile pero usando BZ2Decompressor para descompresión.

Mi punto principal aquí es que su código debe cambiarse para realizar un procesamiento más iterativo de sus datos en lugar de asimilarlos como un todo y luego dividirlos nuevamente.