Документация Python неофициальный перевод
Содержание страницы

26.3. Профилировщики PythonThe Python Profilers

Исходный код: Lib/profile.py и Lib/pstats.py


26.3.1. Введение в профилировщикиIntroduction to the profilers

Профилировщик (profiler) – это программа, которая описывает производительность программы во время выполнения, предоставляя разнообразную статистику. В этой документации описывается функциональность профилировщика, предоставляемая модулями cProfile, profile и pstats. Этот профилировщик предоставляет детерминированное профилирование программ на Python. Он также предоставляет набор инструментов для создания отчетов, позволяющих пользователям быстро просматривать результаты профилирования.

Стандартная библиотека Python предоставляет два различных профилировщика:

  1. Большинству пользователей рекомендуется cProfile; это расширение на C с умеренными накладными расходами, что делает его пригодным для профилирования долго работающих программ. Основан на lsprof, предоставленном Бреттом Розеном и Тедом Цоттером.
  2. profile – чисто Python-модуль, чей интерфейс имитируется модулем cProfile. Добавляет значительные накладные расходы в профилируемые программы. Если вы пытаетесь каким-либо образом расширить профилировщик, эта задача может оказаться проще с этим модулем.

Модули profile и cProfile экспортируют одинаковый интерфейс, поэтому они в основном взаимозаменяемы; cProfile имеет гораздо меньшие накладные расходы, но он новее и может быть доступен не на всех системах. cProfile на самом деле является уровнем совместимости поверх внутреннего модуля _lsprof.

Примечание

Модули профилировщиков предназначены для получения профиля выполнения данной программы, а не для целей тестирования производительности (для этого существует timeit для достаточно точных результатов). Это особенно касается сравнения производительности кода Python с кодом C: профилировщики вносят накладные расходы для кода Python, но не для функций уровня C, поэтому код C будет казаться быстрее любого кода Python.

26.3.2. Краткое руководство пользователяInstant User’s Manual

Этот раздел предназначен для пользователей, которые «не хотят читать руководство». В нём даётся очень краткий обзор, и он позволяет быстро выполнить профилирование существующего приложения.

Чтобы профилировать приложение с главной точкой входа foo(), вы должны добавить следующее в ваш модуль:

import cProfile
cProfile.run('foo()')

(Используйте profile вместо cProfile, если последний недоступен в вашей системе.)

Вышеуказанное действие приведет к запуску foo() и выводу ряда информационных строк (профиля). Вышеописанный подход наиболее полезен при работе с интерпретатором. Если вы хотите сохранить результаты профилирования в файл для последующего просмотра, вы можете указать имя файла в качестве второго аргумента функции run():

import cProfile
cProfile.run('foo()', 'fooprof')

Файл cProfile.py также можно вызвать как скрипт для профилирования другого скрипта. Например:

python -m cProfile myscript.py

cProfile.py принимает два необязательных аргумента в командной строке:

cProfile.py [-o output_file] [-s sort_order]

-s применяется только к стандартному выводу (если не указан -o). Обратитесь к документации Stats для получения допустимых значений сортировки.

Когда вы хотите просмотреть профиль, следует использовать методы модуля pstats. Обычно данные статистики загружаются следующим образом:

import pstats
p = pstats.Stats('fooprof')

Класс Stats (приведенный выше код только что создал экземпляр этого класса) имеет множество методов для обработки и вывода данных, которые были только что прочитаны в p. Когда вы запустили cProfile.run() выше, выведенный результат представлял собой результат трех вызовов методов:

p.strip_dirs().sort_stats(-1).print_stats()

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

p.sort_stats('name')
p.print_stats()

Первый вызов сортирует список по имени функции, а второй вызов выводит статистику. Вот несколько интересных вызовов для экспериментов:

p.sort_stats('cumulative').print_stats(10)

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

Чтобы увидеть, какие функции много циклится и занимают много времени, можно сделать:

p.sort_stats('time').print_stats(10)

для сортировки по времени, затраченному внутри каждой функции, и вывода статистики для десяти первых функций.

Можно также попробовать:

p.sort_stats('file').print_stats('__init__')

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

p.sort_stats('time', 'cum').print_stats(.5, 'init')

Эта строка сортирует статистику по первичному ключу – времени, и вторичному ключу – совокупному времени, а затем выводит часть статистики. А именно: сначала список уменьшается до 50% (ср. .5) от исходного размера, затем сохраняются только строки, содержащие init, и выводится этот под-под-список.

Если вам интересно, какие функции вызывали вышеупомянутые функции, теперь (p всё ещё отсортирован по последнему критерию) можно выполнить:

p.print_callers(.5, 'init')

и будет получен список вызывающих для каждой из перечисленных функций.

Для более широких возможностей придётся прочитать руководство или догадаться, что делают следующие функции:

p.print_callees()
p.add('fooprof')

При запуске в качестве скрипта модуль pstats представляет собой браузер статистики для чтения и изучения дампов профилирования. Он имеет простой построчный интерфейс (реализованный с помощью cmd) и интерактивную справку.

26.3.3. Что такое детерминированное профилирование?What Is Deterministic Profiling?

Deterministic profiling is meant to reflect the fact that all function call, function return, and exception events are monitored, and precise timings are made for the intervals between these events (during which time the user’s code is executing). In contrast, statistical profiling (which is not done by this module) randomly samples the effective instruction pointer, and deduces where time is being spent. The latter technique traditionally involves less overhead (as the code does not need to be instrumented), but provides only relative indications of where time is being spent.

В Python, поскольку во время выполнения активен интерпретатор, для детерминированного профилирования не требуется наличие инструментированного кода. Python автоматически предоставляет ловушку (необязательный колбэк) для каждого события. Кроме того, интерпретируемая природа Python обычно вносит так много накладных расходов на выполнение, что детерминированное профилирование в типичных приложениях добавляет лишь небольшие дополнительные накладные расходы. В результате детерминированное профилирование не так дорого, но при этом предоставляет обширную статистику времени выполнения программы на Python.

Статистику количества вызовов можно использовать для выявления ошибок в коде (неожиданные количества) и для определения возможных точек встраивания (высокое количество вызовов). Статистику внутреннего времени можно использовать для выявления «горячих циклов», которые следует тщательно оптимизировать. Статистику суммарного времени следует использовать для выявления высокоуровневых ошибок в выборе алгоритмов. Обратите внимание, что нестандартная обработка суммарного времени в этом профилировщике позволяет напрямую сравнивать статистику рекурсивных реализаций алгоритмов с итеративными.

26.3.4. Справочное руководство – profile и cProfileReference Manual – profile and cProfile

Основная точка входа для профилировщика – глобальная функция profile.run() (соотв. cProfile.run()). Обычно она используется для создания информации профилирования. Отчеты форматируются и выводятся с помощью методов класса pstats.Stats. Ниже приводится описание всех этих стандартных точек входа и функций. Для более глубокого изучения некоторого кода рекомендуется прочитать раздел о расширениях профилировщика, в котором обсуждается, как вывести «лучшие» профилировщики из представленных классов, или прочитать исходный код этих модулей.

cProfile.run(command, filename=None, sort=-1)

Эта функция принимает один аргумент, который можно передать функции exec(), и необязательное имя файла. Во всех случаях эта процедура пытается выполнить exec() свой первый аргумент и собирает статистику профилирования из выполнения. Если имя файла не указано, эта функция автоматически выводит простой отчет о профилировании, отсортированный по стандартной строке имени (файл/строка/имя-функции), которая представлена в каждой строке. Ниже приведен типичный вывод такого вызова:

      2706 function calls (2004 primitive calls) in 4.504 CPU seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     2    0.006    0.003    0.953    0.477 pobject.py:75(save_objects)
  43/3    0.533    0.012    0.749    0.250 pobject.py:99(evaluate)
 ...

Первая строка показывает, что было отслежено 2706 вызовов. Из этих вызовов 2004 были первичными. Мы определяем первичный как вызов, который не был вызван рекурсией. Следующая строка: Упорядочено по: стандартному имени, указывает, что текстовая строка в крайнем правом столбце использовалась для сортировки вывода. Заголовки столбцов включают:

ncalls
– количество вызовов,
tottime
общее время, затраченное в данной функции (исключая время, затраченное на вызовы подфункций),
percall
– результат деления tottime на ncalls
cumtime
общее время, затраченное в этой и всех подфункциях (от вызова до выхода). Это значение точное даже для рекурсивных функций.
percall
– результат деления cumtime на количество первичных вызовов
filename:lineno(function)
– предоставляет соответствующие данные каждой функции

Если в первом столбце два числа (например, 43/3), то последнее – это количество первичных вызовов, а первое – фактическое количество вызовов. Обратите внимание, что если функция не рекурсивна, эти два значения совпадают, и выводится только одно число.

Если указан аргумент sort, он может быть одним из значений, допустимых для параметра key из pstats.Stats.sort_stats().

cProfile.runctx(command, globals, locals, filename=None)

Эта функция аналогична run(), но с дополнительными аргументами для передачи словарей globals и locals для строки command.

Анализ данных профилировщика выполняется с помощью класса pstats.Stats.

class pstats.Stats(*filenames, stream=sys.stdout)

Конструктор этого класса создает экземпляр «объекта статистики» из filename (или набора имен файлов). Объекты Stats управляются методами для вывода полезных отчетов. Можно указать альтернативный выходной поток данных, передав именованный аргумент stream.

Файл, выбранный указанным конструктором, должен быть создан соответствующей версией profile или cProfile. Если конкретно, не гарантируется совместимость файлов с будущими версиями этого профилировщика, а также нет совместимости с файлами, созданными другими профилировщиками. Если предоставлено несколько файлов, вся статистика для одинаковых функций будет объединена, так что в одном отчете можно рассмотреть общую картину нескольких процессов. Если необходимо объединить дополнительные файлы с данными в существующем объекте Stats, можно использовать метод add().

26.3.4.1. Класс StatsThe Stats Class

Объекты Stats имеют следующие методы:

Stats.strip_dirs()

Этот метод класса Stats удаляет из имен файлов все сведения о пути. Он очень полезен для уменьшения размера вывода, чтобы он помещался (примерно) в 80 столбцов. Этот метод изменяет объект, и удаленная информация теряется. После выполнения операции удаления пути считается, что записи объекта находятся в «случайном» порядке, как сразу после инициализации и загрузки объекта. Если strip_dirs() приводит к тому, что два имени функции становятся неразличимыми (они находятся на одной строке того же файла и имеют одно и то же имя функции), то статистика для этих двух записей объединяется в одну запись.

Stats.add(*filenames)

Этот метод класса Stats добавляет дополнительные данные профилирования в текущий объект профилирования. Его аргументы должны ссылаться на файлы, созданные соответствующей версией profile.run() или cProfile.run(). Статистика для функций с одинаковыми именами (по файлу, строке, имени) автоматически объединяется в единую статистику функции.

Stats.dump_stats(filename)

Сохраняет данные, загруженные в объект Stats, в файл с именем filename. Файл создается, если его не существует, и перезаписывается, если он уже существует. Это эквивалентно одноименному методу классов profile.Profile и cProfile.Profile.

Stats.sort_stats(*keys)

Этот метод изменяет объект Stats, сортируя его в соответствии с переданными критериями. Аргумент обычно представляет собой строку, определяющую основу сортировки (например: 'time' или 'name').

Если указано более одного ключа, то дополнительные ключи используются как вторичные критерии, когда все выбранные перед ними ключи равны. Например, sort_stats('name', 'file') отсортирует все записи по имени функции, а все совпадения (одинаковые имена функций) разрешит сортировкой по имени файла.

Для любых имен ключей можно использовать сокращения, если они однозначны. Ниже перечислены ключи, определенные на данный момент:

Допустимый аргумент Значение
'calls' количество вызовов
'cumulative' совокупное время
'cumtime' совокупное время
'file' имя файла
'filename' имя файла
'module' имя файла
'ncalls' количество вызовов
'pcalls' количество примитивных вызовов
'line' номер строки
'name' имя функции
'nfl' имя/файл/строка
'stdname' стандартное имя
'time' внутреннее время
'tottime' внутреннее время

Обратите внимание, что все сортировки статистики выполняются в порядке убывания (наиболее затратные по времени элементы помещаются первыми), тогда как поиск по имени, файлу и номеру строки выполняется в порядке возрастания (алфавитном). Тонкое различие между 'nfl' и 'stdname' заключается в том, что стандартное имя – это сортировка по имени, как оно выводится, что приводит к странному сравнению встроенных номеров строк. Например, строки 3, 20 и 40 (если имена файлов одинаковы) будут расположены в строковом порядке: 20, 3 и 40. В отличие от этого, 'nfl' выполняет числовое сравнение номеров строк. Фактически, sort_stats('nfl') эквивалентно sort_stats('name', 'file', 'line').

По причинам обратной совместимости допускаются числовые аргументы -1, 0, 1 и 2. Они интерпретируются как 'stdname', 'calls', 'time' и 'cumulative' соответственно. Если используется этот старый числовой формат, будет использован только один ключ сортировки (числовой), а остальные аргументы будут молча проигнорированы.

Stats.reverse_order()

Этот метод класса Stats меняет порядок базового списка внутри объекта. Обратите внимание, что по умолчанию порядок (возрастающий или убывающий) выбирается автоматически на основе выбранного ключа сортировки.

Stats.print_stats(*restrictions)

Этот метод класса Stats выводит отчёт, как описано в определении profile.run().

Порядок вывода определяется последней операцией sort_stats(), выполненной над объектом (с учётом оговорок в add() и strip_dirs()).

Предоставленные аргументы (если есть) можно использовать для ограничения списка значимыми записями. Изначально список содержит полный набор профилированных функций. Каждое ограничение – это либо целое число (выбор определённого количества строк), либо десятичная дробь от 0.0 до 1.0 включительно (выбор процента строк), либо регулярное выражение (для сопоставления с выводимым стандартным именем; начиная с Python 1.5b1 используется синтаксис регулярных выражений в стиле Perl, определённый модулем re). Если указано несколько ограничений, они применяются последовательно. Например:

print_stats(.1, 'foo:')

сначала ограничит вывод первыми 10% списка, а затем выведет только те функции, которые входят в имя файла .*foo:. В отличие от этого, команда:

print_stats('foo:', .1)

ограничит список всеми функциями, имена файлов которых содержат .*foo:, а затем выведет только первые 10% из них.

Stats.print_callers(*restrictions)

Этот метод класса Stats выводит список всех функций, которые вызывали каждую функцию в профилированной базе данных. Порядок соответствует тому, что предоставляется методом print_stats(), и определение аргумента ограничения также идентично. Каждый вызывающий выводится на отдельной строке. Формат немного отличается в зависимости от профилировщика, создавшего статистику:

  • В случае profile число в скобках после каждого вызывающего показывает, сколько раз был сделан этот конкретный вызов. Для удобства второе число без скобок справа повторяет суммарное время, затраченное в функции.
  • В случае cProfile перед каждым вызывающим указывается три числа: количество раз, когда был сделан этот конкретный вызов, общее и суммарное время, затраченное в текущей функции, когда она была вызвана этим конкретным вызывающим.
Stats.print_callees(*restrictions)

Этот метод класса Stats выводит список всех функций, которые были вызваны указанной функцией. За исключением этого изменения направления вызовов (кто вызывает vs кого вызывают), аргументы и порядок идентичны методу print_callers().

26.3.5. ОграниченияLimitations

Одно ограничение связано с точностью информации о времени. Существует фундаментальная проблема с детерминированными профилировщиками, касающаяся точности. Наиболее очевидное ограничение состоит в том, что внутренние «часы» тикают с частотой (обычно) около 0,001 секунды. Следовательно, ни одно измерение не может быть точнее этих часов. Если сделать достаточно измерений, «ошибка» будет стремиться к усреднению. К сожалению, устранение этой первой ошибки приводит ко второму источнику ошибок.

The second problem is that it “takes a while” from when an event is dispatched until the profiler’s call to get the time actually gets the state of the clock. Similarly, there is a certain lag when exiting the profiler event handler from the time that the clock’s value was obtained (and then squirreled away), until the user’s code is once again executing. As a result, functions that are called many times, or call many functions, will typically accumulate this error. The error that accumulates in this fashion is typically less than the accuracy of the clock (less than one clock tick), but it can accumulate and become very significant.

Проблема более значима для profile, чем для менее нагруженного cProfile. По этой причине profile предоставляет средство калибровки под конкретную платформу, чтобы эту погрешность можно было вероятностно (в среднем) устранить. После калибровки профилировщик станет точнее (в смысле наименьших квадратов), но иногда будет выдавать отрицательные числа (когда количество вызовов исключительно мало, и боги вероятности играют против вас :-). Не стоит пугаться отрицательных чисел в профиле. Они должны только появляться, если вы откалибровали профилировщик, и результаты на самом деле лучше, чем без калибровки.

26.3.6. КалибровкаCalibration

Профилировщик модуля profile вычитает константу из времени обработки каждого события, чтобы компенсировать накладные расходы на вызов функции времени и сохранение результатов. По умолчанию константа равна 0. Следующая процедура позволяет получить более точную константу для конкретной платформы (см. обсуждение в разделе «Ограничения» выше).

import profile
pr = profile.Profile()
for i in range(5):
    print(pr.calibrate(10000))

Метод выполняет количество вызовов Python, заданное аргументом, напрямую и снова под профилировщиком, измеряя время для обоих случаев. Затем он вычисляет скрытые накладные расходы на каждое событие профилировщика и возвращает их как число с плавающей запятой. Например, на Pentium 800 МГц под Windows 2000 с использованием time.clock() в качестве таймера это магическое число составляет около 12.5e-6.

Цель этого упражнения – получить достаточно стабильный результат. Если ваш компьютер очень быстрый или функция таймера имеет низкое разрешение, возможно, придётся передать 100000 или даже 1000000, чтобы получить стабильные результаты.

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

import profile

# 1. Применить вычисленное смещение ко всем экземплярам Profile, создаваемым в дальнейшем.
profile.Profile.bias = your_computed_bias

# 2. Применить вычисленное смещение к конкретному экземпляру Profile.
pr = profile.Profile()
pr.bias = your_computed_bias

# 3. Указать вычисленное смещение в конструкторе экземпляра.
pr = profile.Profile(bias=your_computed_bias)

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

26.3.7. Расширения – создание более совершенных профилировщиковExtensions – Deriving Better Profilers

Класс Profile обоих модулей, profile и cProfile, был написан так, чтобы можно было разрабатывать производные классы, расширяющие профилировщик. Подробности здесь не описываются, поскольку для успешного выполнения этого требуется экспертное понимание внутренней работы класса Profile. Если вы хотите этим заняться, внимательно изучите исходный код модуля.

Если вы хотите только изменить способ определения текущего времени (например, чтобы принудительно использовать настенное время или затраченное процессорное время), передайте нужную функцию времени конструктору класса Profile:

pr = profile.Profile(your_time_func)

Полученный профилировщик будет вызывать your_time_func().

profile.Profile

your_time_func() должна возвращать одно число или список чисел, сумма которых даёт текущее время (как возвращает os.times()). Если функция возвращает одно число времени или список длины 2, то будет использована особенно быстрая версия диспетчера.

Имейте в виду, что следует откалибровать класс профилировщика для выбранной функции таймера. На большинстве машин таймер, возвращающий одно целое число, даёт наилучшие результаты с точки зрения низких накладных расходов при профилировании. (os.times() довольно плох, так как возвращает кортеж чисел с плавающей запятой). Если вы хотите заменить таймер наиболее чистым способом, создайте производный класс и жёстко пропишите замену метода диспетчера, который наилучшим образом обрабатывает ваш вызов таймера, вместе с соответствующей константой калибровки.

cProfile.Profile

your_time_func() должна возвращать одно число. Если она возвращает целые числа, можно также вызвать конструктор класса со вторым аргументом, указывающим реальную длительность одной единицы времени. Например, если your_integer_time_func() возвращает время, измеряемое в тысячных долях секунды, то экземпляр Profile создаётся следующим образом:

pr = profile.Profile(your_integer_time_func, 0.001)

Поскольку класс cProfile.Profile нельзя откалибровать, пользовательские функции таймера следует использовать с осторожностью, и они должны быть максимально быстрыми. Для достижения наилучших результатов с пользовательским таймером может потребоваться жёстко закодировать его в исходном коде C внутреннего модуля _lsprof.