![**Zig におけるメモリレイアウト(式付き)**
Zig のメモリモデルは、基盤となるハードウェアを薄く抽象化したものです。
主な概念は **allocators(割り当て器)**、**layouts(配置)**、そして **alignment(整列)** です。以下では、Zig がデータをメモリにどのように並べるかを簡潔にまとめ、必要に応じて数式で表現しています。
---
## 1. 整列規則
| 型 | デフォルト整列(バイト) |
|----|---------------------------|
| `bool`、`i8`、`u8` | 1 |
| `i16`、`u16` | 2 |
| `i32`、`u32`、`f32`| 4 |
| `i64`、`u64`、`f64`| 8 |
| `i128`、`u128`、`f128` | 16 |
**構造体フィールドの整列を求める式**
```
aligned_offset = ceil(previous_end / align_of(field)) * align_of(field)
```
- `previous_end` は前のフィールド直後のオフセットです。
- `ceil(x)` は小数点以下を切り上げて整数にします。
---
## 2. 構造体レイアウト
構造体にフィールド `F₁, F₂, … , Fₙ` があるとき、全体のサイズ **S** は次のように計算されます。
```
offset₁ = 0
for i in 1..n:
offsetᵢ = ceil(offset_{i-1} / align_of(Fᵢ)) * align_of(Fᵢ)
end
size_without_tail_padding = offset_n + size_of(Fₙ)
S = ceil(size_without_tail_padding / max_align) * max_align
```
- `max_align` は全フィールドの中で最も大きい整列要件です。
- 最後のステップで「尾部パディング」を追加し、構造体サイズをその整列に合わせます。
---
## 3. 配列レイアウト
配列 `[N]T` は要素を連続して格納します(間に隙間はありません)。
```
array_size = N * size_of(T)
array_alignment = align_of(T)
```
- `T` が構造体であれば、その内部パディングも各要素に適用されます。
---
## 4. ポインタと参照のレイアウト
- 生ポインタ (`*T`) はマシンのポインタ幅(64‑bit システムでは通常 8 バイト)です。
- Zig の参照 (`&T`) もポインタですが、ライフタイム情報を保持してコンパイル時に安全性チェックが可能です。サイズは生ポインタと同じです。
---
## 5. 動的割り当て
Zig は **allocators** を使ってヒープメモリを管理します。
典型的な割り当てブロックの構造は次のようになります。
```
[ header | payload | footer ]
```
- `header` にはサイズや allocator ID などのメタデータが入ります。
- `footer` はチェックサムやフリーリストリンクを保持する場合があります。
- 正確なフォーマットは allocator に依存します。標準ライブラリの `std.heap.GeneralPurposeAllocator` は 64‑bit システムで 16 バイトのヘッダーを使用しています。
---
## 6. 実例:構造体レイアウト計算
```zig
const Point = struct {
x: f32, // offset 0, size 4, align 4
y: f32, // offset 4, size 4, align 4
};
```
- `offset_x = 0`
- `offset_y = ceil(4 / 4) * 4 = 4`
- `size_without_tail_padding = 4 + 4 = 8`
- `max_align = 4` → 最終サイズは 8(すでに 4 の倍数)。
---
## 7. 要約式
任意の複合型について、レイアウトサイズは次のように求められます。
```
layout_size(type) =
ceil(sum_of_field_sizes_with_alignment / max_align) * max_align
```
この式により、各フィールドが適切に整列されたオフセットで開始し、型全体のサイズもその整列要件を満たすようになります。これにより、Zig がサポートするすべてのアーキテクチャで効率的なメモリアクセスが保証されます。](/_next/image?url=%2Fscreenshots%2F2026-01-25%2F1769298676663.webp&w=3840&q=75)
2026/01/25 0:57
**Zig におけるメモリレイアウト(式付き)** Zig のメモリモデルは、基盤となるハードウェアを薄く抽象化したものです。 主な概念は **allocators(割り当て器)**、**layouts(配置)**、そして **alignment(整列)** です。以下では、Zig がデータをメモリにどのように並べるかを簡潔にまとめ、必要に応じて数式で表現しています。 --- ## 1. 整列規則 | 型 | デフォルト整列(バイト) | |----|---------------------------| | `bool`、`i8`、`u8` | 1 | | `i16`、`u16` | 2 | | `i32`、`u32`、`f32`| 4 | | `i64`、`u64`、`f64`| 8 | | `i128`、`u128`、`f128` | 16 | **構造体フィールドの整列を求める式** ``` aligned_offset = ceil(previous_end / align_of(field)) * align_of(field) ``` - `previous_end` は前のフィールド直後のオフセットです。 - `ceil(x)` は小数点以下を切り上げて整数にします。 --- ## 2. 構造体レイアウト 構造体にフィールド `F₁, F₂, … , Fₙ` があるとき、全体のサイズ **S** は次のように計算されます。 ``` offset₁ = 0 for i in 1..n: offsetᵢ = ceil(offset_{i-1} / align_of(Fᵢ)) * align_of(Fᵢ) end size_without_tail_padding = offset_n + size_of(Fₙ) S = ceil(size_without_tail_padding / max_align) * max_align ``` - `max_align` は全フィールドの中で最も大きい整列要件です。 - 最後のステップで「尾部パディング」を追加し、構造体サイズをその整列に合わせます。 --- ## 3. 配列レイアウト 配列 `[N]T` は要素を連続して格納します(間に隙間はありません)。 ``` array_size = N * size_of(T) array_alignment = align_of(T) ``` - `T` が構造体であれば、その内部パディングも各要素に適用されます。 --- ## 4. ポインタと参照のレイアウト - 生ポインタ (`*T`) はマシンのポインタ幅(64‑bit システムでは通常 8 バイト)です。 - Zig の参照 (`&T`) もポインタですが、ライフタイム情報を保持してコンパイル時に安全性チェックが可能です。サイズは生ポインタと同じです。 --- ## 5. 動的割り当て Zig は **allocators** を使ってヒープメモリを管理します。 典型的な割り当てブロックの構造は次のようになります。 ``` [ header | payload | footer ] ``` - `header` にはサイズや allocator ID などのメタデータが入ります。 - `footer` はチェックサムやフリーリストリンクを保持する場合があります。 - 正確なフォーマットは allocator に依存します。標準ライブラリの `std.heap.GeneralPurposeAllocator` は 64‑bit システムで 16 バイトのヘッダーを使用しています。 --- ## 6. 実例:構造体レイアウト計算 ```zig const Point = struct { x: f32, // offset 0, size 4, align 4 y: f32, // offset 4, size 4, align 4 }; ``` - `offset_x = 0` - `offset_y = ceil(4 / 4) * 4 = 4` - `size_without_tail_padding = 4 + 4 = 8` - `max_align = 4` → 最終サイズは 8(すでに 4 の倍数)。 --- ## 7. 要約式 任意の複合型について、レイアウトサイズは次のように求められます。 ``` layout_size(type) = ceil(sum_of_field_sizes_with_alignment / max_align) * max_align ``` この式により、各フィールドが適切に整列されたオフセットで開始し、型全体のサイズもその整列要件を満たすようになります。これにより、Zig がサポートするすべてのアーキテクチャで効率的なメモリアクセスが保証されます。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Summary
Andrew Kelley の DoD トークは、Zig の型システムとレイアウトプリミティブ(
@alignOf、@sizeOf)が開発者にメモリ構成を明示的に制御する方法に焦点を当て、データ指向設計の核心原則であることを説明しました。彼は次のような正式規則を提示しました:
- 任意の型 T に対して、
かつ@sizeOf(T) ≥ @alignOf(T)
は@alignOf(T)
を割り切ります;両方とも 2 のべき乗です。@sizeOf(T) - 原語型は
を満たします(例:@sizeOf(primitive) = @alignOf(primitive)
、bool
、u8
はすべてサイズ/アラインメント = 8)。*u8 - ビット数は
によりバイトに変換されます。bytes(bits) := max{1, 2^ceil(log₂(bits/8))} - 構造体のアラインメントはそのフィールドのアラインメントの最大値であり、サイズはすべてのフィールドを順序通りに(パディング付き)収めることができるそのアラインメントの最小倍数です。
キーワードは C‑ABI の並び順を強制し、Zig がフィールドをよりタイトに詰めるために再配置するのを防ぎます。extern- 列挙型はバリアント数をカバーする最小限の符号なし整数型を使用しますので、
となり、ここで@alignOf(enum) = @sizeOf(enum) = bytes(b)
です。b = ceil(log₂(len)) - 配列は要素型からアラインメントを継承し、サイズは
です;スライスは 2 つのN * @sizeOf(T)
フィールドを持つ構造体であり、アラインメント = 8、サイズ = 16(64‑bit 系)です。usize - ベア(extern)ユニオンは最大フィールドアラインメントに合わせて整列し、そのサイズは最も大きいフィールドを収める次のそのアラインメントの倍数です。非 extern ユニオンには追加で 1 個分のアラインメントだけのパディングが付加されます。
- タグ付きユニオンはユニオンと列挙型タグを組み合わせたもので、アラインメントは
であり、サイズはmax(@alignOf(union), @alignOf(tag))
を使用します。next_mult(max(field sizes) + @sizeOf(tag), @alignOf(U(E)))
は 64‑bit システム上で固定の 24 バイトのフットプリントを持ち、そのバックバッファはヒープ確保されます;バックバッファのアラインメントはArrayList(T)
、サイズは@alignOf(T)
です。capacity * @sizeOf(T)
は T の各フィールドを別々の配列に格納するため、バックバッファサイズはMultiArrayList(T)
となります。capacity * Σ @sizeOf(field)
これらの規則は、Zig が予測可能で C‑ABI と互換性のあるメモリレイアウトを実現しようとする目的から生じ、言語の明示的なレイアウトプリミティブによって強制されます。トークでは、将来的にこれらの計算をさらに公開または自動化する可能性のある言語機能やコンパイラパスについても触れ、開発者が複雑な構造体やユニオン設計を簡素化できることを示唆しました。システムプログラマ、低レベルライブラリ作者、および高性能または安全性クリティカルコードを必要とする企業にとって、Zig の予測可能なレイアウトはクロスプラットフォームのバイナリ互換性を向上させ、バグを減らし、キャッシュ使用率を最適化します。
本文
2026年1月23日 – 15分で読める記事
最近、Zigの創設者アンドリュー・ケリーによる「データ指向設計(DoD)を適用する実践ガイド」を見るよう勧められました¹。
講義のわずか10分で、私は正式に学んだことがないスキル――型のメモリレイアウトの算術――に直面しました。
プレゼン中、Andrewは様々な型(
u32やboolといったプリミティブから、enumやunionを含むより複雑な構造体まで)について、アラインメントとサイズを計算できるかどうかをテストしました。Zigでのアラインメントとサイズの正確なルールはドキュメントに明記されていませんが、「内輪」の人たちはそれらを理解しています。
低レベルプログラミングに遅れて入門した私は、Zigでアラインメントとサイズを扱う際に得た数式や説明をここにまとめるのが有用だと思いました。
1. メモリレイアウトの原則
メモリに格納されるすべてのデータは2つの重要な性質を持ちます:
| 性質 | 意味 |
|---|---|
| サイズ | 型のインスタンスを表現するために必要なバイト数(パディング込み) |
| アラインメント | コンパイラがその型のアドレスを決める際に従わねばならない規則。 有効なアドレスはこの値の倍数である必要があります。 |
コンパイラは自動的にデータ型をパディングし、アラインメントを満たすように配置します。 これにより、性能低下やクラッシュを招く不揃いアクセスが回避されます。
Zig の組み込みヘルパー
@alignOf(T) // 型 T をアラインするために必要なバイト数 @sizeOf(T) // 型 T を格納するために必要なバイト数(パディング付き)
以下のヘルパーは両方を表示します:
const std = @import("std"); fn memory_printout(T: type) void { std.debug.print("@alignOf( {s} ): {d}\t", .{ @typeName(T), @alignOf(T) }); std.debug.print("@sizeOf( {s} ): {d}\n", .{ @typeName(T), @sizeOf(T) }); }
2. 一般的な不変量
任意の型
T に対して:
@sizeOf(T) ≥ @alignOf(T)
プリミティブ:
プリミティブ型ではサイズ=アラインメントです。
@sizeOf(primitive) = @alignOf(primitive)
整数と浮動小数点は常に 2 のべき乗バイト数で表現されるため、ビットからバイトへの変換式は
bytes(bits) := max{1, 2^ceil(log₂(bits/8))}
したがって:
@alignOf(T) | @sizeOf(T)
つまりサイズは常にそのアラインメントの倍数です。
3. 構造体
アラインメント
@alignOf(struct) = max(@alignOf(field₁), …, @alignOf(fieldₙ))
サイズ(extern 構造体)
C ABI と互換性のある
extern 構造体では、フィールド順序を保持します:
@sizeOf(struct) = next_mult( sum_{fields} @offsetOf(field), @alignOf(struct) )
ここで
next_mult(N, m) := ceil(N/m) * m
各フィールドの配置ルールは次の通りです:
ルール – 前のフィールドの後に、自己アラインメントの最小倍数のアドレスを置く。
例
const ABAB = extern struct { a1: u8, b1: u16, a2: u8, b2: u16, }; const ABBA = extern struct { a1: u8, a2: u8, b1: u16, b2: u16, };
出力:
@alignOf(ABAB): 2 @sizeOf(ABAB): 8 @alignOf(ABBA): 2 @sizeOf(ABBA): 6
構造体のサイズは常にそのアラインメントの倍数です。
パディング対パッキング
extern を付けないと、Zig はパディングを最小化するためにフィールド順序を再配置します。 packed キーワードを使うとすべてのパディングが除去されます(性能低下の可能性があります)。
4. enum
enum のストレージはそのインデックス型で決まります:
@alignOf(enum(u{b})) = @alignOf(u{b}) = bytes(b) @sizeOf (enum(u{b})) = @sizeOf (u{b}) = bytes(b)
Zig はデフォルトで 0 からインデックスを割り当てるため、
b は
ceil(log₂(#variants))
が選ばれます。
例:
const T_default = enum { a, b, c, d, e }; const T_long = enum(u64) { a, b, c, d, e }; memory_printout(T_default); memory_printout(T_long);
出力:
@alignOf( T_default ): 1 @sizeOf( T_default ): 1 @alignOf( T_long ) : 8 @sizeOf( T_long ) : 8
5. 配列とスライス
| 型 | アラインメント | サイズ |
|---|---|---|
| | |
(スライス) | | |
例:
const digits_array = [10]u8{0,1,2,3,4,5,6,7,8,9}; const digits_slice: []const u8 = digits_array[0..]; memory_printout(@TypeOf(digits_array)); memory_printout(@TypeOf(digits_slice));
出力:
@alignOf([10]u8): 1 @sizeOf([10]u8): 10 @alignOf([]const u8): 8 @sizeOf([]const u8): 16
6. union
ベア(extern)union
@alignOf(bare_union) = max(@alignOf(field₁), …) @sizeOf(bare_union) = next_mult(max(@sizeOf(field)), @alignOf(bare_union))
ノンベア union
1 つの追加パディング(アラインメント分)を加える:
@sizeOf(union) = next_mult(max(@sizeOf(field)), @alignOf(union)) + @alignOf(union)
例:
const U_bare = extern union { a: i64, b: extern struct{c:i64,d:i64,e:i64} }; const U = union { a: i64, b: struct {c:i64,d:i64,e:i64} }; memory_printout(U_bare); memory_printout(U);
出力:
@alignOf( U_bare ): 8 @sizeOf( U_bare ): 24 @alignOf( U ) : 8 @sizeOf( U ) : 32
7. タグ付き union
タグ付き union は通常の union に enum タグを加えたものです:
@alignOf(U(E)) = max(@alignOf(U), @alignOf(E)) @sizeOf (U(E)) = next_mult(max(@sizeOf(field)), @alignOf(U(E))) + @sizeOf(E)
3 つの enum(サイズが増える)で例示:
const E = enum { a, b }; const F = enum(u64) { a, b }; const G = enum(u128){ a, b }; const UE = union(E) { a: i64, b: struct{c:i64,d:i64} }; const UF = union(F) { a: i64, b: struct{c:i64,d:i64} }; const UG = union(G) { a: i64, b: struct{c:i64,d:i64} }; memory_printout(UE); memory_printout(UF); memory_printout(UG);
出力:
@alignOf( UE ): 8 @sizeOf( UE ): 24 @alignOf( UF ): 8 @sizeOf( UF ): 24 @alignOf( UG ): 16 @sizeOf( UG ): 32
8. ArrayList と MultiArrayList
| 型 | アラインメント | サイズ |
|---|---|---|
| | (64bit で 24 バイト) |
| 同上 | 同上 |
ArrayList のバックバッファは連続した T のコピーを保持します。サイズ:
buffer_size = capacity * @sizeOf(T)
MultiArrayList は T の各フィールドを個別にタイトにパックされた配列に格納するため、
buffer_size = capacity * Σ @sizeOf(field_i)
例(簡単な構造体):
const T = struct { a: u8, b: u16 }; var list: ArrayList(T) = .{}; var multiList: MultiArrayList(T) = .{}; for (...) |_| { try list.append(allocator, .{ .a = 0, .b = 1 }); try multiList.append(allocator, .{ .a = 0, .b = 1 }); } memory_printout(T); memory_printout(ArrayList(T)); memory_printout(MultiArrayList(T));
出力:
@alignOf( T ): 2 @sizeOf( T ): 4 @alignOf( ArrayList(T) ): 8 @sizeOf( ArrayList(T) ): 24 @alignOf( MultiArrayList(T) ): 8 @sizeOf( MultiArrayList(T) ): 24
9. 実際に試してみた結果
モンスターの ArrayList(19:05)
const Monster = struct { anim: *Animation, kind: Kind, const Kind = enum { snake, bat, wolf, dingo, human }; }; var monsters: ArrayList(Monster) = .{}; while (i < 10_000) : (i += 1) { try monsters.append(.{ .anim = getAnimation(), .kind = rng.enumValue(Monster.Kind), }); }
観測結果
Monster size: 16 bytes ArrayList(Monster) size: 24 bytes monsters size: 160_000 bytes // 10,000 * 16
解説
= 8、@alignOf(anim)
= 1 → 構造体のアラインメントは 8。@alignOf(kind)- 2 つの 8 バイトブロックが必要 →
。@sizeOf(Monster) = 16
はスライス (ArrayList
) と容量([]Monster
)で構成される → サイズ 24 バイト。usize- ヒープ使用量は
バイト。10_000 * 16 = 160,000
脚注
1 – Andrew Kelley の DoD トーク(2026‑01‑23)。