Full title: [Advanced Rust] 1.14. Memory Types Pt.2 - Dynamically Sized Types and Wide Pointers, Packed Layouts, Larger Alignment for Specific Fields or Types, Memory Representation of Complex Types, and Repr Rust

1.14.1. repr(Rust)

前の記事の例を思い出してください。その例では repr(C) が使用されていましたが、C表現の制限として、すべてのフィールドは元の構造体で定義された順序と同じ順序で配置されなければなりません。

repr(Rust) はデフォルトの表現です。意図的に repr(C) よりも少ないレイアウト保証を提供します。コンパイラはフィールドの順序を変更することができ、同じ順序で同じフィールドを持つ2つの型でも、レイアウトが共有されることは保証されません。

コンパイラがフィールドを並べ替える可能性があるため(たとえば、より大きなフィールドを最初に配置するなど)、パディングを減らせる場合がよくあります。前の記事の Foo の例では、パディングを必要としない最適化されたレイアウトが可能です。

レイアウトに関する保証が少ないため、コンパイラは効率的なコードを生成するために自由に再配置できます。

repr(Rust) が使用される場合、上記の Foo 構造体の可能なメモリレイアウトの1つは次のとおりです。

Code Field Type Size Default Representation Padding Final Alignment
#[repr(Rust)]
struct Foo {
long: u64, 8 bytes 8-byte aligned 8 bytes
normal: u32, 4 bytes 4-byte aligned
short: u16, 2 bytes 2-byte aligned small: u8, 1 byte 1-byte aligned tiny: bool, 1 byte 1-byte aligned } Total 16 bytes
  • コンパイラはまずフィールドをサイズ順に並べ、最大のフィールドを最初に配置して、構造体が使用すべきアライメントを決定します。この例では、u64 が最大で8バイトを占めるため、構造体は8バイトにアライメントされます
  • 次に、コンパイラは残りのフィールドを見て、その合計サイズがちょうど8バイトであることを確認し、パディングを避けて一緒に配置できます
  • 最終的に、この構造体は16バイトしか必要とせず、repr(C) と比べて半分のメモリを節約できます
  • これはより効率的ですが、コンパイル時間が少し長くなる可能性があります

1.14.2. Packed Layouts

フィールド間にパディングが必要ないことをコンパイラに指示できますが、その場合は未アライメントアクセスのパフォーマンスコストを受け入れる必要があります。

メモリが制限されている場合や、型のインスタンスが多い場合に、packedレイアウトは有用です。また、低帯域幅のネットワーク接続を介してメモリ表現を送信する場合にも役立ちます。

packedレイアウトを有効にするには、型に #[repr(packed)] アノテーションを追加します。

packedレイアウトでは次の点に注意してください。

  • コードの実行速度が遅くなる可能性があります
  • 極端な場合、CPUがアライメントされたアクセスのみをサポートしている場合、プログラムがクラッシュする可能性があります

1.14.3. Giving a Specific Field or Type a Larger Alignment

#[repr(align(n))] アノテーションを使用すると、特定のフィールドまたは型により大きなアライメントを与えることができます。ここで n は引数です。

たとえば、メモリ上で連続して格納される異なる値(配列内など)が異なるCPUキャッシュラインに配置されるようにしたい場合、false sharing を避けることができます。

関連用語の簡単な説明は次のとおりです。

  • キャッシュはキャッシュラインで構成され、キャッシュはキャッシュラインを単位として動作します。キャッシュライン はキャッシュにマップできる最小のデータ単位です
  • 異なる2つのCPUが同じキャッシュラインを共有する異なる変数にアクセスするときにfalse sharingが発生します。理論的には並列に動作できるはずですが、最終的には同じキャッシュエントリを更新するために競合します。これにより、同時実行プログラムで大きなパフォーマンス低下を引き起こす可能性があります

1.14.4. Memory Representation of Complex Types

  • タプル: メモリ表現は構造体と似ています。フィールド型とタプル要素型は同じ順序です
  • 配列: 含まれる型の連続したシーケンスで、要素間にパディングはありません
  • ユニオン: 各フィールドについてレイアウトの選択は独立しており、アライメントはすべてのフィールドの中で最大のものになります
  • 列挙型: ユニオンと似ていますが、列挙型のバリアントの判別子を格納するために追加の隠し共有フィールドがあります。コードはこの判別子の値を使用して、指定された値にどのバリアントが含まれているかを判断します。判別子のサイズはバリアントの数に依存します

1.14.5. Dynamically Sized Types and Wide Pointers

Rustのほとんどの型は自動的に Sized トレイトを実装します。

Rustは型についてある程度の詳細を知る必要があります。たとえば、特定の型の値に割り当てるスペースの量などです。これが dynamically sized types の概念を少し混乱させるものです。これらは DSTs または unsized types と呼ばれることもあり、実行時にのみサイズが判明する値で動作するコードを書くことを可能にします。

動的サイズ型を使用するために、Rustは Sized トレイトを提供して、型のサイズがコンパイル時に判明しているかどうかを示します。コンパイル時にサイズが判明しているものはすべて、自動的にこのトレイトを実装します。Rustはまた、すべてのジェネリック関数に暗黙的に Sized トレイトを追加します。デフォルトでは、ジェネリック関数はコンパイル時にサイズが判明している型にのみ適用されます。 この制限は ?Sized で緩和できます。?Sized は「TSized を実装するかもしれないし、しないかもしれない」という意味です。つまり、T は動的サイズ型かもしれないということです。この表記は、ジェネリック型はコンパイル時に既知のサイズを持たなければならないというデフォルトの条件を必要としません。この意味を持つ ?Trait 構文は Sized トレイトにのみ適用され、他のトレイトには適用されません。

関数がDST(トレイトオブジェクトやスライスなど)をパラメータとして受け入れる必要がある場合、どうすればよいでしょうか? wide pointerfat pointer とも呼ばれる)を使用できます。

1.14.6. Wide Pointers

Sized 型をwide pointerの背後に配置することで、Sized と非 Sized 型の間のギャップを埋めることができます。

wide pointerとは何でしょうか? wide pointerは、通常のポインタに追加の「word-size」フィールドが付いたものです。これにより、コンパイラはそのポインタを使用する適切なコードを生成するために必要な追加情報を得ることができます。

DSTを参照すると、コンパイラは自動的にwide pointerを構築します。たとえば、slice の追加情報はスライスの長さです。

wide pointerは本質的にポインタであり、そのサイズは固定されているため Sized です(「sized」ポインタの2倍のサイズ — 1つのフィールドはポインタ自体を格納し、もう1つのフィールドは型を「完成させる」ために使用されるメタデータを格納します)。

注: Box<T>Arc<T> はどちらもwide pointerを格納できるため、両方とも ?Sized をサポートします。