このページの目次

基本型とメモリレイアウト

Rustのすべての型には厳密なメモリレイアウトが存在する——整数と浮動小数点数はビット幅に合わせて配置され、structにはパディングが含まれ、ゼロサイズ型(ZST)は0バイトを占め、niche最適化によりOption<bool>boolと同じサイズになる(boolは0/1のみなので、コンパイラは2つのビットパターンでNoneを表す)。このレイアウトを理解することは、Rustに「ボックス化のオーバーヘッド」がない理由を理解する前提条件である。

スカラー型: 「Cとほぼ同じ」ではない

Rustの整数型はCと見た目は似ている——i32, u64 など——が、セマンティクスには重要な違いがある:

let x: u8 = 255;
// x + 1: デバッグビルド → panic!("attempt to add with overflow")
//        リリースビルド → 0にラップされる(2の補数表現、未定義動作なし)

// 明示的なオーバーフロー処理の3つの方法:
255u8.checked_add(1)      // None (Optionを返す。パニックしない)
255u8.wrapping_add(1)     // 0 (明示的にラップしたい場合)
255u8.saturating_add(1)   // 255 (飽和演算)

Cでは符号付き整数のオーバーフローは未定義動作(UB)である。Rustでは、リリースビルドでのラップでさえ定義された動作であり、UBではない。ただし、デバッグビルドでのパニックにより、テスト中にオーバーフローバグを発見しやすくなる。

型サイズとアライメント

各型には、プラットフォームに依存しない(isize/usizeを除く)確定したsize_ofalign_ofがある:

use std::mem::{size_of, align_of};

size_of::<u8>()     // 1
size_of::<i64>()    // 8
size_of::<bool>()   // 1 (1ビットではない!メモリの最小アドレス指定単位はバイト)
size_of::<char>()   // 4 (Unicodeスカラー値であり、コードポイントではない)
size_of::<()>()     // 0 (ユニット型、ZST)
size_of::<&i32>()   // 8 (64ビット環境) または 4 (32ビット環境)

boolが1バイトを占める理由:CPUの命令セットでアドレス指定できる最小単位はバイトだからである。Rustではboolの特定のビットのアドレスを取得することはできない。bool配列をコンパクトに格納する必要がある場合は、bitvecクレートやVec<u64>を使用した手動のビットパックを利用する。

char: 4バイト = 1つのUnicodeスカラー値

let c: char = '';                  // U+3042、UTF-8では3バイト、char型では4バイト
size_of::<char>()                    // 4

// charと文字列の関係:
let s = "";                        // &str、3バイト: [E3, 81, 82]
let c = '';                        // char、4バイト: 0x00003042

charはUnicodeスカラー値(0x0000~0xD7FF, 0xE000~0x10FFFF)を表し、「1文字」ではない(ユーザーが認識する文字は複数のスカラー値の組み合わせになる場合がある。例: é = e + ́)。これはGoのrune(=int32)に類似している。

複合型のメモリレイアウト

#[repr(C)]
struct C { a: u8, b: u32, c: u16 }
// メモリ: [a:1B][パディング:3B][b:4B][c:2B][パディング:2B] = 12バイト
// C ABI: フィールドは宣言順に配置され、アライメント要件を満たすために自動でパディングが挿入される

struct Rust { a: u8, b: u32, c: u16 }
// デフォルトのレイアウト: コンパイラはフィールドの順序を並べ替えてサイズを最適化できる!
// 例: [b:4B][c:2B][a:1B][パディング:1B] = 8バイト

デフォルトのRustのレイアウトでは順序が保証されない——コンパイラはパディングを減らすためにフィールドの順序を並べ替えることができる。Cとの相互運用や、安定したレイアウトを保証する必要がある場合は、#[repr(C)]を明示する必要がある。

配列: サイズは型の一部

let arr1: [i32; 3] = [1, 2, 3];     // コンパイル時に固定サイズ
let arr2: [i32; 1000] = [0; 1000];   // スタック上に4000バイト!

fn take_arr(arr: [i32; 1000]) { ... }  // 値渡し: 4000バイト全体をコピー
fn take_slice(arr: &[i32]) { ... }     // 参照渡し: ファットポインタ(16バイト)のみコピー

配列を値渡しすると、配列全体がコピーされる——Rustのムーブセマンティクスにおいて、配列は要素ごとにコピーされるためである。大きな配列を扱う場合は、常に&[T]または&[T; N]を渡すこと。

ZST (Zero-Sized Types): コンパイル時に目に見えない魔法

struct NoData;                       // 0バイト
let x = NoData;                      // スタックスペースを一切占有しない
let arr: [NoData; 1_000_000];        // 0バイト!メモリは一切割り当てられない

// ZSTの実用的な価値: HashMapのキー
use std::collections::HashSet;
let set: HashSet<NoData> = ...;      // 「存在」と「非存在」のみを区別する

コンパイラはコード生成時にZSTの格納と渡りを完全に省略する——それらはランタイム上のどこにも存在せず、型レベルでのみ意味を持つ。

Niche Optimization: 零コストのOption

Rustコンパイラは「あり得ないビットパターン」を利用して、enumのメモリ使用量を節約する:

size_of::<Option<bool>>()            // 1バイト、2バイトではない!
// boolの有効な値は0x00 (false) と 0x01 (true) の2つのみ
// コンパイラは0x02をNoneとして表現する → 追加のdiscriminantフィールドが不要

size_of::<Option<&i32>>()            // 8バイト、16バイトではない!
// &Tはnullでないことが保証される → コンパイラはnullptrをNoneとしてエンコードする
// Option<&T>をデリファレンスする際: まずnullかどうかをチェック → Noneならパニック/Noneを返す
// → Someの場合は通常の参照として扱われる

size_of::<Option<Box<i32>>>()        // 8バイト — Box内部はnullでないポインタ
size_of::<Option<Option<&i32>>>()    // 8バイト — 2つのniche: nullptrと...?
// 実際にはOption<&T>にはnicheが1つしかない(null)
// Option<Option<&T>>には2つのniche値が必要 → 16バイト(2ワード)

この最適化は、Rustがシステムレベルの安全性においてゼロオーバーヘッドを維持できる鍵である——Option<&T>に対して追加のメモリやランタイムコストを払う必要はない。

参考

  • Rust Reference: Type Layout (セクション「Data Layout」)
  • Rustonomicon: Exotic Sizes, repr, alignment
  • RFC: RFC 139 (niche optimization)

Keywords: integer types, alignment, repr(C), ZST, niche optimization, Option<&T>, layout, size_of, align_of