repr(Rust)
最初に重要なことは、すべての型はバイト単位で指定されたアラインメントに従います。
ある型のアラインメントは、値を格納する有効なアドレスを規定します。
アラインメント n
の値は、n
の倍数のアドレスにのみ格納できます。
つまりアラインメント 2 は、偶数アドレスにのみ格納できることを意味し、
アラインメント 1 はどこにでも格納できることになります。
アラインメントの最小値は 1 で、常に 2 のべき乗になります。
ほとんどのプリミティブ型はそのサイズにアラインメントされますが、
これはプラットフォーム依存の挙動です。
特に x86 では u64
と f64
は 32 bits にアラインされるかもしれません。
型のサイズは、常にそのアラインメントの倍数でなくてはなりません。 こうすることで、サイズの倍数をオフセットすることで、その型の配列のインデックスアクセスになります。 動的にサイズが決まる型 の場合、型のサイズとアラインメントは静的にはわからない場合があることに注意してください。
Rust では次の方法で複合データのメモリレイアウトを制御することができます。
- 構造体(名前付き直積型)
- タプル(名前なし直積型)
- 配列(同じ種類の型の直積型)
- enum(名前付き直交型。またはタグ付き共用体)
enum のすべての要素が関連データを持たない場合、その enum は C-like と呼ばれます。
複合データのアラインメントは、その要素のうち最大のアラインメントと同じです。 そのために、Rust は必要なときにはパディングを挿入して、 すべてのフィールドが適切にアラインされ、 また全体のサイズがアラインメントの倍数になるようにします。 例えば、
struct A {
a: u8,
b: u32,
c: u16,
}
この構造体は、メンバーのプリミティブ型が対応するサイズにアラインされるアーキテクチャでは、 32-bit にアラインされます。そのため全体の構造体のサイズも 32 bit の倍数になります。 このようになるでしょう。
struct A {
a: u8,
_pad1: [u8; 3], // `b` のアラインメントのため
b: u32,
c: u16,
_pad2: [u8; 2], // 全体のサイズを 4 byte の倍数にするため
}
この構造体には 間接参照はありません。C と同様に、すべてのデータは構造体の内部に格納されます。 しかし、配列は例外(配列は隙間なく順にパックされます)ですが、Rust ではデータレイアウトは デフォルトでは規定されていません。以下の 2 つの構造体の定義を見てみましょう。
struct A {
a: i32,
b: u64,
}
struct B {
a: i32,
b: u64,
}
Rust は A の 2 つのインスタンスが同じようにレイアウトされることを保証します。 しかし、A のインスタンスと B のインスタンスとが同じフィールド順や、同じパディングを持つことを 保証しません。(現実的には同じにならない理由はないのですが)
この A, B の例では、レイアウトのが保証されないなんて融通が利かないと思うかもしれませんが、 他の機能を考えると、Rust がデータレイアウトを複雑にいじくれるようにするのは好ましいのです。
例えば、次の構造体を見てみましょう。
struct Foo<T, U> {
count: u16,
data1: T,
data2: U,
}
さて、単体化した Foo<u32, u16>
と Foo<u16, u32>
とを考えてみます。
もし Rust が指定された順にフィールドをレイアウトしなくてはならないとすると、
アラインメントの要求を満たすために、パディングしなくてはなりません。
つまりもし Rust がフィールドを並び替えられないとすると、次のような型を生成すると思われます。
struct Foo<u16, u32> {
count: u16,
data1: u16,
data2: u32,
}
struct Foo<u32, u16> {
count: u16,
_pad1: u16,
data1: u32,
data2: u16,
_pad2: u16,
}
後者の例ははっきり言ってスペースの無駄遣いです。 したがって、スペースを最適に使うには、異なる単体化には異なるフィールド順序が必要になります。
これは仮定の最適化で、Rust 1.0 ではまた実装されていないことに注意してください。
Enum については、もっと複雑な検討が必要になります。つまり、この enum
enum Foo {
A(u32),
B(u64),
C(u8),
}
は、次のようにレイアウトされるでしょう。
struct FooRepr {
data: u64, // `tag` によって、u64, u32, u8 のいずれかになります
tag: u8, // 0 = A, 1 = B, 2 = C
}
実際にこれが、データが一般的にどのようにレイアウトされるかの大体の説明となります。
ところが、このような表現が非効率な場合もあります。
わかりやすい例としては、Rust の "null ポインタ最適化" があります。
これは、ある enum がデータを持たないメンバー(たとえば None
)と、(ネストしてるかもしれない)null を取らないメンバー(たとえば &T
)から構成される場合、null ポインタをデータを持たないメンバーと解釈することができるので、タグが不要になります。
その結果、たとえば size_of::<Optiona<&T>>() == size_of::<&T>()
となります。
Rust には、null ポインタになりえない型や、null ポインタを含まない型がたくさんあります。
例えば Box<T>
, Vec<T>
, String
, &T
, &mut T
などです。
同様に、ネストした複数の enum が、タグを単一の判別子に押し込めることも考えられます。
タグが取り得る値は、定義により限られているからです。
原理的には、enum はとても複雑なアルゴリズムを使って、ネストした型を特別な制約のもとで表現し、
bit を隠すことができるでしょう。
このため、enum のレイアウトを規定しないでおくことは、現状では 特に 好ましいのです。