This commit implements the first phase of Tiny Pool redesign based on
ChatGPT architecture review. The goal is to eliminate Header/Next pointer
conflicts by moving class_idx lookup out-of-band (to SuperSlab metadata).
## P0.1: C0(8B) class upgraded to 16B
- Size table changed: {16,32,64,128,256,512,1024,2048} (8 classes)
- LUT updated: 1..16 → class 0, 17..32 → class 1, etc.
- tiny_next_off: C0 now uses offset 1 (header preserved)
- Eliminates edge cases for 8B allocations
## P0.3: Slab reuse guard Box (tls_slab_reuse_guard_box.h)
- New Box for draining TLS SLL before slab reuse
- ENV gate: HAKMEM_TINY_SLAB_REUSE_GUARD=1
- Prevents stale pointers when slabs are recycled
- Follows Box theory: single responsibility, minimal API
## P1.1: SuperSlab class_map addition
- Added uint8_t class_map[SLABS_PER_SUPERSLAB_MAX] to SuperSlab
- Maps slab_idx → class_idx for out-of-band lookup
- Initialized to 255 (UNASSIGNED) on SuperSlab creation
- Set correctly on slab initialization in all backends
## P1.2: Free fast path uses class_map
- ENV gate: HAKMEM_TINY_USE_CLASS_MAP=1
- Free path can now get class_idx from class_map instead of Header
- Falls back to Header read if class_map returns invalid value
- Fixed Legacy Backend dynamic slab initialization bug
## Documentation added
- HAKMEM_ARCHITECTURE_OVERVIEW.md: 4-layer architecture analysis
- TLS_SLL_ARCHITECTURE_INVESTIGATION.md: Root cause analysis
- PTR_LIFECYCLE_TRACE_AND_ROOT_CAUSE_ANALYSIS.md: Pointer tracking
- TINY_REDESIGN_CHECKLIST.md: Implementation roadmap (P0-P3)
## Test results
- Baseline: 70% success rate (30% crash - pre-existing issue)
- class_map enabled: 70% success rate (same as baseline)
- Performance: ~30.5M ops/s (unchanged)
## Next steps (P1.3, P2, P3)
- P1.3: Add meta->active for accurate TLS/freelist sync
- P2: TLS SLL redesign with Box-based counting
- P3: Complete Header out-of-band migration
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
43 KiB
TLS SLL アーキテクチャ改善と今後の開発方針
調査日: 2025-11-28 対象: Larson ベンチマーク double-free バグの根本原因と包括的なアーキテクチャ改善
エグゼクティブサマリー
根本原因の特定
Larson ベンチマークでの double-free バグの根本原因は、TLS SLL と meta->used の同期不整合です:
-
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--される
-
Slab 再利用時の TLS SLL クリア漏れ
- Slab が
meta->used == 0で empty と判定される - しかし TLS SLL には古いポインタが残っている
- Slab が再初期化されて別のアドレス範囲で使われる
- 古いポインタが再度 allocate されて重複検出
- Slab が
-
既知のバグ(既に修正済み)
- TLS Drain pushback バグ:
tls_sll_drain_box.h:148-162(commitc2f104618で修正) - ポインタが TLS SLL の 2 箇所に存在する状態を作り出していた
- TLS Drain pushback バグ:
影響範囲
- 確実に影響: 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 の設計
// 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 判定条件
// 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 マーク
// 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 再利用フロー
-
shared_pool_acquire_slab()(hakmem_shared_pool.c:700-1300)- Stage 1: EMPTY slots を優先的に再利用 (empty_mask をスキャン)
- Stage 2: UNUSED slots を探す
- Stage 3: 新しい SuperSlab を割り当て
-
superslab_init_slab()(初期化時)meta->used = 0meta->freelist = NULLmeta->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 |
重複チェック
// 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 に古いポインタが残る。
修正案:
// 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) では、ワークロードによっては不十分。
修正案:
// 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;
}
}
使い方:
# 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
#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;
}
使い方:
// 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);
段階的導入:
- Phase 1: マクロ導入、ログのみ(診断用)
- Phase 2:
TinySlabMeta.used_tls_sllフィールド追加 - 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
#pragma once
#include "hakmem_tiny_superslab.h"
#include "hakmem_tiny_config.h"
#include <stdint.h>
#include <stdbool.h>
// ============================================================================
// 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()
// 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:
-
Meta->used の正確性
// 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 -
Empty 判定の条件
// 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); -
TLS SLL のクリーン状態
// 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
// 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
// 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
// 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();
}
}
性能への影響を最小化
戦略:
- 本番ビルドでは完全無効化:
#if !HAKMEM_BUILD_RELEASEでガード - ENV ゲート: デフォルト OFF、必要時のみ有効化
- サンプリング: 全操作ではなく N 回に 1 回だけチェック
- TLS カウンタ: Atomic 操作を避ける
3.4 ドキュメント化
MEMORY_LIFECYCLE.md の内容提案
# メモリライフサイクル設計書
## 概要
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 判定
// 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 クリーン
// 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 + 拡張
# 現在の 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
// 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 の正確性
// 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 の一貫性
// 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 再利用の安全性
// 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 の冪等性
// 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_CLASSESptrは 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_CLASSESout != 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_CLASSESbatch_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は有効な SuperSlab0 <= 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--実行箇所
フロー:
- BASE ポインタ計算(user - 1)
- Freelist に push
meta->used--← ここが重要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
フロー:
- Header から
class_idx読み取り(2-3 cycles) tls_sll_push(class_idx, base, UINT32_MAX)- 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 整合性検証 | (新規提案) |
まとめ
即座に実施すべき修正
-
Slab refill 時の TLS SLL drain (
tiny_superslab_alloc.inc.h)- 工数: 小(1-2 時間)
- 影響: Larson double-free の根本修正
-
Adaptive drain interval (
tls_sll_drain_box.h)- 工数: 小(2-3 時間)
- 影響: Hot classes の drain 頻度 ↑ →
meta->used正確性 ↑
中期的な改善
-
Slab reuse guard (
box/slab_reuse_guard_box.h)- 工数: 中(1-2 日)
- 影響: 再発防止の予防的チェック
-
診断機能の強化
- 工数: 中(3-5 日)
- 影響: デバッグ効率の向上
長期的なアーキテクチャ改善
- Meta->used の正確な同期 (
TinySlabMeta.used_tls_sll)- 工数: 大(14-21 日)
- 影響: 根本的な解決、将来的な問題の予防
END OF REPORT