# TLS SLL アーキテクチャ改善と今後の開発方針 **調査日**: 2025-11-28 **対象**: Larson ベンチマーク double-free バグの根本原因と包括的なアーキテクチャ改善 --- ## エグゼクティブサマリー ### 根本原因の特定 Larson ベンチマークでの double-free バグの根本原因は、**TLS SLL と `meta->used` の同期不整合**です: 1. **TLS SLL 操作時に `meta->used` が更新されない** - `tls_sll_push()`: ポインタを TLS SLL に追加するが `meta->used` は変更なし - `tls_sll_pop()`: ポインタを TLS SLL から取り出すが `meta->used` は変更なし - Drain 時のみ: `tiny_free_local_box()` 経由で `meta->used--` される 2. **Slab 再利用時の TLS SLL クリア漏れ** - Slab が `meta->used == 0` で empty と判定される - しかし TLS SLL には古いポインタが残っている - Slab が再初期化されて別のアドレス範囲で使われる - 古いポインタが再度 allocate されて重複検出 3. **既知のバグ(既に修正済み)** - TLS Drain pushback バグ: `tls_sll_drain_box.h:148-162` (commit c2f104618 で修正) - ポインタが TLS SLL の 2 箇所に存在する状態を作り出していた ### 影響範囲 - **確実に影響**: Larson ベンチマーク(マルチスレッド、高頻度 alloc/free) - **潜在的影響**: 本番環境での長時間稼働時のメモリリーク・重複 free - **性能への影響**: 現在の periodic drain (2048 frees ごと) では不十分な可能性 --- ## Part 1: 現状分析 ### 1.1 TLS SLL の全体構造 #### 主要ファイル | ファイル | 役割 | 行数 | |---------|------|------| | `/mnt/workdisk/public_share/hakmem/core/box/tls_sll_box.h` | TLS SLL のコア実装 (push/pop/splice) | 753行 | | `/mnt/workdisk/public_share/hakmem/core/box/tls_sll_drain_box.h` | Periodic drain (Option B) | 270行 | | `/mnt/workdisk/public_share/hakmem/core/tiny_free_fast_v2.inc.h` | Fast free path (TLS SLL への push) | 370行 | | `/mnt/workdisk/public_share/hakmem/core/box/free_local_box.c` | Slow free path (`meta->used--`) | 210行 | #### TLS SLL の設計 ```c // TLS SLL 構造体 (hakmem_tiny.c で定義) typedef struct { void* head; // リストの先頭ポインタ (BASE pointer) uint32_t count; // リスト内の要素数 } TinyTLSSLL; extern __thread TinyTLSSLL g_tls_sll[TINY_NUM_CLASSES]; // クラスごとに1つ ``` **重要な設計原則**: - すべてのポインタは **BASE ポインタ** (user pointer - 1 byte) - クラス 0 と 7 は `next_offset = 0` (header を上書きしない) - クラス 1-6 は `next_offset = 1` (header を書き戻す) ### 1.2 `meta->used` の更新箇所 #### Increment 箇所 (alloc 時) | ファイル | 行番号 | 条件 | |---------|-------|------| | `tiny_superslab_alloc.inc.h` | 30, 102, 146, 323, 363, 387 | 線形 carve / freelist pop | | `hakmem_tiny_refill_p0.inc.h` | 126, 131, 200 | バッチ refill | | `hakmem_tiny_tls_ops.h` | 93, 136 | TLS slab refill | | `slab_handle.h` | 313 | SlabHandle 経由の操作 | #### Decrement 箇所 (free 時) | ファイル | 行番号 | 条件 | |---------|-------|------| | `free_local_box.c` | 180 | **唯一の decrement 箇所** | | `hakmem_tiny_bg_spill.c` | 72, 81 | バックグラウンド spill | | `hakmem_tiny_tls_ops.h` | 198 | TLS spill | | `slab_handle.h` | 276 | SlabHandle 経由の free | **重要な発見**: `meta->used` の decrement は **`tiny_free_local_box()` を経由した場合のみ** 実行される。 ### 1.3 ポインタのライフサイクル ``` [状態遷移図] CARVED (meta->used++) | v ALLOCATED (ユーザーが使用中) | v (free 呼び出し) | +---> [FAST PATH] TLS SLL に push (meta->used は変更なし) ----+ | | | v | (periodic drain) | | +---> [SLOW PATH] tiny_free_local_box() --------------------+ | v FREELIST (meta->used--, freelist に追加) | v (再利用時) | v ALLOCATED ``` **問題点**: - Fast path (99% のケース) では `meta->used` が更新されない - Periodic drain (2048 frees ごと) まで `meta->used` は高いまま - この間、Slab は "full" に見える → 再利用されない → メモリリーク ### 1.4 Slab 再利用のコードパス #### Empty 判定条件 ```c // ss_hot_cold_box.h:37-39 static inline bool ss_is_slab_empty(const TinySlabMeta* meta) { return (meta->capacity > 0 && meta->used == 0); } ``` #### Empty マーク ```c // free_local_box.c:183-202 if (meta->used == 0) { // Slab became EMPTY → mark for highest-priority reuse ss_mark_slab_empty(ss, slab_idx); fprintf(stderr, "[FREE_LOCAL_BOX] EMPTY detected: cls=%u ss=%p slab=%d\n", cls, (void*)ss, slab_idx); } ``` #### Slab 再利用フロー 1. `shared_pool_acquire_slab()` (hakmem_shared_pool.c:700-1300) - **Stage 1**: EMPTY slots を優先的に再利用 (empty_mask をスキャン) - **Stage 2**: UNUSED slots を探す - **Stage 3**: 新しい SuperSlab を割り当て 2. `superslab_init_slab()` (初期化時) - `meta->used = 0` - `meta->freelist = NULL` - `meta->class_idx = class_idx` を設定 **重要な問題**: Slab が EMPTY と判定されて再初期化される時、**TLS SLL はクリアされない**。 ### 1.5 既存の診断機能 #### 環境変数ゲート | 環境変数 | 機能 | ファイル | |---------|------|---------| | `HAKMEM_TINY_SLL_DIAG` | TLS SLL の詳細診断 | tls_sll_box.h:118 | | `HAKMEM_TINY_SLL_DRAIN_ENABLE` | Periodic drain の有効/無効 | tls_sll_drain_box.h:40 | | `HAKMEM_TINY_SLL_DRAIN_INTERVAL` | Drain 間隔 (デフォルト 2048) | tls_sll_drain_box.h:54 | | `HAKMEM_TINY_SLL_DRAIN_DEBUG` | Drain の詳細ログ | tls_sll_drain_box.h:121 | | `HAKMEM_TINY_SLL_SAFEHEADER` | Header 上書き防止モード | tls_sll_box.h:336 | | `HAKMEM_TINY_LARSON_FIX` | クロススレッド free 検出 | tiny_free_fast_v2.inc.h:189 | #### 重複チェック ```c // tls_sll_box.h:372-402 (debug ビルドのみ) #if !HAKMEM_BUILD_RELEASE void* scan = g_tls_sll[class_idx].head; uint32_t scanned = 0; const uint32_t limit = 256; // 最大 256 要素をスキャン while (scan && scanned < limit) { if (scan == ptr) { fprintf(stderr, "[TLS_SLL_PUSH_DUP] cls=%d ptr=%p ...\n"); abort(); // 重複検出時は即座に abort } // ... } #endif ``` --- ## Part 2: 修正案の設計 ### 2.1 即座のバグ修正(最小変更) #### 修正 A: TLS SLL クリア on Slab Reuse **ファイル**: `/mnt/workdisk/public_share/hakmem/core/tiny_superslab_alloc.inc.h` **場所**: `superslab_refill()` 関数内 (行 192-265) **問題**: Slab が再初期化される時、TLS SLL に古いポインタが残る。 **修正案**: ```c // superslab_refill() 内 (行 243 の後に追加) // CRITICAL FIX: Clear TLS SLL for this class on slab reuse // Problem: When slab is recycled (EMPTY → reinitialized), old pointers // remain in TLS SLL, causing double-free on next allocation // Solution: Drain TLS SLL to ensure no stale pointers remain #if !HAKMEM_BUILD_RELEASE { extern __thread TinyTLSSLL g_tls_sll[TINY_NUM_CLASSES]; uint32_t old_count = g_tls_sll[class_idx].count; if (old_count > 0) { // Drain ALL blocks from TLS SLL to freelist (slow but safe) extern uint32_t tiny_tls_sll_drain(int class_idx, uint32_t batch_size); uint32_t drained = tiny_tls_sll_drain(class_idx, 0); // 0 = drain all fprintf(stderr, "[SUPERSLAB_REFILL_TLS_DRAIN] cls=%d drained=%u from TLS SLL\n", class_idx, drained); } } #endif ``` **影響**: - **本番ビルド**: 変更なし (debug only) - **開発ビルド**: Slab refill 時に TLS SLL を強制的に drain - **性能**: 無視できる (refill は低頻度、O(count) の drain) **副作用**: - なし(防御的コード、性能への影響は最小) --- #### 修正 B: Drain 間隔の動的調整 **ファイル**: `/mnt/workdisk/public_share/hakmem/core/box/tls_sll_drain_box.h` **場所**: `tls_sll_drain_get_interval()` (行 54-73) **問題**: 現在の固定間隔 (2048 frees) では、ワークロードによっては不十分。 **修正案**: ```c // Dynamic drain interval based on workload static inline uint32_t tls_sll_drain_get_interval_dynamic(int class_idx) { static uint32_t g_drain_interval_base = 2048; // デフォルト static int g_adaptive_drain = -1; if (__builtin_expect(g_adaptive_drain == -1, 0)) { const char* env = getenv("HAKMEM_TINY_SLL_DRAIN_ADAPTIVE"); g_adaptive_drain = (env && *env && *env != '0') ? 1 : 0; } if (!g_adaptive_drain) { return g_drain_interval_base; // 固定間隔モード } // Adaptive: Hot classes (0-3) drain more frequently switch (class_idx) { case 0: return 512; // 8B - 最頻使用 case 1: return 768; // 16B case 2: return 768; // 32B case 3: return 1024; // 64B case 4: return 1536; // 128B case 5: return 2048; // 256B case 6: return 2048; // 512B case 7: return 2048; // 2048B default: return g_drain_interval_base; } } ``` **使い方**: ```bash # Adaptive drain を有効化 export HAKMEM_TINY_SLL_DRAIN_ADAPTIVE=1 # または特定クラスの間隔を手動調整 export HAKMEM_TINY_SLL_DRAIN_INTERVAL=512 ``` **影響**: - **性能**: Hot classes の drain 頻度 ↑ → `meta->used` の正確性 ↑ → empty detection ↑ - **メモリ**: TLS SLL の最大サイズ ↓ → footprint ↓ --- ### 2.2 同期マクロの導入 #### 設計: `TLS_SLL_PUSH_TRACKED` / `TLS_SLL_POP_TRACKED` **目的**: TLS SLL 操作時に `meta->used` を追跡する(将来的な同期のための基盤) **ファイル**: 新規作成 `/mnt/workdisk/public_share/hakmem/core/box/tls_sll_tracked_box.h` ```c #pragma once #include "tls_sll_box.h" #include "hakmem_tiny_superslab.h" #include "superslab/superslab_inline.h" // TLS_SLL_PUSH_TRACKED: Push to TLS SLL with optional meta->used tracking // // Args: // cls: Size class index // ptr: BASE pointer to push // cap: Capacity limit // ss: SuperSlab pointer (for meta->used tracking, can be NULL) // slab_idx: Slab index (for meta->used tracking, can be -1) // // Returns: true on success, false on failure // // Behavior: // - Always pushes to TLS SLL (normal path) // - If ss != NULL && slab_idx >= 0: optionally track in meta->used (env-gated) // static inline bool TLS_SLL_PUSH_TRACKED(int cls, void* ptr, uint32_t cap, SuperSlab* ss, int slab_idx) { // Standard TLS SLL push if (!tls_sll_push(cls, ptr, cap)) { return false; } #if !HAKMEM_BUILD_RELEASE // Optional: Track TLS SLL inventory in meta->used_tls_sll (future work) // ENV: HAKMEM_TINY_TLS_SLL_TRACK_USED=1 static int g_track_used = -1; if (__builtin_expect(g_track_used == -1, 0)) { const char* env = getenv("HAKMEM_TINY_TLS_SLL_TRACK_USED"); g_track_used = (env && *env && *env != '0') ? 1 : 0; } if (g_track_used && ss && slab_idx >= 0 && slab_idx < ss_slabs_capacity(ss)) { // Future: Increment ss->slabs[slab_idx].used_tls_sll // Note: Requires adding new field to TinySlabMeta // For now, just log (diagnostic mode) static _Atomic uint64_t g_track_log = 0; uint64_t n = atomic_fetch_add(&g_track_log, 1); if (n < 10) { fprintf(stderr, "[TLS_SLL_TRACK] PUSH cls=%d ptr=%p ss=%p slab=%d\n", cls, ptr, (void*)ss, slab_idx); } } #else (void)ss; (void)slab_idx; #endif return true; } // TLS_SLL_POP_TRACKED: Pop from TLS SLL with optional meta->used tracking static inline bool TLS_SLL_POP_TRACKED(int cls, void** out, SuperSlab** ss_out, int* slab_idx_out) { // Standard TLS SLL pop if (!tls_sll_pop(cls, out)) { return false; } // Resolve SuperSlab/slab for caller (optional metadata) if (ss_out && slab_idx_out) { SuperSlab* ss = hak_super_lookup(*out); if (ss && ss->magic == SUPERSLAB_MAGIC) { int sidx = slab_index_for(ss, *out); if (sidx >= 0 && sidx < ss_slabs_capacity(ss)) { *ss_out = ss; *slab_idx_out = sidx; #if !HAKMEM_BUILD_RELEASE static int g_track_used = -1; if (__builtin_expect(g_track_used == -1, 0)) { const char* env = getenv("HAKMEM_TINY_TLS_SLL_TRACK_USED"); g_track_used = (env && *env && *env != '0') ? 1 : 0; } if (g_track_used) { // Future: Decrement ss->slabs[sidx].used_tls_sll static _Atomic uint64_t g_track_log = 0; uint64_t n = atomic_fetch_add(&g_track_log, 1); if (n < 10) { fprintf(stderr, "[TLS_SLL_TRACK] POP cls=%d ptr=%p ss=%p slab=%d\n", cls, *out, (void*)ss, sidx); } } #endif return true; } } } return true; } ``` **使い方**: ```c // Before (existing code) tls_sll_push(class_idx, base, UINT32_MAX); // After (with tracking) TLS_SLL_PUSH_TRACKED(class_idx, base, UINT32_MAX, ss, slab_idx); ``` **段階的導入**: 1. Phase 1: マクロ導入、ログのみ(診断用) 2. Phase 2: `TinySlabMeta.used_tls_sll` フィールド追加 3. Phase 3: `meta->used` の代わりに `meta->used + meta->used_tls_sll` で empty 判定 --- ### 2.3 Slab 再利用ガードの設計 #### 設計: `tiny_slab_can_reuse()` **目的**: Slab を再利用する前に、TLS SLL に古いポインタが残っていないことを確認 **ファイル**: 新規作成 `/mnt/workdisk/public_share/hakmem/core/box/slab_reuse_guard_box.h` ```c #pragma once #include "hakmem_tiny_superslab.h" #include "hakmem_tiny_config.h" #include #include // ============================================================================ // Slab Reuse Guard: Prevent reuse when TLS SLL contains stale pointers // ============================================================================ // // Problem: // - Slab marked as EMPTY (meta->used == 0) // - But TLS SLL still contains pointers from this slab // - Slab gets reinitialized for different class // - Old pointers in TLS SLL get allocated → double-free // // Solution: // - Before reusing EMPTY slab, check if TLS SLL contains pointers from it // - If yes, force drain TLS SLL first (or skip reuse) // // Design: // - Development builds: strict check + force drain // - Production builds: best-effort check (optional, env-gated) // Check if TLS SLL contains pointers from given slab // Returns: number of pointers found in TLS SLL from this slab static inline uint32_t tiny_slab_count_tls_sll_refs(SuperSlab* ss, int slab_idx, int class_idx) { if (!ss || slab_idx < 0 || slab_idx >= ss_slabs_capacity(ss)) { return 0; } extern __thread TinyTLSSLL g_tls_sll[TINY_NUM_CLASSES]; // Calculate slab address range uint8_t* slab_base = tiny_slab_base_for(ss, slab_idx); uintptr_t slab_start = (uintptr_t)slab_base; uintptr_t slab_end = slab_start + SLAB_SIZE; // Scan TLS SLL for this class uint32_t found = 0; void* cur = g_tls_sll[class_idx].head; uint32_t scanned = 0; const uint32_t limit = 1024; // Scan up to 1024 entries while (cur && scanned < limit) { uintptr_t addr = (uintptr_t)cur; // Check if pointer is in this slab's range if (addr >= slab_start && addr < slab_end) { found++; } // Get next pointer void* next = NULL; PTR_NEXT_READ("reuse_guard_scan", class_idx, cur, 0, next); cur = next; scanned++; } return found; } // Guard: Check if slab can be safely reused // Returns: true if safe to reuse, false if TLS SLL drain needed static inline bool tiny_slab_can_reuse(SuperSlab* ss, int slab_idx, int class_idx) { #if !HAKMEM_BUILD_RELEASE // Development: Always check for safety uint32_t refs = tiny_slab_count_tls_sll_refs(ss, slab_idx, class_idx); if (refs > 0) { fprintf(stderr, "[SLAB_REUSE_GUARD] BLOCKED: cls=%d ss=%p slab=%d has %u refs in TLS SLL\n", class_idx, (void*)ss, slab_idx, refs); // Force drain TLS SLL to clear stale pointers extern uint32_t tiny_tls_sll_drain(int class_idx, uint32_t batch_size); uint32_t drained = tiny_tls_sll_drain(class_idx, 0); // Drain all fprintf(stderr, "[SLAB_REUSE_GUARD] Forced drain: cls=%d drained=%u blocks\n", class_idx, drained); // Re-check after drain refs = tiny_slab_count_tls_sll_refs(ss, slab_idx, class_idx); if (refs > 0) { fprintf(stderr, "[SLAB_REUSE_GUARD] CRITICAL: Still %u refs after drain! (memory corruption?)\n", refs); return false; // Unsafe to reuse } } return true; // Safe to reuse #else // Production: Optional check (env-gated for minimal overhead) static int g_reuse_guard = -1; if (__builtin_expect(g_reuse_guard == -1, 0)) { const char* env = getenv("HAKMEM_TINY_SLAB_REUSE_GUARD"); g_reuse_guard = (env && *env && *env != '0') ? 1 : 0; } if (!g_reuse_guard) { return true; // Guard disabled in production } // Same logic as development build uint32_t refs = tiny_slab_count_tls_sll_refs(ss, slab_idx, class_idx); if (refs > 0) { // Production: Just skip reuse (don't force drain to avoid latency spike) return false; } return true; #endif } ``` **統合場所**: `hakmem_shared_pool.c` の `shared_pool_acquire_slab()` ```c // shared_pool_acquire_slab() 内 (Stage 1: EMPTY slot reuse の前) // Before reusing EMPTY slot, check TLS SLL safety if (!tiny_slab_can_reuse(ss, slot_idx, class_idx)) { // Skip this EMPTY slot (TLS SLL drain needed first) fprintf(stderr, "[ACQUIRE_SLAB] Skipped EMPTY slot due to TLS SLL refs\n"); continue; // Try next slot } ``` **影響**: - **開発ビルド**: 常に有効、TLS SLL の自動 drain 付き - **本番ビルド**: ENV で有効化可能、drain なし(スキップのみ) - **性能**: 開発ビルドで O(TLS_SLL_count) のスキャン、本番では無視できる --- ## Part 3: アーキテクチャ改善 ### 3.1 ポインタライフサイクル管理 #### 状態遷移図(改善版) ``` ┌─────────────────────────────────────────────────────────────┐ │ Pointer Lifecycle States │ └─────────────────────────────────────────────────────────────┘ [UNCARVED] (Slab 初期化直後、meta->used < meta->capacity) | | superslab_alloc_from_slab() - Linear carve v [CARVED] (meta->used++, user に返す直前) | | return block to user v [ALLOCATED] (ユーザーコードが使用中) | | user calls free(ptr) v +---> [FAST FREE PATH 99%] ----+ +---> [SLOW FREE PATH 1%] | | | | hak_tiny_free_fast_v2() | | hak_tiny_free() | - Header から class_idx 読取 | | - SuperSlab lookup | - tls_sll_push(ptr) | | - Ownership 確認 | (meta->used 不変!) | | | | | v | v [IN_TLS_SLL] | [FREELIST] | - TLS SLL に格納 | | - tiny_free_local_box() | - meta->used は高いまま | | - freelist に push | - periodic drain を待つ | | - meta->used-- ★ | | | | (2048 frees 後) | | v | | [DRAINING] | | | tiny_tls_sll_drain() | | | - TLS SLL から pop +<----+ | - tiny_free_local_box() 呼出 | - meta->used-- ★ v [FREELIST] (meta->freelist に連結、meta->used 正確) | | (meta->used == 0 なら) v [EMPTY] (ss_mark_slab_empty()) | | shared_pool_acquire_slab() - Stage 1 | → tiny_slab_can_reuse() ガード ★ v [REUSABLE] (再初期化可能) | | superslab_init_slab() v [UNCARVED] (ループ) ``` **重要な Invariants**: 1. **Meta->used の正確性** ```c // Invariant 1: meta->used reflects allocated + in_freelist // (TLS SLL 内のポインタは含まれない!) meta->used == allocated_count + freelist_count // 不正確 // Correct invariant (with TLS SLL): meta->used == allocated_count + freelist_count + tls_sll_count ``` 2. **Empty 判定の条件** ```c // Current (WRONG): is_empty = (meta->used == 0); // TLS SLL に残っている可能性 // Fixed (with guard): is_empty = (meta->used == 0) && !tiny_slab_has_tls_sll_refs(ss, slab_idx, cls); ``` 3. **TLS SLL のクリーン状態** ```c // Invariant 3: TLS SLL must be drained before slab reuse // Enforced by: tiny_slab_can_reuse() guard ``` ### 3.2 コード構造の改善 #### 責任の分離 | モジュール | 責任 | 主要 API | |-----------|------|---------| | **TLS SLL Box** | TLS 単方向リスト管理 | `tls_sll_push/pop/splice` | | **TLS SLL Drain Box** | Periodic drain、meta->used 同期 | `tiny_tls_sll_drain/try_drain` | | **Slab Meta Box** | Slab メタデータ管理 | `ss_slab_meta_*` | | **Slab Reuse Guard Box** | 再利用安全性チェック | `tiny_slab_can_reuse` | | **Free Local Box** | Freelist 管理、meta->used 更新 | `tiny_free_local_box` | | **Shared Pool** | SuperSlab 割り当て、slot 管理 | `shared_pool_acquire/release_slab` | #### ファイル構成の提案 ``` core/ ├── box/ │ ├── tls_sll_box.h (既存) Push/Pop/Splice │ ├── tls_sll_drain_box.h (既存) Periodic drain │ ├── tls_sll_tracked_box.h (新規) Tracked push/pop │ ├── slab_reuse_guard_box.h (新規) Reuse safety guard │ ├── free_local_box.{h,c} (既存) Freelist + meta->used-- │ ├── ss_slab_meta_box.h (既存) Meta accessors │ └── ss_hot_cold_box.h (既存) Hot/Cold/Empty marking ├── hakmem_shared_pool.{h,c} (既存) Shared pool ├── tiny_superslab_alloc.inc.h (既存) Alloc paths └── tiny_free_fast_v2.inc.h (既存) Fast free path ``` ### 3.3 デバッグ・診断機能の強化 #### 新規追加すべき診断機能 ##### A. TLS SLL 在庫トラッキング **ENV**: `HAKMEM_TINY_TLS_SLL_INVENTORY=1` ```c // Per-class TLS SLL inventory statistics (TLS) static __thread struct { uint64_t total_push; // 累計 push 回数 uint64_t total_pop; // 累計 pop 回数 uint64_t total_drained; // 累計 drain 回数 uint32_t peak_count; // 最大 count uint32_t current_count; // 現在の count } g_tls_sll_stats[TINY_NUM_CLASSES]; // Destructor で出力 static void tls_sll_inventory_report(void) __attribute__((destructor)); ``` ##### B. Slab 再利用ヒストリー **ENV**: `HAKMEM_TINY_SLAB_REUSE_HISTORY=1` ```c // Per-slab reuse history (circular buffer) #define REUSE_HISTORY_SIZE 16 typedef struct { uint64_t timestamp_ns; // 再利用時刻 uint8_t prev_class_idx; // 前のクラス uint8_t new_class_idx; // 新しいクラス uint16_t tls_sll_refs; // 再利用時の TLS SLL 参照数 } SlabReuseEvent; // Per SuperSlab SlabReuseEvent reuse_history[SLABS_PER_SUPERSLAB_MAX][REUSE_HISTORY_SIZE]; uint32_t reuse_index[SLABS_PER_SUPERSLAB_MAX]; ``` ##### C. Meta->used 整合性チェック **ENV**: `HAKMEM_TINY_META_USED_VERIFY=1` ```c // Periodic verification: count actual allocations vs meta->used // (Expensive, debug only) static void verify_meta_used_consistency(SuperSlab* ss, int slab_idx) { TinySlabMeta* meta = &ss->slabs[slab_idx]; // Count freelist entries uint32_t freelist_count = 0; void* cur = meta->freelist; while (cur && freelist_count < meta->capacity) { freelist_count++; cur = tiny_next_read(meta->class_idx, cur); } // Expected: meta->used + freelist_count <= meta->capacity if (meta->used + freelist_count > meta->capacity) { fprintf(stderr, "[META_USED_VERIFY] CORRUPTION: used=%u freelist=%u cap=%u\n", meta->used, freelist_count, meta->capacity); abort(); } } ``` #### 性能への影響を最小化 **戦略**: 1. **本番ビルドでは完全無効化**: `#if !HAKMEM_BUILD_RELEASE` でガード 2. **ENV ゲート**: デフォルト OFF、必要時のみ有効化 3. **サンプリング**: 全操作ではなく N 回に 1 回だけチェック 4. **TLS カウンタ**: Atomic 操作を避ける ### 3.4 ドキュメント化 #### MEMORY_LIFECYCLE.md の内容提案 ```markdown # メモリライフサイクル設計書 ## 概要 HAKMEM の Tiny Pool におけるポインタのライフサイクルと状態遷移を定義。 ## 状態定義 ### UNCARVED - **定義**: Slab 初期化直後、まだ carve されていない領域 - **条件**: `meta->used < meta->capacity` - **次状態**: CARVED (alloc 時) ### CARVED - **定義**: Slab から carve され、ユーザーに返す直前 - **操作**: `meta->used++` - **次状態**: ALLOCATED ### ALLOCATED - **定義**: ユーザーコードが使用中 - **Invariant**: ポインタはユーザー空間にあり、内部リストには未登録 - **次状態**: IN_TLS_SLL (fast free) or FREELIST (slow free) ### IN_TLS_SLL - **定義**: TLS SLL に push された、drain 待ち - **Invariant**: - `g_tls_sll[class_idx]` のリスト内に存在 - `meta->used` は変更なし(高いまま) - Periodic drain まで freelist に戻らない - **次状態**: DRAINING (periodic drain) ### DRAINING - **定義**: TLS SLL から freelist に移動中 - **操作**: 1. `tls_sll_pop()` 2. `tiny_free_local_box()` → `meta->used--` 3. Freelist に push - **次状態**: FREELIST ### FREELIST - **定義**: Slab の freelist に連結 - **Invariant**: - `meta->freelist` のリスト内に存在 - `meta->used` は正確(decrement 済み) - **次状態**: ALLOCATED (再利用) or EMPTY (すべて free) ### EMPTY - **定義**: Slab 内のすべてのブロックが free - **条件**: `meta->used == 0` - **マーク**: `ss_mark_slab_empty(ss, slab_idx)` - **次状態**: REUSABLE (再利用準備) ### REUSABLE - **定義**: 別のクラスで再初期化可能 - **ガード**: `tiny_slab_can_reuse()` でチェック - **操作**: `superslab_init_slab()` - **次状態**: UNCARVED ## 状態遷移の Invariants ### Invariant 1: Meta->used の意味 ```c // WRONG (current): meta->used == allocated_count + freelist_count // CORRECT (with TLS SLL): meta->used == allocated_count + freelist_count + tls_sll_count ``` **問題**: TLS SLL 内のポインタは `meta->used` に反映されない。 ### Invariant 2: Empty 判定 ```c // WRONG: is_empty = (meta->used == 0); // CORRECT: is_empty = (meta->used == 0) && !tiny_slab_has_tls_sll_refs(ss, slab_idx, class_idx); ``` ### Invariant 3: Slab 再利用前の TLS SLL クリーン ```c // Before reuse: tiny_slab_can_reuse(ss, slab_idx, class_idx) == true ⇒ TLS SLL に当該 slab のポインタが存在しない ``` ## デバッグツール - `HAKMEM_TINY_TLS_SLL_DIAG=1`: TLS SLL 診断 - `HAKMEM_TINY_SLAB_REUSE_GUARD=1`: 再利用ガード(本番) - `HAKMEM_TINY_META_USED_VERIFY=1`: Meta->used 検証(開発) ## 参考文献 - `/mnt/workdisk/public_share/hakmem/docs/analysis/LARSON_DOUBLE_FREE_INVESTIGATION.md` - `/mnt/workdisk/public_share/hakmem/core/box/tls_sll_box.h` - `/mnt/workdisk/public_share/hakmem/core/box/tls_sll_drain_box.h` ``` ### 3.5 テスト戦略 #### 単体テスト **ファイル**: 新規作成 `tests/test_tls_sll_lifecycle.c` ```c // Test 1: TLS SLL push/pop/drain cycle void test_tls_sll_basic_cycle(void) { int class_idx = 1; // 16B void* ptr1 = allocate_test_block(class_idx); void* ptr2 = allocate_test_block(class_idx); // Push to TLS SLL assert(tls_sll_push(class_idx, ptr1, 1000)); assert(g_tls_sll[class_idx].count == 1); // Pop from TLS SLL void* out; assert(tls_sll_pop(class_idx, &out)); assert(out == ptr1); assert(g_tls_sll[class_idx].count == 0); // Drain empty list (no-op) uint32_t drained = tiny_tls_sll_drain(class_idx, 0); assert(drained == 0); } // Test 2: Slab reuse guard void test_slab_reuse_guard(void) { int class_idx = 2; // 32B SuperSlab* ss = get_test_superslab(); int slab_idx = 0; // Allocate and free blocks (push to TLS SLL) for (int i = 0; i < 10; i++) { void* ptr = allocate_from_slab(ss, slab_idx); tls_sll_push(class_idx, ptr, 1000); } // Manually mark slab as empty (simulate meta->used == 0) ss->slabs[slab_idx].used = 0; ss_mark_slab_empty(ss, slab_idx); // Reuse guard should detect TLS SLL refs assert(!tiny_slab_can_reuse(ss, slab_idx, class_idx)); // Drain TLS SLL tiny_tls_sll_drain(class_idx, 0); // Now reuse should be safe assert(tiny_slab_can_reuse(ss, slab_idx, class_idx)); } // Test 3: Meta->used consistency after drain void test_meta_used_consistency(void) { // TODO: Implement } ``` #### 統合テスト **ベンチマーク**: Larson + 拡張 ```bash # 現在の Larson (MT stress test) ./mimalloc-bench/out/bench/larson 10 5 200000 # 追加テスト: Slab 再利用を強制 export HAKMEM_TINY_SLL_DRAIN_INTERVAL=100 # 頻繁に drain export HAKMEM_TINY_SLAB_REUSE_GUARD=1 # ガード有効 ./mimalloc-bench/out/bench/larson 10 5 500000 # 診断モード export HAKMEM_TINY_SLL_DIAG=1 export HAKMEM_TINY_SLAB_REUSE_HISTORY=1 ./mimalloc-bench/out/bench/larson 10 5 100000 2>larson_diag.log ``` #### Fuzzing **ツール**: AFL++ or libFuzzer ```c // Fuzzing harness for TLS SLL operations int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { if (size < 4) return 0; int class_idx = data[0] % TINY_NUM_CLASSES; uint8_t op = data[1]; switch (op % 4) { case 0: { // Push void* ptr = allocate_test_block(class_idx); tls_sll_push(class_idx, ptr, 1000); break; } case 1: { // Pop void* out; tls_sll_pop(class_idx, &out); break; } case 2: { // Drain tiny_tls_sll_drain(class_idx, data[2]); break; } case 3: { // Slab reuse // Simulate slab reuse break; } } return 0; } ``` --- ## Part 4: リファクタリング計画 ### 段階的な改善ステップ #### Phase 1: 即座の修正(1-2 日) **目標**: Larson double-free バグを修正 | ステップ | ファイル | 変更内容 | 工数 | |---------|---------|---------|------| | 1.1 | `tiny_superslab_alloc.inc.h` | Slab refill 時の TLS SLL drain | 小 | | 1.2 | `tls_sll_drain_box.h` | Adaptive drain interval | 小 | | 1.3 | テスト | Larson 100 runs → 0% crash | 中 | **期待される結果**: - ✅ Larson ベンチマークで double-free が発生しない - ✅ 既存の性能を維持(変更は診断コードのみ) #### Phase 2: ガード機構の導入(3-5 日) **目標**: 再発防止のための予防的チェック | ステップ | ファイル | 変更内容 | 工数 | |---------|---------|---------|------| | 2.1 | `box/slab_reuse_guard_box.h` | Reuse guard 実装 | 中 | | 2.2 | `hakmem_shared_pool.c` | Guard 統合 | 小 | | 2.3 | テスト | 単体テスト作成 | 中 | **期待される結果**: - ✅ Slab 再利用時に TLS SLL の残留を検出 - ✅ 開発ビルドで自動 drain、本番ビルドで ENV ゲート #### Phase 3: 診断機能の強化(5-7 日) **目標**: デバッグ効率の向上 | ステップ | ファイル | 変更内容 | 工数 | |---------|---------|---------|------| | 3.1 | `box/tls_sll_tracked_box.h` | Tracked push/pop マクロ | 中 | | 3.2 | 診断統計 | TLS SLL inventory tracking | 小 | | 3.3 | 診断統計 | Slab reuse history | 中 | | 3.4 | ドキュメント | `MEMORY_LIFECYCLE.md` 作成 | 中 | **期待される結果**: - ✅ TLS SLL の挙動を可視化 - ✅ Slab 再利用のヒストリーを追跡 - ✅ 将来的な meta->used 同期の準備 #### Phase 4: アーキテクチャ改善(14-21 日) **目標**: 根本的な解決(meta->used の正確性) | ステップ | ファイル | 変更内容 | 工数 | |---------|---------|---------|------| | 4.1 | `superslab_types.h` | `TinySlabMeta.used_tls_sll` フィールド追加 | 小 | | 4.2 | `box/tls_sll_tracked_box.h` | `used_tls_sll` の increment/decrement | 中 | | 4.3 | `box/ss_hot_cold_box.h` | Empty 判定を `used + used_tls_sll` に変更 | 小 | | 4.4 | 全体統合 | Tracked マクロを全箇所に適用 | 大 | | 4.5 | テスト | 統合テスト、Larson 長時間実行 | 中 | **期待される結果**: - ✅ `meta->used` が常に正確 - ✅ TLS SLL 内のポインタも追跡 - ✅ Empty 判定が正しく動作 ### 優先順位の提案 | フェーズ | 緊急度 | 重要度 | 推奨順序 | |---------|-------|-------|---------| | Phase 1 | 🔴 高 | 🔴 高 | 1位(即座に実施) | | Phase 2 | 🟡 中 | 🔴 高 | 2位(Phase 1 の後) | | Phase 3 | 🟢 低 | 🟡 中 | 3位(並行可能) | | Phase 4 | 🟢 低 | 🔴 高 | 4位(長期計画) | --- ## Part 5: 設計ドキュメント草案 ### ポインタライフサイクル図 ``` ┌───────────────────────────────────────────────────────────────────┐ │ HAKMEM Tiny Pool - Pointer Lifecycle │ └───────────────────────────────────────────────────────────────────┘ States: UNCARVED → CARVED → ALLOCATED → IN_TLS_SLL → DRAINING → FREELIST → EMPTY → REUSABLE Critical Paths: 1. Alloc (Fast): UNCARVED → CARVED → ALLOCATED 2. Free (Fast): ALLOCATED → IN_TLS_SLL 3. Free (Slow): ALLOCATED → FREELIST 4. Drain: IN_TLS_SLL → DRAINING → FREELIST 5. Reuse: EMPTY → REUSABLE → UNCARVED Invariants: I1: meta->used = allocated + freelist + tls_sll (total blocks not in UNCARVED) I2: is_empty ⇔ (meta->used == 0) ∧ (tls_sll_refs == 0) I3: reusable ⇔ is_empty ∧ tiny_slab_can_reuse() == true Guards: G1: tiny_slab_can_reuse() - Prevents reuse when TLS SLL has refs G2: tls_sll_push() duplicate check - Prevents double-free in TLS SLL G3: tiny_free_local_box() - Only path that decrements meta->used Synchronization Points: S1: Periodic drain (every N frees) - Syncs TLS SLL → freelist S2: Slab refill - Forces drain if TLS SLL has refs S3: Slab reuse - Blocks if TLS SLL has refs ``` ### 不変条件リスト #### I1: Meta->used の正確性 ```c // Definition: // meta->used counts all blocks that have been carved but not returned to freelist // INCLUDING blocks in TLS SLL (not yet drained) meta->used == SUM(state == ALLOCATED || state == IN_TLS_SLL || state == FREELIST) // Corollary: // Empty detection requires checking BOTH meta->used AND TLS SLL is_empty(slab) ⟺ (meta->used == 0) ∧ (tls_sll_count(slab) == 0) ``` #### I2: TLS SLL の一貫性 ```c // Definition: // TLS SLL contains only pointers that: // 1. Were allocated from this allocator // 2. Have valid headers (magic + class_idx) // 3. Are still in ALLOCATED or IN_TLS_SLL state ∀ptr ∈ TLS_SLL[class]: - ptr is BASE pointer (user - 1) - header[ptr].magic == HEADER_MAGIC - header[ptr].class_idx == class - state[ptr] == IN_TLS_SLL ``` #### I3: Slab 再利用の安全性 ```c // Definition: // Slab can be reused only if: // 1. meta->used == 0 (all blocks returned) // 2. No TLS SLL contains pointers from this slab // 3. Remote queues are drained tiny_slab_can_reuse(ss, slab_idx, class) ⟺ (meta->used == 0) ∧ (tls_sll_count_refs(ss, slab_idx, class) == 0) ∧ (remote_count[slab_idx] == 0) ``` #### I4: Drain の冪等性 ```c // Definition: // Draining an empty TLS SLL is a no-op TLS_SLL[class].count == 0 ⟹ tiny_tls_sll_drain(class, N) == 0 ``` ### API 契約の明文化 #### `tls_sll_push(class_idx, ptr, capacity)` **事前条件**: - `0 <= class_idx < TINY_NUM_CLASSES` - `ptr` は BASE ポインタ(user - 1) - `ptr` は現在 TLS SLL に含まれていない(重複不可) - `capacity > 0` **事後条件**: - 成功時: `TLS_SLL[class].count++`, `ptr` がリストの先頭 - 失敗時: 状態変更なし - **重要**: `meta->used` は変更されない **副作用**: - Header 書き戻し(クラス 1-6 のみ) - 重複チェック(debug ビルドのみ、先頭 256 要素) --- #### `tls_sll_pop(class_idx, *out)` **事前条件**: - `0 <= class_idx < TINY_NUM_CLASSES` - `out != NULL` **事後条件**: - 成功時: `TLS_SLL[class].count--`, `*out` に BASE ポインタ - 失敗時: `return false`, `*out` は未定義 - **重要**: `meta->used` は変更されない **副作用**: - Next ポインタのクリア(リスト切り離し) - Header 検証(debug ビルド) --- #### `tiny_tls_sll_drain(class_idx, batch_size)` **事前条件**: - `0 <= class_idx < TINY_NUM_CLASSES` - `batch_size == 0` → すべて drain **事後条件**: - `N = min(batch_size, TLS_SLL[class].count)` 個の要素を drain - 各要素に対して `tiny_free_local_box()` 呼び出し - **重要**: `meta->used -= N`(freelist 経由) **副作用**: - Freelist への追加 - `meta->used` の decrement - Empty マーク(`meta->used == 0` の場合) --- #### `tiny_slab_can_reuse(ss, slab_idx, class_idx)` **事前条件**: - `ss` は有効な SuperSlab - `0 <= slab_idx < ss_slabs_capacity(ss)` - `meta->used == 0`(caller が確認済み) **事後条件**: - 成功時: `return true`, TLS SLL に refs なし - 失敗時: `return false`, TLS SLL に refs あり(drain 必要) **副作用** (debug ビルドのみ): - TLS SLL の自動 drain(refs が見つかった場合) - ログ出力 --- ## 付録 A: 主要ファイルのサマリー ### `/mnt/workdisk/public_share/hakmem/core/box/tls_sll_box.h` **役割**: TLS SLL のコア実装(push/pop/splice) **主要関数**: - `tls_sll_push_impl()`: 要素を TLS SLL に追加(重複チェック付き) - `tls_sll_pop_impl()`: 要素を TLS SLL から取り出し - `tls_sll_splice()`: チェーンを一括で TLS SLL に統合 **重要な設計**: - すべて BASE ポインタで動作 - Header の読み書き(クラス 1-6) - 重複検出(debug ビルド、先頭 256 要素) --- ### `/mnt/workdisk/public_share/hakmem/core/box/tls_sll_drain_box.h` **役割**: Periodic drain(Option B)の実装 **主要関数**: - `tiny_tls_sll_drain()`: TLS SLL から freelist に移動 - `tiny_tls_sll_try_drain()`: Counter ベースの自動 drain **設計**: - デフォルト間隔: 2048 frees - ENV: `HAKMEM_TINY_SLL_DRAIN_INTERVAL` で調整可能 - **重要**: `tiny_free_local_box()` 経由で `meta->used--` --- ### `/mnt/workdisk/public_share/hakmem/core/box/free_local_box.c` **役割**: Freelist への追加 + `meta->used` の decrement **主要関数**: - `tiny_free_local_box()`: **唯一の `meta->used--` 実行箇所** **フロー**: 1. BASE ポインタ計算(user - 1) 2. Freelist に push 3. **`meta->used--`** ← ここが重要 4. `meta->used == 0` なら `ss_mark_slab_empty()` --- ### `/mnt/workdisk/public_share/hakmem/core/tiny_free_fast_v2.inc.h` **役割**: Fast free path(Header ベース) **主要関数**: - `hak_tiny_free_fast_v2()`: Header から class 読み取り → TLS SLL に push **フロー**: 1. Header から `class_idx` 読み取り(2-3 cycles) 2. `tls_sll_push(class_idx, base, UINT32_MAX)` 3. Periodic drain トリガー(2048 frees ごと) **重要**: `meta->used` は変更しない(drain 時に初めて更新) --- ### `/mnt/workdisk/public_share/hakmem/core/hakmem_shared_pool.c` **役割**: SuperSlab の割り当てと slot 管理 **主要関数**: - `shared_pool_acquire_slab()`: 3-stage allocation - Stage 1: EMPTY slots(最優先) - Stage 2: UNUSED slots - Stage 3: 新規 SuperSlab **問題箇所**: Stage 1 で EMPTY slot を再利用する時、TLS SLL のチェックがない --- ## 付録 B: 環境変数リファレンス | 環境変数 | デフォルト | 説明 | ファイル | |---------|-----------|------|---------| | `HAKMEM_TINY_SLL_DIAG` | 0 | TLS SLL 詳細診断 | tls_sll_box.h:118 | | `HAKMEM_TINY_SLL_DRAIN_ENABLE` | 1 | Periodic drain 有効化 | tls_sll_drain_box.h:40 | | `HAKMEM_TINY_SLL_DRAIN_INTERVAL` | 2048 | Drain 間隔(frees 単位) | tls_sll_drain_box.h:54 | | `HAKMEM_TINY_SLL_DRAIN_DEBUG` | 0 | Drain 詳細ログ | tls_sll_drain_box.h:121 | | `HAKMEM_TINY_SLL_DRAIN_ADAPTIVE` | 0 | Class ごとの adaptive 間隔 | (新規提案) | | `HAKMEM_TINY_SLL_SAFEHEADER` | 0 | Header 上書き防止 | tls_sll_box.h:336 | | `HAKMEM_TINY_LARSON_FIX` | 0 | Cross-thread free 検出 | tiny_free_fast_v2.inc.h:189 | | `HAKMEM_TINY_SLAB_REUSE_GUARD` | 0 (本番) | Slab 再利用ガード | (新規提案) | | `HAKMEM_TINY_TLS_SLL_TRACK_USED` | 0 | TLS SLL inventory 追跡 | (新規提案) | | `HAKMEM_TINY_META_USED_VERIFY` | 0 | Meta->used 整合性検証 | (新規提案) | --- ## まとめ ### 即座に実施すべき修正 1. **Slab refill 時の TLS SLL drain** (`tiny_superslab_alloc.inc.h`) - 工数: 小(1-2 時間) - 影響: Larson double-free の根本修正 2. **Adaptive drain interval** (`tls_sll_drain_box.h`) - 工数: 小(2-3 時間) - 影響: Hot classes の drain 頻度 ↑ → `meta->used` 正確性 ↑ ### 中期的な改善 3. **Slab reuse guard** (`box/slab_reuse_guard_box.h`) - 工数: 中(1-2 日) - 影響: 再発防止の予防的チェック 4. **診断機能の強化** - 工数: 中(3-5 日) - 影響: デバッグ効率の向上 ### 長期的なアーキテクチャ改善 5. **Meta->used の正確な同期** (`TinySlabMeta.used_tls_sll`) - 工数: 大(14-21 日) - 影響: 根本的な解決、将来的な問題の予防 --- **END OF REPORT**