Сообщить об ошибке.

Поддержка Python Linux perf profiler

Поддержка Python профилировщика производительности Linux

Новое в Python 3.12

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

Основная проблема при использовании Linux perf profiler с приложениями Python заключается в том, что perf получает информацию только о собственных символах, то есть именах функций и процедур, написанных на C. Это означает, что имена функций Python и имена файлов в коде не будут отображаться в выходных данных perf.

Начиная с Python 3.12, интерпретатор может запускаться в специальном режиме, который позволяет функциям Python отображаться в выходных данных perf profiler. Когда этот режим включен, то интерпретатор вставляет небольшой фрагмент кода, скомпилированный "на лету", перед выполнением каждой функции Python и обучает perf взаимосвязи между этим фрагментом кода и соответствующей функцией Python, используя файлы perf map.

Примечание. Поддержка профилировщика производительности в настоящее время доступна только для Linux на некоторых архитектурах. Чтобы узнать, поддерживается ли система Linux perf profiler необходимо проверить выходные данные этапа сборки configure или выходные данные команды python -m sysconfig | grep HAVE_PERF_TRAMPOLINE.

Например, рассмотрим следующий сценарий:

def foo(n):
    result = 0
    for _ in range(n):
        result += 1
    return result

def bar(n):
    foo(n)

def baz(n):
    bar(n)

if __name__ == "__main__":
    baz(1000000)

Запустим perf для выборки трассировки стека ЦП на частоте 9999 Гц:

$ perf record -F 9999 -g -o perf.data python my_script.py

Затем используем perf report для анализа данных:

$ perf report --stdio -n -g

# Children      Self       Samples  Command     Shared Object       Symbol
# ........  ........  ............  ..........  ..................  ..........................................
# 
#     91.08%     0.00%             0  python.exe  python.exe          [.] _start
#             |
#             ---_start
#             |
#                 --90.71%--__libc_start_main
#                         Py_BytesMain
#                         |
#                         |--56.88%--pymain_run_python.constprop.0
#                         |          |
#                         |          |--56.13%--_PyRun_AnyFileObject
#                         |          |          _PyRun_SimpleFileObject
#                         |          |          |
#                         |          |          |--55.02%--run_mod
#                         |          |          |          |
#                         |          |          |           --54.65%--PyEval_EvalCode
#                         |          |          |                     _PyEval_EvalFrameDefault
#                         |          |          |                     PyObject_Vectorcall
#                         |          |          |                     _PyEval_Vector
#                         |          |          |                     _PyEval_EvalFrameDefault
#                         |          |          |                     PyObject_Vectorcall
#                         |          |          |                     _PyEval_Vector
#                         |          |          |                     _PyEval_EvalFrameDefault
#                         |          |          |                     PyObject_Vectorcall
#                         |          |          |                     _PyEval_Vector
#                         |          |          |                     |
#                         |          |          |                     |--51.67%--_PyEval_EvalFrameDefault
#                         |          |          |                     |          |
#                         |          |          |                     |          |--11.52%--_PyLong_Add
#                         |          |          |                     |          |          |
#                         |          |          |                     |          |          |--2.97%--_PyObject_Malloc

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

Проведем тот же эксперимент с включенной поддержкой perf:

$ perf report --stdio -n -g

# Children      Self       Samples  Command     Shared Object       Symbol
# ........  ........  ............  ..........  ..................  .....................................................................
# 
#     90.58%     0.36%             1  python.exe  python.exe          [.] _start
#             |
#             ---_start
#             |
#                 --89.86%--__libc_start_main
#                         Py_BytesMain
#                         |
#                         |--55.43%--pymain_run_python.constprop.0
#                         |          |
#                         |          |--54.71%--_PyRun_AnyFileObject
#                         |          |          _PyRun_SimpleFileObject
#                         |          |          |
#                         |          |          |--53.62%--run_mod
#                         |          |          |          |
#                         |          |          |           --53.26%--PyEval_EvalCode
#                         |          |          |                     py::<module>:/src/script.py
#                         |          |          |                     _PyEval_EvalFrameDefault
#                         |          |          |                     PyObject_Vectorcall
#                         |          |          |                     _PyEval_Vector
#                         |          |          |                     py::baz:/src/script.py
#                         |          |          |                     _PyEval_EvalFrameDefault
#                         |          |          |                     PyObject_Vectorcall
#                         |          |          |                     _PyEval_Vector
#                         |          |          |                     py::bar:/src/script.py
#                         |          |          |                     _PyEval_EvalFrameDefault
#                         |          |          |                     PyObject_Vectorcall
#                         |          |          |                     _PyEval_Vector
#                         |          |          |                     py::foo:/src/script.py
#                         |          |          |                     |
#                         |          |          |                     |--51.81%--_PyEval_EvalFrameDefault
#                         |          |          |                     |          |
#                         |          |          |                     |          |--13.77%--_PyLong_Add
#                         |          |          |                     |          |          |
#                         |          |          |                     |          |          |--3.26%--_PyObject_Malloc

Как включить поддержку профилирования производительности

Поддержка профилирования perf может быть включена либо с самого начала, используя переменную окружения PYTHONPERFSUPPORT или параметр командной строки Python -X perf, либо динамически, используя sys.activate_stack_trampoline() и sys.deactivate_stack_trampoline().

Функции модуля sys имеют приоритет над параметром -X, параметр -X имеет приоритет над переменной окружения PYTHONPERFSUPPORT.

Пример использования переменной окружения:

$ PYTHONPERFSUPPORT=1 python script.py
# смотрим выходные данные
$ perf report -g -i perf.data

Пример использования параметра командной строки Python -X:

$ python -X perf script.py
# смотрим выходные данные
$ perf report -g -i perf.data

Пример использования sys API в файле example.py:

import sys

sys.activate_stack_trampoline("perf")
do_profiled_stuff()
sys.deactivate_stack_trampoline()

non_profiled_stuff()

Затем смотрим выходные данные:

$ python ./example.py
$ perf report -g -i perf.data

Как получить наилучшие результаты

Для достижения наилучших результатов, Python следует компилировать с CFLAGS="-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer", т.к. это позволяет профилировщикам разматывать стек, используя только указатель фрейма, а не отладочную информацию DWARF. Это связано с тем, что код, который вставляется для обеспечения поддержки perf, генерируется динамически, в нем нет никакой доступной информации об отладке DWARF.

Можете проверить, была ли система скомпилирована с этим флагом, выполнив:

$ python -m sysconfig | grep 'no-omit-frame-pointer'

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