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?

¿Fue útil?

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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top