Rust: プロファイリング
概要
ソフトウェア開発におけるプロファイリング (profiling) とは、プログラムのボトルネックやメモリリークを発見することを目的とした CPU 時間やメモリ使用量の計測である。ソフトウェアの最適なパフォーマンスを引き出すためにはソースコードを最適に調整する作業が重要であるが、一般にプロファイリングはその優先順位や費用対効果を推定するための事前調査を目的として実施される。
Table of Contents
トレースプロファイラ (tracing profiler) またはイベントベースプロファイラ (event-based profiler) は特定のイベントに対するプロファイル情報を収集する。代表的な動作は関数ごとの開始/終了時間を計測するプロファイリングである。これは、直感的ではあるものの、対象のイベントが多くなるほど収集するデータ量も増大し、システムコールの多発により目的のプロファイリングに与える影響も大きくなる。
統計的プロファイラ (statistical profiler) はプログラム実行中に定期的にプログラムカウンターをサンプリングする。結果の数値は完全に正確ではないが、どの処理が全体のどれだけを占めているかを現実的な統計的近似値として示すことができる。トレースプロファイラに比べて収集するデータ量が少なく、フットプリントも小さいという利点がある。
perf
によるプロファイリング
perf
は Linux カーネルとともに開発されている Linux 向けの統計的プロファイラである。単独のコマンドのプロファイル情報のみならず、カーネルのシステムコールを含むシステム全体のプロファイル情報を収集することができる (コマンドがどのような言語やツールチェーンで作られているかには依存しない)。
Ubuntu 環境であれば apt 経由で perf
をインストールすることができる。libc6-dbg
は perf
の実行に必須ではないが、プロファイル情報に libc のシンボルが表示されるようになる。
torao@beryl:~$ uname -r
5.15.0-41-generic
torao@beryl:~$ sudo apt install linux-tools-generic
...
torao@beryl:~$ perf version
perf version 5.15.39
Windows や macOS で perf を使いたい場合は VirtualBox などの完全仮想化環境に構築した Linux で行う必要がある。これは、Windows の WSL 2 環境 (WSL 2 バックエンドの Docker を含む) や LinuxKit をコンテナサブシステムとする Docker 環境では Linux カーネルがハードウェアパフォーマンスカウンターをサポートしていないためである。
torao@lazurite:~$ uname -r
5.10.102.1-microsoft-standard-WSL2 ❌ Windows WSL2
root@e91231e975f2:/# uname -r
5.10.76-linuxkit ❌ Docker for macOS
pi@pirite:~ $ uname -r
5.15.21-v8+ ❌ Raspberry Pi OS
実行統計の表示
ここでは --release
オプション付きでビルドされた target/release/terp
というコマンドを例にプロファイリングの方法を説明する。繰り返しになるが perf
を使用するプロファイリングは Rust には依存しない。
torao@beryl:~/git/terp$ cargo clean
torao@beryl:~/git/terp$ cargo build --release
まず perf
を使ってプログラムの実行統計を調べてみよう。
torao@beryl:~/git/terp$ sudo perf stat -- target/release/terp
Performance counter stats for 'target/release/terp':
3.74 msec task-clock # 0.953 CPUs utilized
0 context-switches # 0.000 /sec
0 cpu-migrations # 0.000 /sec
161 page-faults # 43.075 K/sec
14,025,884 cycles # 3.753 GHz
26,601,658 instructions # 1.90 insn per cycle
5,210,002 branches # 1.394 G/sec
112,760 branch-misses # 2.16% of all branches
0.003921140 seconds time elapsed
0.000000000 seconds user
0.003958000 seconds sys
この実行統計を使用して、パフォーマンス調整の前後でコンテキストスイッチ回数や CPU サイクル数、ページフォルト回数の改善が見られたかを知ることができるだろう。
プロファイル情報の収集
プログラムのプロファイリングは perf record
で行う。ファイルを指定しなければプロファイル情報は perf.data
に保存される。
torao@beryl:~/git/terp$ sudo perf record -- target/release/terp
...
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.030 MB perf.data (228 samples) ]
torao@beryl:~/git/terp$ ls
total 100
...
-rw-rw-r-- 1 torao torao 1069 Jul 13 19:33 LICENSE
-rw------- 1 root root 43844 Jul 13 22:41 perf.data ✅
-rw-rw-r-- 1 torao torao 44 Jul 13 19:33 README.md
drwxrwxr-x 4 torao torao 4096 Jul 13 19:33 src/
drwxrwxr-x 3 torao torao 4096 Jul 13 22:40 target/
perf record
の実行オプションに -a
を指定すると、対象プログラムのバックグラウンドで動作しているシステム全体のプロファイル情報も含めて収集することができる。
perf
が収集するプロファイル情報は、どのような関数やシステムコールが実行されているかや、それらの実行時間が含まれている。このような情報はシステムに対する攻撃のヒントとなりうるため実行には特権モードが必要である (ただしカーネルパラメータを変更して特権モードなしで実行できるようにすることもできる)。
torao@beryl:~/git/terp$ perf record -- target/release/terp
Error:
Access to performance monitoring and observability operations is limited.
Consider adjusting /proc/sys/kernel/perf_event_paranoid setting to open
access to performance monitoring and observability operations for processes
without CAP_PERFMON, CAP_SYS_PTRACE or CAP_SYS_ADMIN Linux capability.
More information can be found at 'Perf events and tool security' document:
https://www.kernel.org/doc/html/latest/admin-guide/perf-security.html
perf_event_paranoid setting is 4:
-1: Allow use of (almost) all events by all users
Ignore mlock limit after perf_event_mlock_kb without CAP_IPC_LOCK
>= 0: Disallow raw and ftrace function tracepoint access
>= 1: Disallow CPU event access
>= 2: Disallow kernel profiling
To make the adjusted perf_event_paranoid setting permanent preserve it
in /etc/sysctl.conf (e.g. kernel.perf_event_paranoid = <setting>)
プロファイル情報の確認と表示
プロファイル情報を収集したらまず統計的に信用できる程度のサンプリングが行われたかを確認する必要がある。-n
オプション付きでレポートを表示してみよう。
torao@beryl:~/git/terp$ sudo perf report -n
デフォルトで perf.data
が読み込まれるが -i [file]
でファイルを指定することもできる。
ここで左上の Samples の数値に注意。Fig 1 の例は「プログラム実行中に 130 回のサンプリングを行い、うち 16 回が libc の _init_malloc
を実行中だった」ことを意味している。つまり Samples が少ないプロファイル情報は統計的に近似した結果を表していない可能性がある。この場合、-F max
オプションでカーネルで定義されている最大サンプリング頻度を指定したり、目的の処理をループ実行するなどプログラムを修正してプロファイリングをやり直す必要がある。
今回のプロファイリングに使用した Rust コードは libc のメモリの割当と開放に関係する処理が上位の多くを締めていることが分かる。
コールグラフ表示
パフォーマンス改善を行うためには枝葉の関数を知るだけでは不十分で、それらがどこから多く呼び出されているかも知る必要がある。これは呼び出し関係を含めたコールグラフで表示すると分かりやすい。
-g
オプションを使ってコールスタック情報をつけてプロファイル情報を収集するとコールグラフを表示することができる。
torao@beryl:~/git/terp$ sudo perf record -g -- target/release/terp
...
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.054 MB perf.data (252 samples) ]
先程と同様に perf report
で表示すると、左側に +
記号が表示され + で展開できるようになっていることが分かるだろう。
torao@beryl:~/git/terp$ sudo perf report
Flame Graph 表示
コマンドラインで表示するコールグラフは便利ではあるものの、全体を俯瞰してみるには Frame Graph のほうが向いている。perf record -g
で収集したプロファイリング情報を Flame Graph に変換するための Perl スクリプトをダウンロードする。
torao@beryl:~/git/terp$ wget https://raw.githubusercontent.com/brendangregg/FlameGraph/master/stackcollapse-perf.pl
torao@beryl:~/git/terp$ wget https://raw.githubusercontent.com/brendangregg/FlameGraph/master/flamegraph.pl
プロファイル情報をテキストデータに変換し、スクリプトで Frame Graph の SVG ファイルを作成する。
torao@beryl:~/git/terp$ sudo perf script | perl stackcollapse-perf.pl | perl flamegraph.pl --title "terp" > flamegraph_terp.svg
生成した SVG ファイルは Chrome などで表示することができる (この例の SVG ファイル)。カーソルを当てるとその処理が全体の何 % を締めているかが表示される。また各層をクリックして拡大表示することもできる。
より詳細なプロファイリング向けビルド
ここまでの例で挙げた方法は Rust 側でビルドオプションを変更する必要はなかった。しかし perf
でより詳細なプロファイル情報を収集しようとするとデバッグ情報付きでリリースビルドを行う必要が出てくるかもしれない。そのような場合、通常のリリースビルドとは異なる設定を使う必要があるため、git 開発を行っているのであればプロファイリング用の git ブランチを作成して作業することをお勧めする。
リリースビルドのバイナリにデバッグ情報を含むようにするには Cargo.toml
を修正して debug
に値を指定する。これによりリースビルドのデフォルトのプロファイル設定を上書きすることができる。
[profile.release]
debug = 1
上記の設定で --release
フラグを付けてバイナリをビルドするとビルド完了時のメッセージが relase [optimized + debuginfo]
に変わるだろう。
torao@beryl:~/git/terp$ cargo clean
torao@beryl:~/git/terp$ cargo build --release
Compiling terp v0.1.0 (/home/torao/git/terp)
Finished release [optimized + debuginfo] target(s) in 1.00s
このバイナリが問題なく実行できることも確認しておこう。
torao@beryl:~/git/terp$ target/release/terp
...