Что этот результат cProfile говорит мне, что мне нужно исправить?

Я хотел бы улучшить производительность сценария Python и использую cProfile создать отчет о производительности:

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

Я открыл это chrX.prof файл с питоном pstats и распечатал статистику:

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) 

Вопрос: Что я могу сделать с join, split а также write операции, чтобы уменьшить видимое влияние, которое они оказывают на производительность этого скрипта?

Если это уместно, вот полный исходный код рассматриваемого скрипта:

#!/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))

РЕДАКТИРОВАТЬ

Если я закомментирую sys.stdout.write утверждение в первом условном parseJarchLine()то время выполнения увеличивается с 10,2 до 4,8 с:

# 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

Пишет в stdout действительно так дорого в Python?

3 ответа

Решение

ncalls имеет отношение только к той степени, в которой сравнение чисел с другими значениями, такими как число символов / полей / строк в файле, может выделить аномалии; tottime а также cumtime это то, что действительно имеет значение. cumtime время, потраченное на функцию / метод, включая время, потраченное на функции / методы, которые она вызывает; tottime время, потраченное на функцию / метод, исключая время, потраченное на функции / методы, которые она вызывает.

Я считаю полезным сортировать статистику по tottime и снова на cumtime, не на name,

bgchar определенно относится к выполнению сценария и не имеет значения, поскольку занимает 13,9 секунды из 13,5; что 8,9 секунды НЕ включают время в вызываемых им функциях / методах! Внимательно прочитайте, что @Lie Ryan говорит о модульности вашего скрипта в функции, и реализуйте его советы. Точно так же, как говорит @jonesy.

string упоминается потому, что вы import string и использовать его только в одном месте: string.find(elements[0], 'p'), В другой строке вывода вы заметите, что string.find был вызван только один раз, так что это не проблема производительности в этом запуске этого скрипта. ОДНАКО: Вы используете str методы везде string функции устарели в настоящее время и реализуются путем вызова соответствующих str метод. Вы бы лучше написали elements[0].find('p') == 0 для точного, но более быстрого эквивалента, и, возможно, хотел бы использовать elements[0].startswith('p') который спасет читателей, задающихся вопросом, является ли это == 0 должно быть на самом деле == -1,

Четыре метода, упомянутые @Bernd Petersohn, занимают всего 3,7 секунды из общего времени выполнения 13,541 секунды. Прежде чем беспокоиться об этом, разбейте ваш скрипт на функции, снова запустите cProfile и отсортируйте статистику по tottime,

Обновление после исправления вопроса с измененным сценарием:

"" "Вопрос: что я могу сделать с операциями объединения, разделения и записи, чтобы уменьшить видимое влияние, которое они оказывают на производительность этого сценария?""

А? Эти 3 вместе занимают 2,6 секунды из 13,8. Ваша функция parseJarchLine занимает 8,5 секунд (что не включает время, затрачиваемое вызываемыми ей функциями / методами). assert(8.5 > 2.6)

Бернд уже указал вам на то, что вы могли бы сделать с ними. Вы напрасно разделяете строку, чтобы снова присоединиться к ней при записи. Вам нужно осмотреть только первый элемент. Вместо elements = line.split('\t') делать elements = line.split('\t', 1) и заменить '\t'.join(elements[1:]) от elements[1],

Теперь давайте погрузимся в тело parseJarchLine. Количество использований в источнике и способ использования long встроенная функция удивительна Также удивительным является тот факт, что long не упоминается в выходных данных cProfile.

Зачем тебе long совсем? Файлы больше 2 Гб? Хорошо, тогда вы должны учитывать, что начиная с Python 2.2, int переполнение вызывает повышение long вместо того, чтобы поднять исключение. Вы можете воспользоваться более быстрым исполнением int арифметика. Вы также должны учитывать, что делать long(x) когда x уже наглядно long это пустая трата ресурсов.

Вот функция parseJarchLine с изменениями удаления отходов, помеченными [1], и изменениями, внесенными в int, помеченными [2]. Хорошая идея: вносить изменения небольшими шагами, перепроверять, перепрофилировать.

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

Обновление после вопроса оsys.stdout.write

Если закомментированное вами утверждение было чем-то похожим на оригинальное:

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

Тогда ваш вопрос... интересен. Попробуй это:

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

Теперь закомментируйте sys.stdout.write заявление...

Кстати, кто-то упомянул в комментарии о том, чтобы разбить это на более чем одну запись... Вы рассматривали это? Сколько байт в среднем в элементах [1:]? В хромосоме?

=== смена темы: меня беспокоит, что вы инициализируете lastEnd в "" а не к нулю, и что никто не прокомментировал это. В любом случае, вы должны это исправить, что допускает довольно резкое упрощение и добавление других предложений:

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)

Теперь я так же беспокоюсь о двух глобальных переменных lastEnd а также pLength - функция parseJarchLine теперь настолько мала, что ее можно сложить обратно в тело своего единственного абонента, extractData, который сохраняет две глобальные переменные и вызов функции gazillion. Вы также можете сохранить поиск миллиардов sys.stdout.write положив write = sys.stdout.write однажды впереди extractData и используя это вместо этого.

Кстати, скрипт тестирует для Python 2.5 или выше; Вы пробовали профилирование на 2.5 и 2.6?

Этот вывод будет более полезным, если ваш код будет более модульным, как заявил Ли Райан. Тем не менее, пара вещей, которые вы можете извлечь из вывода и просто смотреть на исходный код:

Вы делаете много сравнений, которые на самом деле не нужны в Python. Например, вместо:

if len(entryText) > 0:

Вы можете просто написать:

if entryText:

Пустой список оценивается как False в Python. То же самое верно для пустой строки, которую вы также проверяете в своем коде, и изменение ее также сделает код немного короче и более читабельным, поэтому вместо этого:

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

Вы можете просто сделать:

for line in metadataLines:
    if line:
       metadataList.append(line)

Есть несколько других проблем с этим кодом с точки зрения организации и производительности. Вы назначаете переменные несколько раз одной и той же вещи, вместо того, чтобы просто создать экземпляр объекта один раз и, например, выполнить все обращения к объекту. Это уменьшит количество назначений, а также количество глобальных переменных. Я не хочу звучать слишком критично, но этот код, похоже, написан не с точки зрения производительности.

Записи, релевантные для возможной оптимизации, - это записи с высокими значениями для ncalls и totaltime. bgchr:4(<module>) а также <string>:1(<module>) вероятно, ссылаются на выполнение вашего тела модуля и здесь не актуальны.

Очевидно, что ваша проблема производительности связана с обработкой строк. Это, возможно, должно быть уменьшено. Горячие точки split, join а также sys.stdout.write, bz2.decompress также кажется дорогостоящим.

Я предлагаю вам попробовать следующее:

  • Ваши основные данные, кажется, состоят из значений CSV, разделенных табуляцией. Попробуйте, если читатель CSV работает лучше.
  • sys.stdout буферизуется и сбрасывается при каждом написании новой строки. Рассмотрите возможность записи в файл с большим размером буфера.
  • Вместо того, чтобы соединять элементы перед их записью, записывайте их последовательно в выходной файл. Вы также можете рассмотреть возможность использования CSV Writer.
  • Вместо того, чтобы сразу распаковывать данные в одну строку, используйте объект BZ2File и передайте его в CSV-ридер.

Кажется, что тело цикла, которое фактически распаковывает данные, вызывается только один раз. Возможно, вы найдете способ избежать звонка dataHandle.read(size), который создает огромную строку, которая затем распаковывается, и напрямую работает с файловым объектом.

Приложение: BZ2File, вероятно, не применим в вашем случае, потому что он требует аргумента имени файла. То, что вам нужно, это что-то вроде представления файлового объекта со встроенным пределом чтения, сравнимым с ZipExtFile, но использующим BZ2Decompressor для распаковки.

Здесь я хочу сказать, что ваш код должен быть изменен, чтобы выполнять более итеративную обработку ваших данных, вместо того, чтобы вбивать их в единое целое и потом снова разбивать.

Другие вопросы по тегам