Linux perf 分析器的 Python 支持

作者:

Pablo Galindo

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

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

从 Python 3.12 开始,解释器可以在特殊模式下运行,该模式允许 Python 函数出现在 perf 分析器的输出中。 启用此模式后,解释器将在每次执行 Python 函数之前插入一段即时编译的代码,并使用 perf 映射文件 将此代码片段与关联的 Python 函数之间的关系告知 perf

注意

目前,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 分析支持

可以使用环境变量 PYTHONPERFSUPPORT-X perf 选项从一开始就启用 perf 分析支持,也可以使用 sys.activate_stack_trampoline()sys.deactivate_stack_trampoline() 动态启用。

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

示例,使用环境变量

$ PYTHONPERFSUPPORT=1 perf record -F 9999 -g -o perf.data python script.py
$ perf report -g -i perf.data

示例,使用 -X 选项

$ perf record -F 9999 -g -o perf.data 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()

...然后

$ perf record -F 9999 -g -o perf.data python ./example.py
$ perf report -g -i perf.data

如何获得最佳结果

为了获得最佳结果,应该使用 CFLAGS="-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer" 编译 Python,因为这允许分析器仅使用帧指针进行展开,而不是使用 DWARF 调试信息。 这是因为用于允许 perf 支持的代码是动态生成的,因此没有任何可用的 DWARF 调试信息。

您可以通过运行以下命令来检查系统是否已使用此标志进行编译

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

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

如何在没有帧指针的情况下工作

如果您使用的是未编译帧指针的 Python 解释器,您仍然可以使用 perf 分析器,但开销会稍高,因为 Python 需要为每个 Python 函数调用动态生成展开信息。 此外,perf 将花费更多时间来处理数据,因为它需要使用 DWARF 调试信息来展开堆栈,这是一个缓慢的过程。

要启用此模式,您可以使用环境变量 PYTHON_PERF_JIT_SUPPORT-X perf_jit 选项,这将为 perf 分析器启用 JIT 模式。

注意

由于 perf 工具中的一个错误,只有高于 v6.8 的 perf 版本才能在 JIT 模式下工作。 该修复程序也已向后移植到 v6.7.2 版本的工具中。

请注意,在检查 perf 工具的版本时(可以通过运行 perf version 来完成),必须考虑到某些发行版会添加一些自定义版本号,其中包括 - 字符。 这意味着 perf 6.7-3 不一定是 perf 6.7.3

当使用 perf JIT 模式时,您需要在运行 perf report 之前执行一个额外的步骤。 您需要调用 perf inject 命令将 JIT 信息注入到 perf.data 文件中。

$ perf record -F 9999 -g --call-graph dwarf -o perf.data python -Xperf_jit my_script.py
$ perf inject -i perf.data --jit --output perf.jit.data
$ perf report -g -i perf.jit.data

或者使用环境变量

$ PYTHON_PERF_JIT_SUPPORT=1 perf record -F 9999 -g --call-graph dwarf -o perf.data python my_script.py
$ perf inject -i perf.data --jit --output perf.jit.data
$ perf report -g -i perf.jit.data

perf inject --jit 命令将读取 perf.data,自动选取 Python 创建的 perf 转储文件(位于 /tmp/perf-$PID.dump 中),然后创建 perf.jit.data,其中合并了所有 JIT 信息。 它还应该在当前目录中创建许多 jitted-XXXX-N.so 文件,这些文件是 Python 创建的所有 JIT trampoline 的 ELF 映像。

警告

请注意,当使用 --call-graph dwarf 时,perf 工具将拍摄正在分析的进程的堆栈快照,并将信息保存在 perf.data 文件中。 默认情况下,堆栈转储的大小为 8192 字节,但用户可以通过在逗号后传递大小来更改大小,例如 --call-graph dwarf,4096。 堆栈转储的大小很重要,因为如果大小太小,perf 将无法展开堆栈,并且输出将不完整。 另一方面,如果大小太大,则 perf 将无法像它希望的那样频繁地采样进程,因为开销会更高。