ライブラリの最適化とベンチマーク
推奨読書: The Rust performance book
何を最適化するか
実際のコードで重要なものとして現れるコードを最適化することが推奨されます。
たとえば、Command::spawn の小さなアロケーションを削るよりも、[T]::sort を高速化するほうが有益です。
後者は syscall のコストが支配的だからです。
遅いライブラリコードに関する issue には I-slow T-libs、 コードサイズに関する issue には I-heavy T-libs というラベルが付けられています。
ベクトル化
現在、core と alloc ではベースラインのターゲット機能(例: x86_64-unknown-linux-gnu における SSE2)のみ使用できます。 これは、実行時の機能検出が std でしか利用できないためです。 可能な場合、ベクトル化を実現する推奨される方法は、コンパイラバックエンドの自動ベクトル化パスが理解できるようにコードを形作ることです。 これは、ユーザーの crate がジェネリックなライブラリ関数、たとえばイテレーターをインスタンス化する際に、追加のターゲット機能付きでコンパイルされる場合に利益をもたらします。
rustc-perf
標準ライブラリのうち rustc 自身で頻繁に使用される部分については、 ベンチマークサーバーを使用すると便利な場合があります。
これは crate のコンパイル時間のみを測定し、実行時性能は測定しないため、コンパイラで使用されない機能のベンチマークには使用できません。 たとえば、浮動小数点コード、リンクリスト、mpsc チャネルなどです。 それらについては、明示的なベンチマークを作成するか、実際のコードから抽出する必要があります。
組み込みマイクロベンチマーク
組み込みベンチマークは cargo bench を使用しており、
core と alloc については benches ディレクトリに、std については test モジュール内にあります。
ベンチマークは、ループの多数の反復にわたって実行時間を平均化するため、Bencher::iter によってループ内で自動的に実行されます。
CPU バウンドなマイクロベンチマークでは、単一イテレーションの実行時間はナノ秒からマイクロ秒の範囲に収まるべきです。
特定の は、rustc を再コンパイルせずに
./x bench library/<lib> --stage 0 --test-args <benchmark name> 経由で呼び出せます。
cargo bench は実時間を測定します。多くの場合これで十分ですが、大きな関数内で数命令を節約するような小さな変更は、
システムノイズに埋もれてしまうことがあります。そのような場合、次の変更により実行結果をより再現しやすくできます。
config.tomlでインクリメンタルビルドを無効にする- std とベンチマークを
RUSTFLAGS_BOOTSTRAP="-Ccodegen-units=1"でビルドする - システムを可能な限りアイドル状態にする
- ASLR を無効化する
- ベンチマークプロセスを特定のコアに固定する
- CPU のスケーリングガバナーを
固定周波数のもの(
performanceまたはpowersave)に変更する - 特にノート PC のような熱制限のあるシステムでは、 クロックブーストを無効化する
スタンドアロンテスト
x や cargo のベンチマークハーネスが邪魔になる場合は、ベンチマークを別の crate に抽出すると便利です。
たとえば、perf stat や cachegrind の下で実行する場合です。
標準ライブラリをビルドし、stage0-sysroot を rustup ツールチェーンとしてリンクし、それを使用して変更済みの標準ライブラリでスタンドアロンベンチマークをビルドします。
std の再ビルド時間が高速な反復には長すぎる場合、ベンチマークだけでなく、 テスト対象のコードも別の crate に抽出すると便利です。
perf-record の下での実行
コードを別の crate に抽出するのが現実的でない場合、まずベンチマークをビルドし、その後
perf record の下で再実行して、perf report でベンチマークカーネルまで掘り下げることができます。
# インライン化の変更とコードの並べ替えを減らすための 1CGU、ソース注釈用の debuginfo
$ export RUSTFLAGS_BOOTSTRAP="-Ccodegen-units=1 -Cdebuginfo=2"
# ベンチマークを実行せずにビルドする
$ ./x bench --stage 0 library/core/ --test-args skipallbenches
# perf の下でベンチマークを実行する
$ perf record --call-graph dwarf -e instructions ./x bench --stage 0 library/core/ --test-args <benchmark name>
$ perf report
perf.data の名前を変更して後続の実行で上書きされないようにしておくことで、後で
perf diff を使って、変更済みライブラリでの実行結果と比較できます。
アセンブリの比較
perf report はベンチマークコードのアセンブリを表示しますが、何が変わったのかをうまく把握するのが難しい場合があります。
特に複数のベンチマークが影響を受けた場合はそうです。代替手段として、ベンチマークスイートからアセンブリを直接抽出して diff できます。
# インライン化の変更とコードの並べ替えを減らすための 1CGU、ソース注釈用の debuginfo
$ export RUSTFLAGS_BOOTSTRAP="-Ccodegen-units=1 -Cdebuginfo=2"
# ベンチマークライブラリをビルドする
$ ./x bench --stage 0 library/core/ --test-args skipallbenches
# これにより、次のようなものが出力されるはずです
Running benches/lib.rs (build/x86_64-unknown-linux-gnu/stage0-std/x86_64-unknown-linux-gnu/release/deps/corebenches-2199e9a22e7b1f4a)
# すべてのベンチマークのアセンブリを取得する
$ objdump --source --disassemble --wide --no-show-raw-insn --no-addresses \
build/x86_64-unknown-linux-gnu/stage0-std/x86_64-unknown-linux-gnu/release/deps/corebenches-2199e9a22e7b1f4a \
| rustfilt > baseline.asm
# 変更を含むブランチに切り替える
$ git switch feature-branch
# 上記の手順を繰り返す
$ ./x bench ...
$ objdump ... > changes.asm
# 出力を比較する
$ kdiff3 baseline.asm changes.asm
これはスタンドアロンベンチマークにも適用できます。