![**Zig における静的割り当て**
Zig のコンパイル時メモリ管理を使えば、実行時ではなくコンパイル時にストレージを確保できます。データ構造のサイズが事前に分かっている場合やヒープ割り当てを避けたいときに便利です。
### 重要概念
- **コンパイル時定数**
`const` や `comptime` の値を使い、コンパイラがコンパイル中に評価できるサイズを記述します。
- **固定長配列**
リテラルサイズで配列を宣言します。
```zig
const buf = [_]u8{0} ** 128; // 128 バイト、すべてゼロ初期化
```
- **静的フィールドを持つ構造体**
固定長配列やその他コンパイル時に決まる型を含む構造体を定義します。
### 例
```zig
const std = @import("std");
// 静的サイズのバッファを持つ構造体
pub const Message = struct {
id: u32,
payload: [256]u8, // 256 バイト、コンパイル時に確保
};
// 静的割り当てを使う関数
fn process(msg: *Message) void {
// ヒープ割り当ては不要;msg はスタック上またはグローバルに存在
std.debug.print("ID: {d}\n", .{msg.id});
}
pub fn main() !void {
var msg = Message{
.id = 42,
.payload = [_]u8{0} ** 256, // すべてのバイトをゼロで初期化
};
process(&msg);
}
```
### 利点
- **決定的なメモリ使用量** – サイズはコンパイル時に分かる
- **実行時割り当てオーバーヘッドがゼロ** – ヒープアロケータ呼び出しなし
- **安全性** – コンパイラが境界と寿命を検証できる
### 使うべき場面
- 固定長バッファ(例:ネットワークパケット、ファイルヘッダー)
- 短時間しか存続しない小規模補助データ構造
- 性能や決定的な動作が重要な状況
---
コンパイル時定数・固定配列・構造体定義を活用することで、Zig は最小限のボイラープレートで最大の安全性を保ちつつメモリを静的に割り当てることができます。](/_next/image?url=%2Fscreenshots%2F2025-12-30%2F1767047772973.webp&w=3840&q=75)
2025/12/30 1:07
**Zig における静的割り当て** Zig のコンパイル時メモリ管理を使えば、実行時ではなくコンパイル時にストレージを確保できます。データ構造のサイズが事前に分かっている場合やヒープ割り当てを避けたいときに便利です。 ### 重要概念 - **コンパイル時定数** `const` や `comptime` の値を使い、コンパイラがコンパイル中に評価できるサイズを記述します。 - **固定長配列** リテラルサイズで配列を宣言します。 ```zig const buf = [_]u8{0} ** 128; // 128 バイト、すべてゼロ初期化 ``` - **静的フィールドを持つ構造体** 固定長配列やその他コンパイル時に決まる型を含む構造体を定義します。 ### 例 ```zig const std = @import("std"); // 静的サイズのバッファを持つ構造体 pub const Message = struct { id: u32, payload: [256]u8, // 256 バイト、コンパイル時に確保 }; // 静的割り当てを使う関数 fn process(msg: *Message) void { // ヒープ割り当ては不要;msg はスタック上またはグローバルに存在 std.debug.print("ID: {d}\n", .{msg.id}); } pub fn main() !void { var msg = Message{ .id = 42, .payload = [_]u8{0} ** 256, // すべてのバイトをゼロで初期化 }; process(&msg); } ``` ### 利点 - **決定的なメモリ使用量** – サイズはコンパイル時に分かる - **実行時割り当てオーバーヘッドがゼロ** – ヒープアロケータ呼び出しなし - **安全性** – コンパイラが境界と寿命を検証できる ### 使うべき場面 - 固定長バッファ(例:ネットワークパケット、ファイルヘッダー) - 短時間しか存続しない小規模補助データ構造 - 性能や決定的な動作が重要な状況 --- コンパイル時定数・固定配列・構造体定義を活用することで、Zig は最小限のボイラープレートで最大の安全性を保ちつつメモリを静的に割り当てることができます。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
概要:
このプロジェクトは、Zigで書かれた軽量Redis互換のキー/バリューサーバー「kv」を構築し、最小限のコマンドセットで本番環境に適した設計を目指しています。コアデザインでは起動時にすべてのメモリを確保することで、実行中にダイナミックヒープを使用せず、レイテンシスパイクやユース・アフター・フリー(use‑after‑free)バグを回避します。接続はで非同期に処理され、システムは3つのプール(Connection、受信バッファプール、送信バッファプール)を事前確保し、デフォルトでは約1000件までの同時接続数をサポートします。各接続は設定パラメータから派生した固定サイズの受信/送信バッファを使用します。io_uring
コマンド解析はRedisのRESPプロトコルのサブセットに従い、Zigのを用いてゼロコピーで解析し、各リクエスト後にアロケータをリセットします。バッファサイズはstd.heap.FixedBufferAllocatorとlist_length_maxに依存します。val_size_max
ストレージは未管理型のを使用し、初期化時にStringHashMapUnmanaged(Value)で容量を確保します。キーと値は共有ensureTotalCapacityに格納され、マップはポインタのみを保持します。削除操作では墓石(tombstone)が残り、墓石数が増えると再ハッシュが必要になる場合があります。ByteArrayPool
設定構造体()はConfig、connections_max、key_count、key_size_max、val_size_maxなどのフィールドを公開し、派生アロケーションで接続ごとのバッファサイズを決定します。デフォルト設定(総計約748 MB、2048エントリ)ではlist_length_maxまたはval_size_maxを倍増すると、割り当て量が約2.8 GBに上昇する可能性があります。list_length_max
今後の作業としては、カスタム静的コンテキストマップ実装の改善、より良いメモリ利用を実現する代替アロケータの探索、境界検査(fuzz)テストの追加による限界確認、および墓石再ハッシュ処理への対応が挙げられます。
本文
Redis 互換キー/バリュー ストアにおける静的メモリアロケーション
私は kv という、Zig で書かれた小規模な Redis 互換キー/バリュースーバーの開発を行っています。
目的は、実運用レベルに近づけつつ、コマンドを極力少なく実装することです。
私が徹底している設計原則の一つが 静的メモリアロケーション です:サーバー起動時に OS から必要な全てのメモリを確保し、以降は再度割り当ても解放もしません。このパターンは use‑after‑free のような予測不能な挙動を排除し、性能解析を簡素化し、結果としてより効率的な設計へと導きます。
なぜ静的アロケーションなのか?
サーバーが起動した時点で、設定に基づいて必要メモリ量を正確に算出できます:
- 最大同時接続数
- キー・値・リストのサイズ上限
- プロトコル要件(RESP)
事前に答えが分かれば、すべてを先に割り当てることができ、起動後に OS へメモリアロケーションを要求する必要がなくなります。
高レベルアーキテクチャ
kv はリクエストを三段階で処理します:
- 接続ハンドリング – ソケット受け付け、I/O バッファリング
- コマンドパース – RESP を解釈
- キー/バリュー格納 – ハッシュマップ + リストバックエンド
各段階で専用のプール(事前割り当て済みオブジェクトやバッファ)を使用します。
接続ハンドリング
const Connection = struct { completion: Completion, client: posix.socket_t, recv_buffer: *ByteArray, send_buffer: *ByteArray, };
初期化時に次のプールを作成します:
構造体Connection- 受信バッファ (
)recv_buffers - 送信バッファ (
)send_buffers
const ConnectionPool = struct { const Pool = std.heap.MemoryPoolExtra(Connection, .{ .growable = false }); recv_buffers: ByteArrayPool, send_buffers: ByteArrayPool, connections: Pool, fn init(config: Config, gpa: std.mem.Allocator) !ConnectionPool { const allocation = config.allocation(); const pool = try Pool.initPreheated(gpa, config.connections_max); const recv_buffers = try ByteArrayPool.init(gpa, config.connections_max, allocation.connection_recv_size); const send_buffers = try ByteArrayPool.init(gpa, config.connections_max, allocation.connection_send_size); return .{ .recv_buffers = recv_buffers, .send_buffers = send_buffers, .connections = pool, }; } };
クライアントが接続すると、プールから
Connection を取り出し、新しいバッファを割り当てます。利用可能な接続が無ければリクエストは拒否されます。
コマンドパース
Redis のコマンドは RESP 形式で届きます。例:
*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n
パースには入力バッファのゼロコピースライスだけが必要です。
各リクエスト後にリセットされる
std.heap.FixedBufferAllocator を使用します。
pub const Runner = struct { config: Config, fba: std.heap.FixedBufferAllocator, kv: *Store, pub fn init(config: Config, gpa: std.mem.Allocator, kv: *Store) !Runner { const L = config.list_length_max; const V = config.val_size_max; // 最大コマンドサイズの容量 const parse_cap = (1 + 1 + L); const parse_size = parse_cap * @sizeOf([]const u8); // 応答で値をコピーするためのスペース const copy_size = L * @sizeOf([]const u8); const copy_data = L * V; const fba_size = parse_size + copy_size + copy_data; const buffer = try gpa.alloc(u8, fba_size); const fba = std.heap.FixedBufferAllocator.init(buffer); return .{ .config = config, .fba = fba, .kv = kv }; } };
このアロケータはすべてのリクエストで再利用され、応答を書き込んだ後に
fba をリセットします。
キー/バリュー格納
ハッシュマップにキーと値を保存します。静的割り当てが必要なので、unmanaged マップを使用します:
var map: std.StringHashMapUnmanaged(Value) = .empty; try map.ensureTotalCapacity(gpa, capacity); // 内部テーブルのみ確保 // 後で挿入する際は map.putAssumeCapacity(key, value);
ensureTotalCapacity は内部表だけを割り当てます。ユーザーデータ(キーと値)は専用の
ByteArrayPool から取得します。各キーは最大 list_length_max 要素まで保持できるように、最悪ケース分のスペースを確保しています。
削除と墓石
unmanaged マップは線形探索(linear probing)でオープンアドレスリングを行います。キー削除時に検索を壊さないために tombstone を使って論理的に削除済みとマークします。静的環境では、墓石が集まる前に十分なスペースを確保しておけば再ハッシュは不要です。
割り当てサイズの計算
Config 構造体にユーザー設定可能な上限値を保持し、派生値は allocation() で算出します:
pub const Config = struct { pub const Allocation = struct { connection_recv_size: u64, connection_send_size: u64, }; connections_max: u32, key_count: u32, key_size_max: u32, val_size_max: u32, list_length_max: u32, pub fn allocation(config: Config) Allocation { // RESP プロトコルと設定に基づくサイズ計算 return .{ .connection_recv_size = ..., .connection_send_size = ... }; } };
典型的な設定で実行すると:
$ zig build run config connections_max = 1000 key_count = 1000 key_size_max = 1024 val_size_max = 4096 list_length_max = 50 allocation connection_recv_size = 206299 connection_send_size = 205255 map capacity = 2048, map size = 0, available = 1638 total_requested_bytes = 748213015 ready!
約 750 MB が一連の上限で事前確保されます。
val_size_max や list_length_max を増やすと線形に拡張し、両方を倍にすると約 2.8 GB になります。
最後に
静的アロケーションは推論を簡素化し、決定論的性能を保証します。
サイズ調整が必要ですが、動的割り当てのオーバーヘッドや断片化を避けたいサーバーには十分価値があります。
今後の課題:
- 静的利用に最適化したハッシュマップ(墓石処理の改善)
- メモリ効率向上のための代替アロケータ検討
- エッジケースを洗い出し、限界値を検証するファズテスト追加
ソースコードは GitHub で公開しています。ぜひ別設定で
total_requested_bytes がどう変わるか試してみてください!