Chapter 04: General Purpose Allocator (GPA)
学習目標
このチャプターでは、Zigの汎用アロケータ(General Purpose Allocator - GPA)の設計思想と実装について学びます。GPAは、多様なメモリ要求に対応できる柔軟なアロケータで、実用的なアプリケーション開発において中心的な役割を果たします。
このチャプターで学ぶこと
- GPAの設計哲学: なぜGPAが必要なのか、どのような問題を解決するのか
- サイズクラスシステム: 効率的なメモリ管理のための分類方式
- フリーリスト構造: 各サイズクラスごとの再利用可能メモリ管理
- スレッドセーフティ: マルチスレッド環境での安全な使用
- 大規模割り当て処理: 大きなメモリブロックの特別な扱い
- 実践的な使用例: CLIツール、ビルドシステムでの活用
General Purpose Allocatorとは
General Purpose Allocator(GPA)は、Zigの標準ライブラリが提供する汎用的なメモリアロケータです。C言語のmalloc/freeに相当しますが、より安全で効率的な設計になっています。
GPAの特徴
const std = @import("std");
pub fn main() !void {
// GPAのインスタンスを作成
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
// プログラム終了時にメモリリークをチェック
const leaked = gpa.deinit();
if (leaked == .leak) {
std.debug.print("メモリリークが検出されました!\n", .{});
}
}
const allocator = gpa.allocator();
// 様々なサイズのメモリを割り当て
const small = try allocator.alloc(u8, 16); // 小さい割り当て
defer allocator.free(small);
const medium = try allocator.alloc(u8, 1024); // 中程度の割り当て
defer allocator.free(medium);
const large = try allocator.alloc(u8, 1024 * 1024); // 大きい割り当て
defer allocator.free(large);
std.debug.print("全ての割り当てが成功しました\n", .{});
}
なぜGPAが必要なのか
問題: 単純なアロケータの限界
前のチャプターで学んだFixedBufferAllocatorやArenaAllocatorは、特定のユースケースには最適ですが、汎用的なアプリケーションには不十分です。
const std = @import("std");
// ❌ 問題例: FixedBufferAllocatorの制限
pub fn problematicFixedBuffer() !void {
var buffer: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
// 小さい割り当てを繰り返すとすぐに容量オーバー
var list = std.ArrayList([]u8).init(allocator);
defer list.deinit();
var i: usize = 0;
while (i < 100) : (i += 1) {
// どこかで OutOfMemory エラーが発生
const data = try allocator.alloc(u8, 32);
try list.append(data);
}
}
// ❌ 問題例: ArenaAllocatorのメモリ浪費
pub fn problematicArena() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); // ここまでメモリが解放されない
const allocator = arena.allocator();
// 長時間動作するサーバーで使用すると...
var i: usize = 0;
while (i < 1000000) : (i += 1) {
// 一時的なデータでもメモリが蓄積される
const temp = try allocator.alloc(u8, 1024);
_ = temp;
// メモリが解放されずに蓄積し続ける!
}
}
解決策: GPAの利点
const std = @import("std");
// ✅ GPAによる解決
pub fn improvedWithGPA() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var list = std.ArrayList([]u8).init(allocator);
defer list.deinit();
var i: usize = 0;
while (i < 100) : (i += 1) {
const data = try allocator.alloc(u8, 32);
try list.append(data);
}
// 個別に解放可能
for (list.items) |item| {
allocator.free(item);
}
list.clearRetainingCapacity();
// メモリが再利用可能な状態に
}
GPAの設計原理
1. サイズクラス分類
GPAは、割り当てサイズに応じてメモリを分類管理します。これにより、フラグメンテーション(断片化)を最小限に抑えます。
サイズクラスの例:
┌─────────────────────────────────────────┐
│ Size Class 0: 8バイト │
│ Size Class 1: 16バイト │
│ Size Class 2: 32バイト │
│ Size Class 3: 64バイト │
│ Size Class 4: 128バイト │
│ Size Class 5: 256バイト │
│ Size Class 6: 512バイト │
│ Size Class 7: 1024バイト │
│ ... │
│ Large Alloc: > 4096バイト │
└─────────────────────────────────────────┘
2. バケット単位の管理
各サイズクラスは、複数のメモリブロックを含む「バケット」として管理されます。
バケット構造(例: 32バイトクラス):
┌────────────────────────────────────────┐
│ Bucket Header │
│ - 使用中ブロック数 │
│ - フリーリストへのポインタ │
├────────────────────────────────────────┤
│ [32B Block 0] ← 使用中 │
│ [32B Block 1] ← フリー │
│ [32B Block 2] ← 使用中 │
│ [32B Block 3] ← フリー │
│ [32B Block 4] ← 使用中 │
│ ... │
└────────────────────────────────────────┘
3. フリーリストによる再利用
解放されたメモリは、即座にOSに返却するのではなく、フリーリストに登録して再利用します。
// フリーリストの概念図
const FreeNode = struct {
next: ?*FreeNode,
};
// 32バイトクラスのフリーリスト:
// head -> [Block A] -> [Block C] -> [Block E] -> null
//
// 新しい割り当て要求が来たら:
// 1. headからブロックを取得 (Block A)
// 2. headを次のノードに更新 (Block C)
// 3. Block Aを返却
GPAの設定オプション
GPAは、用途に応じて様々な設定が可能です。
const std = @import("std");
pub fn main() !void {
// デフォルト設定
var gpa_default = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa_default.deinit();
// デバッグモード有効
var gpa_debug = std.heap.GeneralPurposeAllocator(.{
.safety = true, // メモリ安全性チェックを有効化
}){};
defer _ = gpa_debug.deinit();
// スレッドセーフモード
var gpa_threadsafe = std.heap.GeneralPurposeAllocator(.{
.thread_safe = true, // マルチスレッド対応
}){};
defer _ = gpa_threadsafe.deinit();
// カスタム設定
var gpa_custom = std.heap.GeneralPurposeAllocator(.{
.safety = true,
.thread_safe = true,
.verbose_log = false, // デバッグログを抑制
}){};
defer _ = gpa_custom.deinit();
}
設定オプションの詳細
| オプション | 説明 | デフォルト | 推奨用途 |
|---|---|---|---|
| `safety` | メモリ安全性チェック(use-after-free検出など) | `true` | 開発環境 |
| `thread_safe` | スレッドセーフな動作 | `false` | マルチスレッドアプリ |
| `verbose_log` | 詳細なデバッグログ出力 | `false` | デバッグ時 |
基本的な使用パターン
パターン1: 長時間動作するアプリケーション
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Webサーバーのようなアプリケーション
while (true) {
// リクエストごとに一時的なメモリを割り当て
const request_buffer = try allocator.alloc(u8, 4096);
defer allocator.free(request_buffer);
// リクエスト処理...
processRequest(request_buffer);
// deferによって自動的に解放され、メモリが再利用される
}
}
fn processRequest(buffer: []u8) void {
_ = buffer;
// リクエスト処理のロジック
}
パターン2: 動的データ構造
const std = @import("std");
const User = struct {
id: u32,
name: []const u8,
email: []const u8,
};
pub fn manageUsers(allocator: std.mem.Allocator) !void {
var users = std.ArrayList(User).init(allocator);
defer users.deinit();
// ユーザーを動的に追加
try users.append(.{
.id = 1,
.name = "Alice",
.email = "alice@example.com",
});
try users.append(.{
.id = 2,
.name = "Bob",
.email = "bob@example.com",
});
// ユーザーの削除(メモリは自動的に管理される)
_ = users.orderedRemove(0);
// 残りのユーザーを処理
for (users.items) |user| {
std.debug.print("User {}: {s}\n", .{ user.id, user.name });
}
}
パターン3: 異なるライフタイムのメモリ管理
const std = @import("std");
pub fn complexMemoryManagement(allocator: std.mem.Allocator) !void {
// 短命なメモリ
{
const temp = try allocator.alloc(u8, 1024);
defer allocator.free(temp);
// 一時的な計算に使用
performCalculation(temp);
}
// 長命なメモリ
const cache = try allocator.alloc(u8, 1024 * 1024);
defer allocator.free(cache);
// キャッシュは長期間保持される
var i: usize = 0;
while (i < 1000) : (i += 1) {
// 各イテレーションで一時メモリを使用
const iteration_buffer = try allocator.alloc(u8, 512);
defer allocator.free(iteration_buffer);
useBuffer(iteration_buffer, cache);
}
}
fn performCalculation(buffer: []u8) void {
_ = buffer;
}
fn useBuffer(buffer: []u8, cache: []u8) void {
_ = buffer;
_ = cache;
}
パフォーマンス特性
メモリ割り当てのコスト
const std = @import("std");
pub fn benchmarkAllocations(allocator: std.mem.Allocator) !void {
const iterations = 10000;
// 小さい割り当て(高速)
var timer = try std.time.Timer.start();
var i: usize = 0;
while (i < iterations) : (i += 1) {
const small = try allocator.alloc(u8, 32);
allocator.free(small);
}
const small_time = timer.read();
// 大きい割り当て(中速)
timer.reset();
i = 0;
while (i < iterations) : (i += 1) {
const large = try allocator.alloc(u8, 4096);
allocator.free(large);
}
const large_time = timer.read();
std.debug.print("小割り当て: {}ns/op\n", .{small_time / iterations});
std.debug.print("大割り当て: {}ns/op\n", .{large_time / iterations});
}
メモリフラグメンテーション
GPAは、サイズクラスシステムにより内部フラグメンテーションを制限します。
内部フラグメンテーションの例:
┌─────────────────────────────────────┐
│ 要求: 48バイト │
│ 割り当て: 64バイト(Size Class 3) │
│ 無駄: 16バイト (25%) │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 要求: 500バイト │
│ 割り当て: 512バイト(Size Class 6) │
│ 無駄: 12バイト (2.4%) │
└─────────────────────────────────────┘
まとめ
このチャプターでは、General Purpose Allocatorの基礎を学びました。
重要ポイント
次のステップ
次のセクション「Explanation 04」では、GPAの内部実装について詳しく学びます:
理解度チェック
以下の質問に答えられるか確認してください:
次のセクションでは、これらの概念をより深く掘り下げていきます。