型のサイズ
Essential前提知識
let x: u32 = 42; と書くとき、Rustは x のために何バイトを確保すべきかをコンパイル時に、無条件に知る必要がある。この要件を型のサイズ(size)と呼び、スタックフレーム、配列のストライド、memcpy 呼び出し、FFI境界など、メモリレイアウトのほぼすべての基盤となっている。
std::mem::size_of でサイズを照会する
std::mem::size_of::<T>() は T のサイズをバイト単位で usize として返す。const 関数なので定数式の中で使える。
use std::mem;
fn main() {
println!("{}", mem::size_of::<u8>()); // 1
println!("{}", mem::size_of::<i32>()); // 4
println!("{}", mem::size_of::<f64>()); // 8
println!("{}", mem::size_of::<bool>()); // 1
println!("{}", mem::size_of::<char>()); // 4
println!("{}", mem::size_of::<usize>()); // 64ビットターゲットでは 8
println!("{}", mem::size_of::<()>()); // 0
}
型ではなく手元の値がある場合は、代わりに mem::size_of_val(&x) を使う。
プリミティブ型のサイズ
下の表は標準的なプリミティブ型をまとめたものだ。サイズはプリミティブ型で学んだビット幅から直接導かれる。
| 型 | サイズ(バイト) | 備考 |
|---|---|---|
u8, i8 | 1 | |
u16, i16 | 2 | |
u32, i32, f32 | 4 | |
u64, i64, f64 | 8 | |
u128, i128 | 16 | |
usize, isize | 4 または 8 | ポインタ幅に一致 |
bool | 1 | 有効なビットパターンは 0x00 と 0x01 のみ |
char | 4 | Unicodeスカラー値を格納 |
() | 0 | ユニット型はデータを持たない |
usize と isize は、ターゲットプラットフォームのポインタと同じ幅だ。
構造体のサイズ:フィールドにパディングを加えたもの
構造体のサイズは各フィールドのサイズの単純な合計ではない。コンパイラはフィールド間に不可視のパディング(padding)バイトを挿入して、各フィールドが必要なアドレス境界で始まるようにする。その境界がフィールドのアライメント(alignment)だ — 詳細は次のチェックポイントで扱う。
use std::mem;
struct Foo {
a: u8, // 1 バイト
b: u32, // 4 バイト
c: u16, // 2 バイト
}
fn main() {
println!("{}", mem::size_of::<Foo>()); // 7 ではなく 12
}
余分な5バイトはコンパイラが各フィールドのアライメント要件を満たすために追加するパディングだ。正確なレイアウトは型のアライメントで説明する。
列挙型のサイズ:判別子プラス最大バリアント
Rustの列挙型は本質的にタグ付き共用体だ。サイズは次のようになる。
に加えて、全体をアライメントするために必要なパディングだ。判別子(discriminant)は、どのバリアントが有効かを識別する整数だ。
use std::mem;
enum Message {
Quit, // データなし
Move { x: i32, y: i32 }, // 8 バイトのデータ
Write(String), // 64ビットでは 24 バイト
}
fn main() {
println!("{}", mem::size_of::<Message>()); // 少なくとも 24 + 判別子バイト
}
Option<&T> のニッチ最適化
コンパイラは参照がnullにならないことを知っている(ビットパターン 0x0…0 は無効だ)。そのため、その禁じられたビットパターンを None の判別子として使う。結果として Option<&T> は &T と完全に同じサイズになる。
use std::mem;
fn main() {
assert_eq!(
mem::size_of::<Option<&u32>>(),
mem::size_of::<&u32>(), // 64ビットではどちらも 8 バイト
);
}
これをニッチ最適化(niche optimisation)と呼ぶ — コンパイラが型内の無効なビットパターンを利用して判別子をタダで格納する。同じテクニックが Option<Box<T>>、Option<fn()>、その他のnullにならないポインタ型に適用される。
動的サイズ型(DST)
意図的にコンパイル時サイズを持たない型がある。
[T]—T値のスライス。長さは型の一部ではないdyn Trait— トレイトオブジェクト。具体的な型は実行時にしかわからない
size_of::<[u8]>() はコンパイルエラーになる。参照と一緒に size_of_val を使う。
use std::mem;
fn print_slice_size(s: &[u32]) {
println!("{}", mem::size_of_val(s)); // 長さ * 4
}
fn main() {
print_slice_size(&[1, 2, 3]); // 12
}
動的サイズ型(DST)へのファットポインタ(fat pointer)— &[T] または &dyn Trait — は既知のサイズを持つ。ポインタ幅二つ分だ(ポインタ + 長さ、またはポインタ + vtableポインタ)。
use std::mem;
fn main() {
println!("{}", mem::size_of::<&[u32]>()); // 16: ptr + len
println!("{}", mem::size_of::<&dyn std::fmt::Display>()); // 16: ptr + vtable
}
Sized トレイトと ?Sized
コンパイル時サイズを持つすべての型は暗黙的に Sized マーカートレイトを実装する。DSTは実装しない。デフォルトでは、ジェネリックな型パラメータは Sized を要求する。
fn wrap<T>(x: T) -> Box<T> { // T: Sized は暗黙的
Box::new(x)
}
T: ?Sized と書くことでその要件をオプトアウトし、T = [u8] や T = dyn Trait を受け取れる関数にできる。?Sized は標準ライブラリの中でよく見かける(例:Box<T: ?Sized>、Rc<T: ?Sized>)。
まとめ
std::mem::size_of::<T>()はTのコンパイル時バイトサイズを返す。値しか手元にない場合はsize_of_val(&x)を使う。- プリミティブ型のサイズはビット幅に一致する。
()はゼロバイトだ。 - 構造体のサイズはフィールドサイズの合計にアライメントのためのコンパイラ挿入パディングを加えたものだ。
- 列挙型のサイズは判別子プラス最大バリアントだ。ニッチ最適化により
Option<&T>が&Tと同じサイズになることがある。 - DST(
[T]、dyn Trait)はコンパイル時サイズを持たない。それらへのファットポインタはポインタ幅二つ分だ。 Sizedはすべてのジェネリックパラメータへの暗黙の境界だ。?Sizedはそれを解除する。