Python support for the Linux perf profiler¶
- 作者:
Pablo Galindo
The Linux perf profiler
is a very powerful tool that allows you to profile and obtain
information about the performance of your application.
perf also has a very vibrant ecosystem of tools
that aid with the analysis of the data that it produces.
The main problem with using the perf profiler with Python applications is that
perf only gets information about native symbols, that is, the names of
functions and procedures written in C. This means that the names and file names
of Python functions in your code will not appear in the output of perf.
Since Python 3.12, the interpreter can run in a special mode that allows Python
functions to appear in the output of the perf profiler. When this mode is
enabled, the interpreter will interpose a small piece of code compiled on the
fly before the execution of every Python function and it will teach perf the
relationship between this piece of code and the associated Python function using
perf map files.
备注
Support for the perf profiler is currently only available for Linux on
select architectures. Check the output of the configure build step or
check the output of python -m sysconfig | grep HAVE_PERF_TRAMPOLINE
to see if your system is supported.
例如,考虑以下脚本:
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 perf record -F 9999 -g -o perf.data python my_script.py
$ perf report -g -i perf.data
示例,使用 -X 选项:
$ perf record -F 9999 -g -o perf.data python -X perf my_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
如何获取最佳结果¶
For best results, Python should be compiled with
CFLAGS="-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer" as this allows
profilers to unwind using only the frame pointer and not on DWARF debug
information. This is because as the code that is interposed to allow perf
support is dynamically generated it doesn't have any DWARF debugging information
available.
你可以通过运行以下命令来检查你的系统是否附带此标志来编译的:
$ 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 工具的一个程序错误,只有 perf 版本号高于 v6.8 才能使用 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 -k 1 --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 中),然后创建将所有 JIT 信息合并到一起的 perf.jit.data 文件。 它还会在当前目录下创建大量 jitted-XXXX-N.so 文件,它们是 Python 所创建的所有 JIT 跳板的 ELF 映像。
警告
当使用 --call-graph dwarf 时,perf 工具将对被分析进程的栈打快照并将信息保存在 perf.data 文件中。在默认情况下,栈转储的大小为 8192 字节,但你可以通过额外传入一个逗号加数值如 --call-graph dwarf,16384 来改变这个大小。
栈转储的大小很重要,因为如果这个值太小 perf 将无法展开栈信息,而输出将不完整。另一方面,如果这个值太大,那么 perf 将无法按需以足够的频率对进程执行采样,因为那样资源开销会过高。
栈大小在对使用较低优化级别 (如 -O0) 编译的 Python 代码进行性能分析时更为重要,因为这类构建版往往有更大的栈帧。如果你是使用 -O0 来编译 Python 并且没有在你的性能分析输出中看到 Python 函数,请尝试将栈转储大小增加到 65528 字节 (最大值):
$ perf record -F 9999 -g -k 1 --call-graph dwarf,65528 -o perf.data python -Xperf_jit my_script.py
不同的编译标志可能显著地影响栈大小:
使用
-O0构建通常会比使用-O1或更高级别的构建具有大得多的栈帧添加优化 (
-O1,-O2等) 通常会减小栈大小帧指针 (
-fno-omit-frame-pointer) 通常会提供更可靠的栈展开