2010-10-09 11 views
11

me gustaría mejorar el rendimiento de una secuencia de comandos de Python y han estado utilizando cProfile para generar un informe de rendimiento:¿Qué es este resultado de cProfile que me dice que necesito corregirlo?

python -m cProfile -o chrX.prof ./bgchr.py ...args... 

abrí este archivo chrX.prof con Python de pstats e imprimir 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 con las operaciones join, split y write para reducir el impacto aparente que tienen en el rendimiento de este script?

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(), a continuación, mi tiempo de ejecución va de 10.2 segundos 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 

Está escribiendo en stdout realmente tan caro en Python?

+2

Divida el código en funciones pequeñas. Python's cProfile es bastante inútil para el código que está escrito como una gran porción, porque es un perfil de función, no un generador de perfiles línea por línea. Mientras tanto, puede aumentar un poco la velocidad si coloca todo en una función main(), ya que en Python el acceso a las variables globales es más lento que el acceso a una variable local. –

+0

@Lie Ryan: ¡mira los números! Estos son lo suficientemente detallados para mostrar dónde se necesita la optimización. El acceso a variables globales no es relevante aquí, y los tiempos para bgchr: 4 () y : 1 () corresponden al tiempo total de ejecución. –

+0

@Bernd Petersohn: Considere la posibilidad de que esté equivocado. Ver mi respuesta –

Respuesta

26

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

Encuentro útil ordenar las estadísticas en tottime y nuevamente en cumtime, no en name.

bgchardefinitivamente se refiere a la ejecución de la secuencia de comandos y no es irrelevante, ya que toma 8.9 segundos de 13.5; ¡que 8.9 segundos NO incluye tiempo en las funciones/métodos que llama! Lea atentamente lo que dice @Lie Ryan sobre la modularización de su script en funciones, e implemente su consejo. Del mismo modo, lo que dice @jonesy.

string se menciona porque usted import string y úsela en un solo lugar: string.find(elements[0], 'p'). En otra línea del resultado, notará que se invocó a string.find 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 en todos lados. Las funciones string están en desuso en la actualidad y se implementan llamando al método str correspondiente. Sería mejor escribir elements[0].find('p') == 0 para obtener un equivalente exacto pero más rápido, y podría gustarle usar elements[0].startswith('p'), lo que salvaría a los lectores preguntándose si ese == 0 realmente debería ser == -1.

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

actualización después de la pregunta revisada con el cambio de secuencia de comandos:

"" "Pregunta: ¿Qué puedo hacer al respecto unir, separar y escribir operaciones para reducir la aparente impacto que tienen en el desempeño de este script""

Huh? los 3 juntos tomar 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 las funciones/métodos que llama. assert(8.5 > 2.6)

Bernd ya tiene te señaló lo que podrías considerar hacer eso. Estás dividiendo innecesariamente la línea completamente solo para unirla nuevamente al escribirla. Necesitas inspeccionar solo el primer elemento. En lugar de elements = line.split('\t'), haga elements = line.split('\t', 1) y reemplace '\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 son sorprendentes. También es asombroso 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? De acuerdo, entonces debe tener en cuenta que, dado que Python 2.2, int, el desbordamiento provoca la promoción a long en lugar de generar una excepción. Puede aprovechar la ejecución más rápida de la aritmética int. También debe tener en cuenta que al hacer long(x) cuando x ya es demostrable un long es un desperdicio de recursos.

Aquí está la función parseJarchLine con cambios de eliminación de residuos marcados con [1] y cambios de cambio a inserción marcados con [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 

Update después pregunta sobre sys.stdout.write

Si la declaración de que era comentada en nada a la original:

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

Entonces la 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 comentario cabo la declaración sys.stdout.write ...

Por cierto, alguien ha mencionado en un comentario sobre romper esto en más de una escritura ... ¿ha considerado esto? ¿Cuántos bytes en promedio en los elementos [1:]? En el cromosoma?

=== cambio de tema: Me preocupa que inicie lastEnd en "" en lugar de hacerlo en cero, y que nadie lo haya comentado.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 similarmente preocupado por las dos variables globales lastEnd y pLength - la función parseJarchLine es ahora tan pequeño que se puede plegar hacia atrás en el cuerpo de su único interlocutor, extractData, que guarda dos variables globales, y se llama a un montón de funciones. También puede guardar búsquedas de miles de millones de sys.stdout.write poniendo write = sys.stdout.write una vez en la parte delantera de extractData y usar eso en su lugar.

BTW, el script prueba Python 2.5 o superior; ¿Has probado perfilando en 2.5 y 2.6?

+0

Estoy de acuerdo con la interpretación de los números. Pero también miré el código: obviamente, el trabajo principal se hace en la última sección marcada como "Extraer datos". Lo que veo es una gran cantidad de creaciones de cuerda de lo que debe ser una gran cantidad de datos. Y este es el punto que necesita optimización a mis ojos. Probablemente no obtendrá muchos más números informativos si pone ese código en una función. Y sí, personalmente habría estructurado este código de manera diferente. –

+0

He reemplazado el código fuente y los resultados resultantes del análisis cProfile. Si tiene un momento para echar un vistazo y ofrecer sugerencias, lo agradecería. Gracias. –

+0

Gracias por su útil consejo. No soy un experto en Python y no tenía idea de que los tipos 'int' se promocionan a' long' automáticamente, en función del trabajo en otros idiomas. Sin embargo, los cambios que sugirió solo parecen disminuir 0.8 segundos. Mi script aún toma el doble de tiempo que una solución 'csh/awk' (una que no permite el uso de acceso aleatorio basado en' seek', como Python, y debe ser más lenta). A menos que haya otras optimizaciones y trucos específicos del lenguaje Python (que aún permiten escribir en el resultado estándar), creo que tendré que buscar una solución basada en C en este momento. –

-1

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 de módulo y no son relevantes aquí.

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

sugiero que pruebe lo siguiente:

  • Sus datos principal parece consistir en la pestaña de valores separados CSV. Pruebe, si el lector CSV tiene un mejor rendimiento.
  • sys.stdout es un buffer 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 usar 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 datos solo se invoca una vez. Tal vez encuentre una forma de evitar la llamada dataHandle.read(size), que produce una cadena enorme que luego se descomprime, y para trabajar 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 objeto de archivo con límite de lectura integrado, comparable a ZipExtFile pero usando BZ2Decompressor para la descompresión.

Mi punto principal aquí es que su código debe ser cambiado para realizar un procesamiento más iterativo de sus datos en lugar de sorberlo en su totalidad y dividirlo nuevamente después.

+0

-1 Ver mi respuesta. –

3

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

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

if len(entryText) > 0:

Usted puede escribir:

if entryText:

Una lista vacía se evalúa como False en Python. Lo mismo es cierto para una cadena vacía, que también ensayan en su código, y el cambio sería también hacer 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) 

Existen muchos otros problemas con este código en términos de organización y rendimiento. Asigna variables varias veces a lo mismo en lugar de simplemente crear una instancia de objeto una vez y hacer todos los accesos en el objeto, por ejemplo. Hacer esto reduciría la cantidad de asignaciones y también el número de variables globales. No quiero parecer excesivamente crítico, pero este código no parece estar escrito teniendo en cuenta el rendimiento.

+0

He reemplazado el código fuente y los resultados resultantes del análisis cProfile. Si tiene un momento para echar un vistazo y ofrecer sugerencias, lo agradecería. Gracias. –

+0

Primero, creo que lo que se ha sugerido aquí por varias personas generalmente es un buen consejo. En segundo lugar, todavía estoy pensando que el uso desenfrenado de los globales tendrá un impacto en algo que analiza una gran cantidad de datos: coloque las funciones apropiadas en una clase, y eso desaparecerá casi por completo. Además, envolver las cosas en una clase hace que sea mucho más fácil experimentar con soluciones como el enhebrado y el multiprocesamiento (prefiero el multiprocesamiento sobre el subprocesamiento para este caso, p. Ej.). Si muestra una entrada de muestra, probablemente obtendrá más comentarios. – jonesy

+2

'if line == '': break; else: metadataList.append (línea) 'no es lo mismo que' if line: metadataList.append (line) ' – warvariuc

Cuestiones relacionadas