Rust 実践講座 #7 リリース最適化とクロスコンパイル — 小さく速いバイナリをどこでも

読了 5分

テスト(第 6 回)という安全網ができたので、いよいよ人に渡すバイナリを作ります。今回の目標は 2 つです。release ビルドをもう一段締めて小さく速くすること、そして手元の機械ではない OS・アーキテクチャのためのバイナリを作ることです。

release プロファイルを締める #

第 4 回で --release の効果を測りましたが、その release にもまだ締められるねじが残っています。Cargo.toml にプロファイル設定を追加します。

Cargo.toml
[profile.release]
lto = true           # リンク時最適化: クレート境界を越えるインライン化・除去
codegen-units = 1    # 並列コード生成を諦めて最適化品質を最大に
strip = true         # デバッグシンボル除去でバイナリサイズ削減

どのオプションも無料ではなく、交換です。

  • lto(link-time optimization): 通常はクレート単位で最適化が終わりますが、lto はリンク時点で全体を見直し、クレート境界を越えるインライン化とデッドコード除去を行います。代償はコンパイル時間の増加です。
  • codegen-units = 1: 既定はコード生成を複数の断片に分けて並列コンパイルしますが、断片の境界で最適化の機会が消えます。1 にすると品質は最大、コンパイルは最も遅くなります。
  • strip: デバッグシンボルを取り除いてサイズを大きく減らします。代償は配布バイナリのバックトレースが貧弱になることです。

まとめると、開発の反復が遅くなる代償を配布品質と交換する設定で、release プロファイルにだけ置くので日常の cargo runcargo test には影響しません。loglens 程度のプロジェクトでも、strip 1 つでバイナリが数 MB から 1MB 台まで下がるのはよく見る光景です。1 つ注意があります。サイズ削減の一覧によく登場する panic = "abort"(巻き戻しなしの即時終了)は慎重に。サイズは減りますが、パニック時の後始末とバックトレースの品質を犠牲にするので、利用者のバグ報告を受け取りたいツールなら既定値のままが無難です。

クロスコンパイル — ターゲットという概念 #

開発は Mac でしたが、loglens が動く場所は Linux サーバーです。Rust はコンパイルターゲットを「トリプル」と呼びます。x86_64-unknown-linux-gnu(アーキテクチャ-ベンダー-OS-ライブラリ)のような形で、rustup でターゲットを追加できます。

ビルド
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl

ターゲットに -musl を選んだのが、この回の核心の決定です。既定ターゲット(-gnu)のバイナリはシステムの glibc に動的にリンクされるため、ビルド環境の glibc が実行サーバーより新しいと、あの有名な GLIBC_2.xx not found エラーで実行を拒否されます。musl ターゲットは C ライブラリまで静的にリンクした単一ファイルを作るので、古いディストリビューションでも Alpine コンテナでも、コピーするだけで動きます。「インストール不要でファイル 1 つ」という CLI ツールの美徳が完成する地点で、Linux 配布用 Rust CLI の事実上の標準の選択です。

リンカ問題と cross #

ところが上のコマンドを Mac でそのまま実行すると、たいていリンクの段階で止まります。Rust コンパイラはどのターゲットのコードでも生成できますが、最後にバイナリを組み立てるリンカはターゲットプラットフォームのものが必要だからです。基本はターゲットごとのリンカを導入して設定することで、実務で広く使われる迂回路が cross です。

crossでビルド
cargo install cross
cross build --release --target x86_64-unknown-linux-musl

cross は、ターゲットごとのツールチェーンが揃った Docker コンテナの中で cargo build を代行します。コマンドの使い勝手が cargo と同じなので学習コストがほぼなく、リンカ設定という沼を丸ごと飛び越えられます。Docker が必要というのが唯一の前提です。そして次回見るように、配布の自動化ではこの問題を別の方法でも解決します。各 OS のランナーがそれぞれビルドすれば、クロスコンパイル自体が減ります。 cross は「手元でいま作る必要があるとき」の道具として位置づければ十分です。

macOS と Windows の利用者向けバイナリも同じ原理です。Apple Silicon(aarch64-apple-darwin)、Intel Mac(x86_64-apple-darwin)、Windows(x86_64-pc-windows-msvc)をそれぞれビルドして、配布対象の一覧を埋めます。この一覧が次回のリリース自動化のマトリックスになります。

まとめ #

  • release プロファイルの lto、codegen-units = 1、strip は、コンパイル時間を差し出して実行速度とサイズを受け取る交換です。配布プロファイルにだけ効くので、開発の反復はそのままです。
  • panic = "abort" はさらにサイズを減らしますが、バックトレースの品質を犠牲にします。バグ報告を受け取るツールなら既定値が無難です。
  • クロスコンパイルは rustup target add + --target が基本形です。Linux 配布は、glibc のバージョン地獄を避ける musl 静的リンクが事実上の標準です。
  • コンパイルは通ってもリンカに足を取られたら、cross が Docker でターゲットのツールチェーンを丸ごと代行します。
  • 配布対象のトリプル一覧(Linux musl、Mac 2 種、Windows)が次回の材料です。crates.io への公開と GitHub Releases の自動化でシリーズを締めくくります。
X