前提
Rustのビルド時間は長いことで有名ですね。
自分も直近で実際にビルド時間の長さを感じる機会がありました。
いい機会なので、ビルドに関連するオプションによる差異、短縮に向けたアプローチ等を模索した結果をまとめたいと思います。
環境・依存関係設定
| 環境 | 値 |
|---|---|
| OS | Mac(M1) メモリ16GB |
| 言語 | Rust 1.90.0 |
| 直接依存関係数 | 25 |
| 全体依存関係数 | 409 |
ビルド時間(ローカル・デフォルト)
まずは単純に下記コマンドでローカルでの時間を計測してみました。
リリースビルドに対するオプションは変更なし、デフォルトの状態です。
time cargo build --release
結果は1003.40s user 42.07s system 626% cpu 2:46.83 totalでした。
2分46秒 が実際の時間ということになります。
ビルド時間(ローカル・単一ユニットオプション指定)
続いて、下記のビルドオプションを付与した場合です。
codegen-units = 1
codegen-unitsとは
クレートをいくつのコード生成ユニットに分割するかを指定するオプションです。
releaseビルドのデフォルト値は16であり、数が多いほど並列で処理されるためビルド時間が短縮されます。
ただし分割しない方が処理の最適化範囲が広がるため、実行速度が高速化する可能性があります。
結果は582.18s user 18.00s system 341% cpu 2:55.90 totalでした。
2分55秒 なので、オプションなしの2分46秒と比べると若干ビルド時間が長くなっていますね。
実行速度への影響
ちなみにこのオプションを適用したことで、プログラムの処理時間は下記のように変わりました。
検証のしやすさと変更の大きさからdev(デフォルト値は256)での結果になります。
- dev・codegen-units=256:3.71s user 0.51s system 93% cpu 4.534 total
- dev・codegen-units=1:3.78s user 0.50s system 98% cpu 4.342 total
逆にあえて値を512(devデフォルトの2倍)にしてみた結果は下記の通りです。
- dev・codegen-units=256:3.71s user 0.51s system 93% cpu 4.534 total
- dev・codegen-units=512:3.76s user 0.52s system 92% cpu 4.608 total
そんなに重たくないプログラムというのもあり、微々たる変化ですね。
公式にも推奨のある通り、何がなんでも実行速度優先にするのではなく、性能要件とプログラムの規模に応じて最適な選択を取るのが良さそうです。
ビルド時間(ローカル・リンク最適化オプション指定)
今度は下記のビルドオプションを付与した場合です。
lto = true # または fat でもOK
ltoとは
LLVMのリンク時最適化を行うかどうかを制御するオプションです。
releaseビルドであってもデフォルト値はfalseなので、有効化することで実行速度が高速化する可能性があります。が、その分ビルド時間は長くなります。
結果は619.56s user 34.70s system 325% cpu 3:20.98 totalでした。
3分20秒 なので、オプションなしの2分46秒と比べるとだいぶ長くなっていますね。
実行速度への影響
こちらも実行速度への影響を計測してみました。
例の如くdev(releaseと同じくデフォルト値false)での結果ですが、下記の通りです。
- dev・lto=false:3.71s user 0.51s system 93% cpu 4.534 total
- dev・lto=true :3.92s user 0.47s system 99% cpu 4.419 total
こちらも微々たる変化ですね。
ビルド時間の増加量に対して割に合わない感はあるので、ゲームなど性能重視のアプリじゃない限り、無理して設定変更しなくても良いかもしれません。
ビルド時間(ローカル・オプトレベルダウン)
続いて、オプトレベルをあえてダウンさせた場合です。
opt-level = 0
opt-levelとは
コンパイル時にどの程度の最適化パスを適用するか(未使用コードの削除やインライン関数の適用など)を指定するオプションです。
releaseビルドのデフォルト値は3(すべて最適化)ですが、今回はあえて0(最適化なし)にした場合の検証となります。
結果は242.29s user 28.79s system 432% cpu 1:02.68 totalでした。
1分02秒 なので、3(全て最適化)の2分46秒と比べると圧倒的にビルド時間は短いですね。
実行速度への影響
こちらも実行速度への影響を計測してみました。
例の如くdev(デフォルトは0なので、3に変更した場合との比較)での結果ですが、下記の通りです。
- dev・opt-level=0:3.71s user 0.51s system 93% cpu 4.534 total
- dev・opt-level=3:1.37s user 0.42s system 78% cpu 2.278 total
圧倒的に早いですね。
ビルド時間と実行速度の両方に大きく影響するので悩ましいところですが、よっぽど性能要件が低くない限りはデフォルトの3(全て最適化)の方が良いのかなと感じました。
ビルド時間(Docker)
最後にDockerビルドです。
昨今の環境下では、ローカルでビルドするよりDockerビルドを使うことの方が多いことでしょう。
time docker build --no-cache --platform linux/amd64 -t app .
結果は0.41s user 0.21s system 0% cpu 11:59.63 totalでした。
11分59秒 なのでめちゃくちゃ時間かかっていますね……!
うーん、なんとかならないものか。
ビルド時間短縮へのアプローチ
ここからはビルド時間短縮に向けたいくつかのアプローチをトライしてみた結果を記載します。
短縮アプローチ① 依存関係を整理する
まずは基本からですが、不必要な依存関係がないか、依存関係の不必要なfeaturesを取り込んでいないかチェックしてみました。
特にtokioはfullで導入するととても大きいので、開発は一旦そのままで進めたとしても、リリース前に必要最小限へと絞り込むのが良さそうです。
今回は下記のように変更しました。
regex = "1.12.2" # <- 削除
reqwest = "0.12.24" # <- 削除
# tokio = { version = "1.48.0", features = ["full"] }から下記へ変更
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "sync"] }
結果は0.40s user 0.17s system 0% cpu 10:44.67 totalでした。
約1分20秒 の短縮に繋がっています。
短縮アプローチ② platformをlinux/arm64にする(Apple Siliconの場合)
EC2やECSのデフォルトはamd64なので、先程はamd64ビルドを行っていました。
しかしApple SiliconのMacにおいてamd64はエミュレータを介して操作するものになってしまうため、どうしてもビルド時間が長くなってしまいます(これはRustに限らずどの言語でもそう)。
AWS Gravitonの利用などが可能であれば、platformをlinux/arm64にすることで、時間を劇的に短縮することができそうです。
time docker build --platform linux/arm64 -t app .
結果は0.40s user 0.25s system 0% cpu 5:09.11 totalでした。
約7分 の短縮なので圧倒的ですね。
短縮アプローチ③ リンカーをmoldに変更する
Rust1.90.0でターゲットx86_64-unknown-linux-gnuに対するデフォルトリンカーがLLDへと変更になりました。
これにより従来のリンカーに比べると高速化が見込めるようになったのですが、より速度を求める場合はmoldというリンカー(マルチコアを活かすことで高速に動作する)を使うと良いようです。
# .cargo/config.tomlに下記を記載
[target.'cfg(target_os = "linux")']
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
# 下記を追加
ARG MOLD_VERSION=2.40.4
RUN ARCH=$(uname -m) \
&& curl -L -O https://github.com/rui314/mold/releases/download/v${MOLD_VERSION}/mold-${MOLD_VERSION}-${ARCH}-linux.tar.gz \
&& tar xf mold-* && \
cp -p mold-*/bin/* /usr/local/bin/ && \
rm -rf mold-*
結果は0.41s user 0.18s system 0% cpu 11:01.03 totalでした。
約1分 の短縮となっていますね。
まとめ
時間短縮アプローチの組み合わせを適用した結果は下記の通りです。
opt-levelはデフォルトの3のままとしました(処理速度を犠牲にしたくなかったため)。
| target | ビルド時間 | 短縮アプローチ |
|---|---|---|
| linux/amd64 | 10:36.10 total | 依存関係整理 + リンカーをmoldに変更 |
| linux/arm64 | 4:08.20 total | 依存関係整理 + arm64ビルド + リンカーをmoldに変更 |
元の11分59秒と比べると、どちらにせよ高速化できているのがわかりますね。
今回は初回のビルド時間短縮に限定して、やれることを調査・まとめてみました。
2回目以降のビルドに関してはもっと色々やれることがあって、実際にcargo-chefとBuildKitキャッシュを導入することで約50秒程度になっています。
ここら辺については下記の参考記事が非常にわかりやすいので、気になる方はぜひ見てみてください。
また、別アプローチとして、cargo watchでこまめに裏でビルドしておくという方法の紹介記事もありました。記事にもある通りtargetフォルダは急速に肥大化しそうなので、採用するかどうかは好みが分かれそうです。
色々調べて実際に検証してみたことで、だいぶ理解が進んだように感じます。
初回ビルドについてもやれるだけのことはやりつつ、とはいえRustの仕組み上限界もあるので2回目以降も工夫を入れて、運用に支障をきたさないようにしていくのが良いのかなと思いました。