PythonでPerfをとってみた [リベンジ]

Python3.12から、PythonスクリプトでPerfが取れるようになったそうなので試してみました。[^1][^2]

https://docs.python.org/3/howto/perf_profiling.html#python-support-for-the-linux-perf-profiler

:::note info
[元記事](https://qiita.com/joule/items/5d247a5eb21b34431cff)についてコメントを頂き、ミスが分かったため改版しました。どうもありがとうございました。(2024/7/14)
- Pyenvの導入について (y-vectorfieldさん)
- Pythonのビルドフラグと環境変数の設定 (dameyodamedameさん)
:::

### 事前準備

(1) PythonがLinux perfに対応しているかを確認します。`sysconfig`の`PY_HAVE_PERF_TRAMPOLINE`が設定されていれば良いようです。[^1]

```console
$ python -m sysconfig | grep HAVE_PERF_TRAMPOLINE
  PY_HAVE_PERF_TRAMPOLINE = "1"
```

(2) CPythonのビルドオプションに`-fno-omit-frame-pointer`, `-mno-omit-leaf-frame-pointer`があったかを確認します。これは、perfが解析に利用するフレームポインタを保持するためのビルドオプション(gcc)のようです。

```console
$ python -m sysconfig | grep 'no-omit-frame-pointer'
	CFLAGS = "-fno-strict-overflow -DNDEBUG -g -O3 -Wall -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer"
```

(3) ユーザー権限でカーネル解析ができるようにシステムの`kptr_restrict`と`perf_event_paranoid`のレベルを変更しておきます。

```console
$ echo 0 | sudo tee  /proc/sys/kernel/kptr_restrict > /dev/null
$ echo 0 | sudo tee  /proc/sys/kernel/perf_event_paranoid > /dev/null
```

---

筆者の環境ではPythonが上記(1), (2)に対応していませんでした。
対応手順をpyenvで行うことにしました。[^3]

```console: pythonのインストール・確認
$ export PYTHON_CFLAGS='-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer'
$ pyenv install 3.12.4
$ pyenv local 3.12.4
$ python --version
Python 3.12.4
$ python -m sysconfig | grep HAVE_PERF_TRAMPOLINE
PY_HAVE_PERF_TRAMPOLINE = "1"
$ python -m sysconfig | grep 'no-omit-frame-pointer'
CFLAGS = "-fno-strict-overflow -DNDEBUG -g -O3 -Wall -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer"
```

### perfの取得

サンプルコード[^1]でコールグラフを取得してみました。

```python: example.py
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(10000000)
```

元記事のコメントで頂いたとおり、`perf record`では`PYTHONPERFSUPPORT=1`を環境変数で指定する必要がありました。[^3]
この環境変数は実行時にこの機能をアクティベートするための条件になっているようです。[^4][^5] 

```console: perf record
$ PYTHONPERFSUPPORT=1 perf record -F 9999 -g -o perf.data python example.py
[ perf record: Woken up 4 times to write data ]
[ perf record: Captured and wrote 0.944 MB perf.data (4448 samples) ]
```

```console: perf report
$ perf report --stdio -g -n 
:
# Children      Self       Samples  Command   Shared Object         Symbol                                                                        
# ........  ........  ............  ........  ....................  ..............................................................................
#
    92.93%     0.00%             0  python    libc.so.6             [.] __libc_start_call_main
            |
            ---__libc_start_call_main
               Py_BytesMain
               |          
               |--91.32%--Py_RunMain
               |          |          
               |           --90.96%--pymain_run_python.constprop.0
               |                     |          
               |                      --90.94%--_PyRun_AnyFileObject
               |                                _PyRun_SimpleFileObject
               |                                |          
               |                                 --90.91%--run_mod
               |                                           |          
               |                                            --90.89%--run_eval_code_obj
               |                                                      PyEval_EvalCode
               |                                                      py::<module>:.../example.py
               |                                                      _PyEval_EvalFrameDefault
               |                                                      |          
               |                                                       --90.87%--PyObject_Vectorcall
               |                                                                 py::baz:.../example.py
               |                                                                 _PyEval_EvalFrameDefault
               |                                                                 PyObject_Vectorcall
               |                                                                 py::bar:.../example.py
               |                                                                 _PyEval_EvalFrameDefault
               |                                                                 PyObject_Vectorcall
               |                                                                 py::foo:.../example.py
               |                                                                 |          
               |                                                                 |--81.36%--_PyEval_EvalFrameDefault

:
```

コールグラフが取得できているようでした。（関数のトレース情報を含む`/tmp/perf-<PID>.map`も残っていました）

---

#### 環境

CPU: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz
OS: Ubuntu-22.04
Kernel: 6.5.0-41-generic
Pyenv: 2.4.7
Python: 3.12.4

[^1]: https://docs.python.org/3/howto/perf_profiling.html#python-support-for-the-linux-perf-profiler
[^2]: https://github.com/python/cpython/issues/96143
[^3]: https://realpython.com/python312-perf-profiler/#install-python-312-using-pyenv
[^4]: https://github.com/python/cpython/blob/v3.12.4/Python/initconfig.c#L152
[^5]: https://github.com/python/cpython/blob/v3.12.4/Python/perf_trampoline.c#L427