Python 對 Linux perf 分析器的支援

作者:

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 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

如何獲得最佳結果

為了獲得最佳結果,Python 應該使用 CFLAGS="-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer" 進行編譯,因為這允許分析器僅使用幀指標而不是 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 -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 中),然後建立 perf.jit.data,將所有 JIT 資訊合併在一起。它還應該在當前目錄中建立許多 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)通常提供更可靠的堆疊回溯