型サイズ
頻繁にインスタンス化される型を小さくすると、パフォーマンス向上に役立つことがあります。
たとえば、メモリ使用量が多い場合、DHAT のようなヒーププロファイラーを使うと、ホットな割り当て箇所と関係する型を特定できます。これらの型を小さくすると、ピークメモリ使用量を削減でき、メモリトラフィックとキャッシュ圧迫を減らすことでパフォーマンスが向上する可能性もあります。
さらに、128 バイトを超える Rust 型は、インラインコードではなく memcpy でコピーされます。プロファイルで memcpy が無視できない量として現れる場合、DHAT の “copy profiling” モードを使うと、ホットな memcpy 呼び出しがどこにあり、どの型が関係しているかを正確に教えてくれます。これらの型を 128 バイト以下に小さくすると、memcpy 呼び出しを避け、メモリトラフィックを減らすことで、コードを高速化できます。
型サイズの測定
std::mem::size_of は型のサイズをバイト単位で返しますが、多くの場合、正確なレイアウトも知りたいはずです。たとえば、1 つの過大なバリアントが原因で、enum が予想外に大きくなることがあります。
-Zprint-type-sizes オプションは、まさにこれを行います。これは rustc のリリース版では有効になっていないため、nightly 版の rustc を使用する必要があります。Cargo 経由で呼び出す一例を示します。
RUSTFLAGS=-Zprint-type-sizes cargo +nightly build --release
rustc を呼び出す一例を示します。
rustc +nightly -Zprint-type-sizes input.rs
これにより、使用されているすべての型について、サイズ、レイアウト、アラインメントの詳細が出力されます。たとえば、次の型の場合:
#![allow(unused)] fn main() { enum E { A, B(i32), C(u64, u8, u64, u8), D(Vec<u32>), } }
次の内容に加え、いくつかの組み込み型に関する情報が出力されます。
print-type-size type: `E`: 32 bytes, alignment: 8 bytes
print-type-size discriminant: 1 bytes
print-type-size variant `D`: 31 bytes
print-type-size padding: 7 bytes
print-type-size field `.0`: 24 bytes, alignment: 8 bytes
print-type-size variant `C`: 23 bytes
print-type-size field `.1`: 1 bytes
print-type-size field `.3`: 1 bytes
print-type-size padding: 5 bytes
print-type-size field `.0`: 8 bytes, alignment: 8 bytes
print-type-size field `.2`: 8 bytes
print-type-size variant `B`: 7 bytes
print-type-size padding: 3 bytes
print-type-size field `.0`: 4 bytes, alignment: 4 bytes
print-type-size variant `A`: 0 bytes
出力からは次のことが分かります。
- 型のサイズとアラインメント。
- enum の場合、判別子のサイズ。
- enum の場合、各バリアントのサイズ(大きいものから小さいものへソート)。
- すべてのフィールドのサイズ、アラインメント、順序。(コンパイラが
Eのサイズを最小化するために、バリアントCのフィールドを並べ替えていることに注意してください。) - すべてのパディングのサイズと位置。
あるいは、top-type-sizes クレートを使用して、出力をよりコンパクトな形式で表示できます。
ホットな型のレイアウトが分かれば、それを小さくする方法はいくつもあります。
フィールド順序
Rust コンパイラは、サイズを最小化するために、構造体と enum のフィールドを自動的にソートします(#[repr(C)] 属性が指定されている場合を除く)。そのため、フィールド順序を気にする必要はありません。ただし、ホットな型のサイズを最小化する方法はほかにもあります。
より小さい enum
enum に過大なバリアントがある場合、1 つ以上のフィールドを Box 化することを検討してください。たとえば、次の型を:
#![allow(unused)] fn main() { type LargeType = [u8; 100]; enum A { X, Y(i32), Z(i32, LargeType), } }
次のように変更できます。
#![allow(unused)] fn main() { type LargeType = [u8; 100]; enum A { X, Y(i32), Z(Box<(i32, LargeType)>), } }
これにより、A::Z バリアントに追加のヒープ割り当てが必要になる代わりに、型サイズが小さくなります。A::Z バリアントが比較的まれである場合、全体としてパフォーマンス向上につながる可能性が高くなります。Box によって、特に match パターンでは A::Z がやや使いにくくもなります。
例 1,
例 2,
例 3,
例 4,
例 5,
例 6.
より小さい整数
より小さい整数型を使用することで型を小さくできることはよくあります。たとえば、インデックスには usize を使用するのが最も自然ですが、インデックスを u32、u16、場合によっては u8 として格納し、使用箇所で usize に変換することは、多くの場合妥当です。
例 1,
例 2.
Box 化されたスライス
Rust のベクターは、長さ、容量、ポインターの 3 ワードを含みます。将来変更される可能性が低いベクターがある場合、Vec::into_boxed_slice を使ってそれを Box 化されたスライス に変換できます。Box 化されたスライスは、長さとポインターの 2 ワードだけを含みます。余分な要素容量は破棄されるため、再割り当てが発生することがあります。
#![allow(unused)] fn main() { use std::mem::{size_of, size_of_val}; let v: Vec<u32> = vec![1, 2, 3]; assert_eq!(size_of_val(&v), 3 * size_of::<usize>()); let bs: Box<[u32]> = v.into_boxed_slice(); assert_eq!(size_of_val(&bs), 2 * size_of::<usize>()); }
あるいは、Iterator::collect を使って、イテレーターから Box 化されたスライスを直接構築できます。イテレーターの長さが事前に分かっている場合、これにより再割り当てを避けられます。
#![allow(unused)] fn main() { let bs: Box<[u32]> = (1..3).collect(); }
Box 化されたスライスは、クローンや再割り当てなしで slice::into_vec を使ってベクターに変換できます。
ThinVec
Box 化されたスライスの代替となるのが、thin_vec クレートの ThinVec です。これは機能的には Vec と同等ですが、長さと容量を(要素がある場合は)要素と同じ割り当て内に格納します。つまり、size_of::<ThinVec<T>> は 1 ワードだけです。
ThinVec は、頻繁にインスタンス化される型の中で、空であることが多いベクターに適した選択肢です。また、あるバリアントが Vec を含む場合に、enum の最大バリアントを小さくするためにも使用できます。
リグレッションの回避
型が、そのサイズがパフォーマンスに影響を与え得るほどホットである場合は、意図せずリグレッションしないように静的アサーションを使用することをお勧めします。次の例では、static_assertions クレートのマクロを使用しています。
// この型は頻繁に使用されます。意図せず大きくならないようにしてください。
#[cfg(target_arch = "x86_64")]
static_assertions::assert_eq_size!(HotType, [u8; 64]);
型サイズはプラットフォームによって異なる可能性があるため、cfg 属性は重要です。アサーションを x86_64(通常、最も広く使われているプラットフォーム)に制限するだけで、実際にはリグレッションを防ぐのに十分である可能性が高いでしょう。