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
将无法像它希望的那样频繁地采样进程,因为开销会更高。