Python получает значимые результаты от cProfile

У меня есть Python script в файле, для запуска которого требуется более 30 секунд. Я пытаюсь профилировать его, поскольку я хотел бы сократить это время резко.

Я пытаюсь профилировать script с помощью cProfile, но по существу все, что мне кажется, говорит, что да, главная script заняла много времени, но не дает такого рода я ожидал. На терминале я печатаю что-то вроде:

cat my_script_input.txt | python -m cProfile -s time my_script.py

Получаемые результаты:

<my_script_output>
             683121 function calls (682169 primitive calls) in 32.133 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1   31.980   31.980   32.133   32.133 my_script.py:18(<module>)
   121089    0.050    0.000    0.050    0.000 {method 'split' of 'str' objects}
   121090    0.038    0.000    0.049    0.000 fileinput.py:243(next)
        2    0.027    0.014    0.036    0.018 {method 'sort' of 'list' objects}
   121089    0.009    0.000    0.009    0.000 {method 'strip' of 'str' objects}
   201534    0.009    0.000    0.009    0.000 {method 'append' of 'list' objects}
   100858    0.009    0.000    0.009    0.000 my_script.py:51(<lambda>)
      952    0.008    0.000    0.008    0.000 {method 'readlines' of 'file' objects}
 1904/952    0.003    0.000    0.011    0.000 fileinput.py:292(readline)
    14412    0.001    0.000    0.001    0.000 {method 'add' of 'set' objects}
      182    0.000    0.000    0.000    0.000 {method 'join' of 'str' objects}
        1    0.000    0.000    0.000    0.000 fileinput.py:80(<module>)
        1    0.000    0.000    0.000    0.000 fileinput.py:197(__init__)
        1    0.000    0.000    0.000    0.000 fileinput.py:266(nextfile)
        1    0.000    0.000    0.000    0.000 {isinstance}
        1    0.000    0.000    0.000    0.000 fileinput.py:91(input)
        1    0.000    0.000    0.000    0.000 fileinput.py:184(FileInput)
        1    0.000    0.000    0.000    0.000 fileinput.py:240(__iter__)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

Это, кажется, не говорит мне ничего полезного. Подавляющее большинство времени просто перечисляется как:

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1   31.980   31.980   32.133   32.133 my_script.py:18(<module>)

В my_script.py строка 18 представляет собой не что иное, как закрытие """ комментария блока заголовка файла, так что это не означает, что весь объем работы сосредоточен в строке 18. script в целом в основном состоящий из линейной обработки, в основном с разделением строк, сортировкой и настройкой, поэтому я ожидал, что большую часть времени вы перейдете к одному или нескольким из этих действий. Как бы то ни было, все время, сгруппированные в результатах cProfile, встречающиеся на линии комментариев, не имеют никакого смысла или, по крайней мере, не проливают свет на то, что на самом деле потребляет все время.

EDIT: Я построил минимальный рабочий пример, похожий на мой вышеприведенный случай, чтобы продемонстрировать одно и то же поведение:

mwe.py

import fileinput

for line in fileinput.input():
    for i in range(10):
        y = int(line.strip()) + int(line.strip())

И назовите его с помощью:

perl -e 'for(1..1000000){print "$_\n"}' | python -m cProfile -s time mwe.py

Чтобы получить результат:

         22002536 function calls (22001694 primitive calls) in 9.433 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    8.004    8.004    9.433    9.433 mwe.py:1(<module>)
 20000000    1.021    0.000    1.021    0.000 {method 'strip' of 'str' objects}
  1000001    0.270    0.000    0.301    0.000 fileinput.py:243(next)
  1000000    0.107    0.000    0.107    0.000 {range}
      842    0.024    0.000    0.024    0.000 {method 'readlines' of 'file' objects}
 1684/842    0.007    0.000    0.032    0.000 fileinput.py:292(readline)
        1    0.000    0.000    0.000    0.000 fileinput.py:80(<module>)
        1    0.000    0.000    0.000    0.000 fileinput.py:91(input)
        1    0.000    0.000    0.000    0.000 fileinput.py:197(__init__)
        1    0.000    0.000    0.000    0.000 fileinput.py:184(FileInput)
        1    0.000    0.000    0.000    0.000 fileinput.py:266(nextfile)
        1    0.000    0.000    0.000    0.000 {isinstance}
        1    0.000    0.000    0.000    0.000 fileinput.py:240(__iter__)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

Я как-то неправильно использую cProfile?

Ответы

Ответ 1

Как я упоминал в комментарии, когда вы не можете заставить cProfile работать извне, вы можете часто использовать его внутри. Это не так сложно.

Например, когда я запускаю с -m cProfile в моем Python 2.7, я получаю те же результаты, что и вы. Но когда я вручную использую вашу примерную программу:

import fileinput
import cProfile

pr = cProfile.Profile()
pr.enable()
for line in fileinput.input():
    for i in range(10):
        y = int(line.strip()) + int(line.strip())
pr.disable()
pr.print_stats(sort='time')

... вот что я получаю:

         22002533 function calls (22001691 primitive calls) in 3.352 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
 20000000    2.326    0.000    2.326    0.000 {method 'strip' of 'str' objects}
  1000001    0.646    0.000    0.700    0.000 fileinput.py:243(next)
  1000000    0.325    0.000    0.325    0.000 {range}
      842    0.042    0.000    0.042    0.000 {method 'readlines' of 'file' objects}
 1684/842    0.013    0.000    0.055    0.000 fileinput.py:292(readline)
        1    0.000    0.000    0.000    0.000 fileinput.py:197(__init__)
        1    0.000    0.000    0.000    0.000 fileinput.py:91(input)
        1    0.000    0.000    0.000    0.000 {isinstance}
        1    0.000    0.000    0.000    0.000 fileinput.py:266(nextfile)
        1    0.000    0.000    0.000    0.000 fileinput.py:240(__iter__)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

Это намного полезнее: он говорит вам, что вы, вероятно, уже ожидали, что более половины вашего времени потрачено на вызов str.strip().


Также обратите внимание, что если вы не можете отредактировать файл, содержащий код, который вы хотите профилировать (mwe.py), вы всегда можете сделать это:

import cProfile
pr = cProfile.Profile()
pr.enable()
import mwe
pr.disable()
pr.print_stats(sort='time')

Даже это не всегда работает. Если ваша программа вызывает exit(), например, вам придется использовать обертку try:/finally: и/или atexit. И это он называет os._exit(), или segfaults, вы, вероятно, полностью hosed. Но это не очень распространено.


Однако, что-то, что я обнаружил позже: если вы переместите весь код из глобальной области видимости, -m cProfile, похоже, работает, по крайней мере, для этого случая. Например:

import fileinput

def f():
    for line in fileinput.input():
        for i in range(10):
            y = int(line.strip()) + int(line.strip())
f()

Теперь вывод из -m cProfile включает, среди прочего:

2000000 4,819 0,000 4,819 0,000: 0 (полоса)  100001 0.288 0.000 0.295 0.000 fileinput.py:243(next)

Я понятия не имею, почему это также делало его в два раза медленнее... или, может быть, это просто эффект кеша; это было несколько минут с тех пор, как я в последний раз его запускал, и я много раз просматривал веб-страницы. Но это не важно, важно то, что большую часть времени взимается с разумных мест.

Но если я изменю это, чтобы переместить внешний цикл на глобальный уровень, а только его тело в функцию, большую часть времени снова исчезает.


Другая альтернатива, которую я бы не предложил, кроме как в крайнем случае...

Я замечаю, что если я использую профиль вместо cProfile, он работает как внутри, так и снаружи, время зарядки до нужных вызовов. Однако эти вызовы также примерно в 5 раз медленнее. И, похоже, дополнительные 10 секунд постоянных накладных расходов (которые заряжаются до import profile, если они используются внутри, независимо от того, что включено в строке 1, если они используются снаружи). Итак, чтобы узнать, что split использует 70% моего времени, вместо того, чтобы ждать 4 секунды и делать 2.326/3.352, я должен ждать 27 секунд и делать 10.93/(26.34 - 10.01). Не очень весело...


Последнее: я получаю те же результаты с результатами построения CPython 3.4 dev, когда они используются внутри, и все они загружаются в первую строку кода при использовании извне. Но PyPy 2.2/2.7.3 и PyPy3 2.1b1/3.2.3 оба, кажется, дают мне правильные результаты с -m cProfile. Это может означать, что PyPy cProfile подделывается поверх profile, потому что код с чистым Python достаточно быстр.


В любом случае, если кто-то может понять/объяснить, почему -m cProfile не работает, это было бы здорово... но в остальном это обычно отлично подходит для решения проблемы.