🍣
cargo check のパフォーマンスを計測しよう

myuon

myuon

2024年12月23日

# はじめに

弊社では複数ある web サービスのうち、Rust を採用しているサービスがあります。 これの開発時の cargo check が重いなあと以前から感じていたので、どこがボトルネックになっているのか調査したいと思い、パフォーマンスの計測の仕方について調べました。

# TL;DR

timings を使うと crate ごとのビルド時間がわかり、self-profile を使ったコンパイラのフェーズごとのボトルネックを調べることができます。

# スコープ

今回は cargo check のみを対象として測定することにしました。 rust-analyzer や cargo build などは対象とはしていません。

# 対象とするファイルの規模

対象となっているリポジトリに含まれる Rust コードは 600 ファイル、 80000 行程度です。

# timings による計測

cargo のビルド時間を見る方法でまず浮かぶのは timings を使った計測でしょう。 cargo check --timings で crate ごとのビルド(check)時間を見ることができます。

これを適用すると、以下のような結果が得られます。

UnitTotalCodegen
弊社サービス28.0s0.4s (1%)
diesel4.6s0.0s (1%)
chrono-tz2.7s0.0s (1%)
google_maps2.1s0.0s (1%)

ということで、素の状態でのビルドはほぼ弊社サービスが支配的となっていることがわかります。 また、開発時には依存ライブラリのビルドはほぼキャッシュされていることが予想できるため、やはり弊社サービスのビルド時間が開発者体験にダイレクトに影響を与えているのではないかという予想が立ちました。

そこで今度はより深ぼって、弊社サービスの cargo check 自体のパフォーマンスを計測する方法を調べました。

# self-profile による計測

Rust には self-profile というオプションがあり、これを使うとコンパイル時の trace を吐いてくれるようです。

https://doc.rust-lang.org/beta/unstable-book/compiler-flags/self-profile.html

self-profile の吐く結果はそのままだと読めないので、以下で提供されているツール群を使うと良いです。

self-profile を使った計測は以下のように行います。

RUSTFLAGS="-Zself-profile" cargo +nightly check
RUSTFLAGS="-Zself-profile" cargo +nightly clean -p my_crate
RUSTFLAGS="-Zself-profile" cargo +nightly check

内容としては、まず cargo check でビルドして、 cargo clean -p my_crate で当該 crate のキャッシュのみ削除し、再度 cargo check でビルドします。 これにより .mm_profdata なるファイルが作成されます。

これを上記の summarize で集計すると、以下のようなフェーズごとの結果が得られます。 (一部抜粋)

ItemSelf time% of total timeTimeItem countIncremental result hashing time
evaluate_obligation5.85s20.0175.96s30527535.86ms
typeck4.62s15.7979.50s2667778.19ms
type_op_prove_predicate3.70s12.6454.22s15979736.40ms
mir_borrowck2.42s8.28314.60s266774.18ms
normalize_canonicalized_projection_ty1.06s3.6391.38s300336.55ms
expand_proc_macro926.08ms3.167926.08ms14030.00ns
compare_impl_item737.40ms2.522966.19ms170811.83ms
mir_built629.82ms2.1541.54s26677144.08ms

Self time のところがそのフェーズにかかっている時間を表しています。 一番大きな evalulate_obligation が 5.85s かかっていることがわかります。しかしこれでも全体の 20%なので、どこかがとても大きなボトルネックというわけではなさそうです。

また、Chrome の profiler に読み込ませることができるフォーマットで吐いてくれる crox というツールもあるようですが、私の環境では Chrome は読んでくれませんでした(残念)。 flamegraph を吐いてくれるツールもあるので、こちらも使ってみると以下のようになります。

flamegraph

これをみると、型チェックと借用チェックが支配的であることがわかります。 cargo check でこれらが重くなることは順当だなと思います。

# まとめ

以上のことから取れる対策としては、

  • コードの中でコンパイルが重そうな箇所をコメントアウトするなどして、支配的な場所を探す
  • crate を分割する

あたりかなと思います。 ただ、前者についてはどこかに強烈なボトルネックがあるようにも感じないので、そこまで効果があるかはわかりません。

対策については計測する前からうすうすわかっていたことではありますが、計測するツールについて知ることができたので良いことにします。