Linux perf 分析器对 Python 的支持

作者:

Pablo Galindo

Linux perf 分析器 是一款非常强大的工具,允许您分析和获取有关应用程序性能的信息。 perf 还拥有一个非常活跃的工具生态系统,可以帮助分析它生成的数据。

使用 perf 分析器分析 Python 应用程序的主要问题是,perf 只能获取有关原生符号的信息,即用 C 语言编写的函数和过程的名称。这意味着您的代码中 Python 函数的名称和文件名不会出现在 perf 的输出中。

从 Python 3.12 开始,解释器可以在一种特殊模式下运行,允许 Python 函数出现在 perf 分析器的输出中。启用此模式后,解释器将在执行每个 Python 函数之前插入一小段代码,并使用 perf 映射文件 教会 perf 这段代码与相关 Python 函数之间的关系。

注意

目前,perf 分析器的支持仅适用于某些架构上的 Linux。检查 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 次的频率采样 CPU 堆栈跟踪

$ 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 函数没有显示在输出中,只有 _PyEval_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 分析支持

perf 性能分析支持可以通过环境变量 PYTHONPERFSUPPORT-X perf 选项在启动时启用,也可以使用 sys.activate_stack_trampoline()sys.deactivate_stack_trampoline() 动态启用。

sys 函数优先于 -X 选项,-X 选项优先于环境变量。

例如,使用环境变量

$ PYTHONPERFSUPPORT=1 python script.py
$ perf report -g -i perf.data

例如,使用 -X 选项

$ python -X perf script.py
$ perf report -g -i perf.data

例如,在文件 example.py 中使用 sys API

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'

如果您没有看到任何输出,则意味着您的解释器没有使用帧指针编译,因此可能无法在 perf 的输出中显示 Python 函数。