Files
hakmem/docs/analysis/TLS_SLL_ARCHITECTURE_INVESTIGATION.md
Moe Charm (CI) dc9e650db3 Tiny Pool redesign: P0.1, P0.3, P1.1, P1.2 - Out-of-band class_idx lookup
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>
2025-11-28 13:42:39 +09:00

43 KiB
Raw Blame History

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 の設計

// 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 再利用フロー

  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

重複チェック

// 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);

段階的導入:

  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

#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.cshared_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:

  1. 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
    
  2. 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);
    
  3. 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();
    }
}

性能への影響を最小化

戦略:

  1. 本番ビルドでは完全無効化: #if !HAKMEM_BUILD_RELEASE でガード
  2. ENV ゲート: デフォルト OFF、必要時のみ有効化
  3. サンプリング: 全操作ではなく N 回に 1 回だけチェック
  4. 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_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 -= Nfreelist 経由)

副作用:

  • 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 == 0caller が確認済み)

事後条件:

  • 成功時: return true, TLS SLL に refs なし
  • 失敗時: return false, TLS SLL に refs ありdrain 必要)

副作用 (debug ビルドのみ):

  • TLS SLL の自動 drainrefs が見つかった場合)
  • ログ出力

付録 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 drainOption 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 pathHeader ベース)

主要関数:

  • 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 正確性 ↑

中期的な改善

  1. Slab reuse guard (box/slab_reuse_guard_box.h)

    • 工数: 中1-2 日)
    • 影響: 再発防止の予防的チェック
  2. 診断機能の強化

    • 工数: 中3-5 日)
    • 影響: デバッグ効率の向上

長期的なアーキテクチャ改善

  1. Meta->used の正確な同期 (TinySlabMeta.used_tls_sll)
    • 工数: 大14-21 日)
    • 影響: 根本的な解決、将来的な問題の予防

END OF REPORT