このページの目次
ジェネリクスと単一化
Rustにおけるジェネリクスの中核メカニズムは単一化(monomorphization)です。コンパイラは各具体的な型引数に対して独立した機械語を生成します。つまり、
Vec<i32>とVec<String>は全く異なる関数本体を持ち、実行時のオーバーヘッドはゼロですが、その代償としてバイナリサイズが膨張します。const genericsは定数値もジェネリックパラメータに組み込み、specializationは特定の型に対してより最適な実装を提供することを可能にします(現時点ではnightly版のみ利用可能)。
単一化(Monomorphization): 各具体的な型に対してコンパイル時にコードを生成
Rustのジェネリクスはランタイムでのポリモーフィズム(vtableなし)ではありません。コンパイラは、ジェネリクスが使用される各具体的な型に対して独立した機械語を生成します。
let _ = max; // コンパイラは max_i32 を生成
let _ = max; // コンパイラは max_f64 を生成
let x = "a"; let y = "b";
let _ = max; // コンパイラは max_&str を生成
// 出力されるバイナリには、3つの独立した max 関数が含まれており、それぞれが具体的な型に対して最適化されています
これはC++テンプレートのメカニズムと同じく、コンパイル時のコード生成です。しかし、Rustではジェネリクスは定義時にチェックされます(インスタンス化を待たないため)、その結果、エラーメッセージがより明確になります。C++テンプレートでのエラーは通常、インスタンス化時に発生するため、数十行に及ぶテンプレート関連のエラーメッセージになることがあります。
「ゼロコスト抽象」の意味
リリースビルドでは、ジェネリック関数は通常、呼び出し元でインライン展開されます。最終的なアセンブリコードにはジェネリック関数の存在は表示されず、i32やf64を直接操作するcmp命令が数行残るだけです。これがRustが言うところの「ゼロコスト抽象(zero-cost abstraction)」です。高レベルな抽象化を利用できますが、コンパイル後のコードは、具体的な型に対して手書きで記述したコードと同じ速度で実行されます。
コードの肥大化と緩和策
各具体的な型に対してコードが生成されるため、多くの型組み合わせが存在すると、コンパイル時間の爆発的増加とバイナリサイズの肥大化を招きます。古典的な緩和策は、非ジェネリックな内部関数を抽出することです。
// Tに依存しない → 分岐しない
重要なのは、ジェネリックでなければならない部分(型変換など)と、トレイトオブジェクトや具体的な型で処理できる部分(ビジネスロジックなど)を分離することです。
const generics: 値をジェネリックパラメータとして使用
従来のジェネリックパラメータは型でしたが、const generics(Rust 1.51以降)により値もジェネリックパラメータとして使用できるようになりました。具体的には、コンパイル時定数です。
let b1: = Buffer ;
let b2: = Buffer ;
// b1 と b2 は**異なる型**です! 互いに代入することはできません
主な用途としては、固定サイズの配列/ベクタ、SIMDレーン、コンパイル時における配列サイズの検証などがあります。
Specialization(nightly版、まだstableではない)
現在のstable版Rustではトレイトのspecializationはサポートされていません。つまり、あるトレイトに対して「より具体的なimplがより一般的なimplを上書きする」ということはできません。これは、Vec<u8>に対してジェネリックなVec<T>よりも効率的なExtend implを提供するなど、いくつかのパフォーマンス最適化を制限しています。nightly版ではmin_specializationが利用可能ですが、stable版ではまだ利用できません。現時点での代替案としては、newtypeパターンを使用して特殊化を提供する方法があります。
参考
- Rust Book: 第10章 10.1節
- Rust Reference: const generics
- RFC 2000: const generics; RFC 1210: Specialization
Keywords: monomorphization, zero-cost abstraction, code bloat, const generics, specialization