Files
hakmem/docs/analysis/SMALLOBJECT_CORE_V6_DESIGN.md
Moe Charm (CI) 406835feb3 Phase V6-HDR-0: C6-only headerless core 設計確定
- CURRENT_TASK.md: V6-HDR-0 セクション追加(4層 Box Theory)
- SMALLOBJECT_CORE_V6_DESIGN.md: V6-HDR-0 設計方針追加
- REGIONID_V6_DESIGN.md: RegionIdBox 設計書新規作成
- smallobject_core_v6_box.h: SmallTlsLaneV6 型+TLS API 追加
- smallobject_core_v6.c: OBSERVE モード追加
- region_id_v6_box.h: RegionIdBox 型スケルトン
- page_stats_v6_box.h: PageStatsV6 箱スケルトン
- AGENTS.md: v6 研究箱ルールセクション追加

サニティベンチ: Mixed 42.1M, C6-heavy 25.0M(挙動不変確認)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 23:07:26 +09:00

19 KiB
Raw Blame History

SmallObject Core v6 設計ドキュメント

Date: 2025-12-11 Phase: V6-HDR-0 (headerless core 設計確定) Status: Design Refresh - C6-only headerless 研究ライン


Phase V6-HDR-0: 設計方針転換

背景

Tiny/ULTRA 層が完成世代43.9M ops/sとして固定化されたため、 v6 は C6-only の headerless 研究ライン として再定義する。

重要な設計変更

  1. C7 ULTRA は frozen 箱として独立

    • TinyC7UltraBox / C7UltraSegmentBox はそのまま維持
    • v6 は C7 に一切触らないC6-only
  2. ptr 分類は RegionIdBox に一元化

    • 従来: classify_ptr / hak_super_lookup / ss_fast_lookup が分散
    • v6: region_id_lookup_v6(ptr) で (kind, region_id, page_meta*) を返す
  3. Stats は page lifetime summary のみを L3 に渡す

    • L1/L2 で個別 block の stats は取らない
    • page retire 時に summary を push

目的

16〜2KiB 帯の small-object/mid を、責務を厳密に分離した 4 層構造で再設計し、 Mixed 161024B を mimalloc の 5割50〜60M ops/sクラスに近づけるための「核」となる Core v6 の仕様を固定する。

v5 までは:

  • Segment/O(1) page_meta までは到達済みだが、
  • ヘッダ書き・page->used 管理・segment 判定などの責務が HotPath に残り続け、
  • C6-only でも v1/pool 比 -20% 前後から抜け出せなかった。

v6 では:

  • C7 ULTRA で成功している「TLS freelist + segment + mask free」パターンを L0 に、
  • small-object の本体は ヘッダレスside-meta 前提の Core v6 (L1) として再定義し、
  • 安全性・学習・route の責務を Cold/Policy 側に徹底的に落とす。

層構造(固定)

v6 では、small-object/mid を次の 4 層に固定する。

  1. L0: ULTRA lane

    • C7・ごく少数の超ホットクラス専用。
    • TLS freelist + small ULTRA segment2MiB / 64KiB page+ mask 判定のみを HotPath とする。
    • ヘッダレス or side-meta 前提。header 書き・学習はすべて slow/refill 側。
  2. L1: SmallObject Core v6新 HotBox

    • 16〜2KiB の大半を扱う per-thread heap。
    • 責務:
      • size→class 決定後の alloc/freesame-threadのみ。
      • ptr→page→page_meta→freelist pop/pushただし page_meta 参照は slow/refill で極力まとめる)。
    • ヘッダレスblock 先頭は freelist 用 next のみ。class/region 情報は page_meta 側に持つ。
  3. L2: Segment / Remote / ColdIface

    • Segment v6: 2MiB Segment / 64KiB Page + page_meta[]
    • RemoteBox: cross-thread free キュー。
    • SmallColdIface_v6: HotBox からの唯一の橋渡し:
      • refill_page(class_idx)
      • retire_page(page)
      • remote_push(page, ptr)
      • remote_drain()
    • Superslab/OS/DSO guard/Budget は、この層の内部で完結させる。
  4. L3: Policy / Learning / Guard

    • SmallPolicySnapshot_v6:
      • route_kind[class]ULTRA / CORE / POOL / LEGACY
      • block_size[class]
      • max_tls_slots[class] / max_partial_pages[class] など。
    • ENV と Stats を読み、snapshot を更新する箱。
    • L0/L1 は snapshot の値を読むだけHotPath 内で ENV や Stats を触らない)。

この 4 層は v6 の設計で固定とし、以降は「層内の微調整」はあっても層の責務は動かさない前提とする。


ヘッダレス / side-meta ポリシー

L1/L0 のルール

L1/L0 の HotPath では:

  • block 先頭は freelist 用 next ポインタ専用とし、
  • Tiny header や region/class 情報を一切置かない(ヘッダレス)。

class_idx / region 情報が必要な場合は:

  • SmallPageMetaV6 側に class_idxregion_tag を持たせ、
  • free 時には page = page_of(ptr)page->class_idx を読む。

外部との互換(既存 header の扱い)

既存 Tiny/mid/free は header ベースの検証を行っているので、 v6 導入後は:

  • header が必要な経路は L1/L0 の外側の「RegionIdBox」 で page 単位の情報に変換する:
    • map登録時: page ごとに region_id を registry に記録。
    • free 時: page_of(ptr)region_id を見てどの allocator の所有物か判定。
  • L1/L0 は region/header の存在を知らず、「自分の page_meta かどうか」だけを ColdIface 経由で教えてもらう。

これにより:

  • HotPath から header 書き/読みを完全に排除しつつ、
  • 既存の header ベースの guard は RegionIdBox 側で段階的に移行・互換維持できる。

SmallObject Core v6L1の型

Segment/ページメタ

#define SMALL_SEGMENT_V6_SIZE   (2 * 1024 * 1024) // 2MiB
#define SMALL_PAGE_V6_SIZE      (64 * 1024)       // 64KiB
#define SMALL_PAGES_PER_SEGMENT (SMALL_SEGMENT_V6_SIZE / SMALL_PAGE_V6_SIZE)

typedef struct SmallPageMetaV6 {
    void*     free_list;   // block先頭をnextとして使う
    uint16_t  used;        // 現在使用中スロット数
    uint16_t  capacity;    // ページ内スロット数
    uint8_t   class_idx;   // サイズクラス
    uint8_t   flags;       // FULL / PARTIAL / REMOTE_PENDING など
    uint16_t  page_idx;    // Segment 内 index
    void*     segment;     // SmallSegmentV6*
} SmallPageMetaV6;

typedef struct SmallSegmentV6 {
    uintptr_t       base;       // Segment base address
    uint32_t        num_pages;
    uint32_t        owner_tid;
    uint32_t        magic;      // 例えば 0xC0REV6
    SmallPageMetaV6 page_meta[SMALL_PAGES_PER_SEGMENT];
} SmallSegmentV6;

ptr→page_meta の取得は mask+shift による O(1) で行う:

static inline SmallPageMetaV6* small_page_meta_v6_of(void* ptr) {
    uintptr_t addr = (uintptr_t)ptr;
    uintptr_t seg_base = addr & ~(SMALL_SEGMENT_V6_SIZE - 1);
    SmallSegmentV6* seg = (SmallSegmentV6*)seg_base;
    if (unlikely(seg->magic != SMALL_SEGMENT_V6_MAGIC)) return NULL;
    size_t page_idx = (addr - seg_base) >> SMALL_PAGE_V6_SHIFT; // PAGE_SHIFT=16
    if (unlikely(page_idx >= seg->num_pages)) return NULL;
    return &seg->page_meta[page_idx];
}

per-class heap 状態

typedef struct SmallClassHeapV6 {
    SmallPageMetaV6* current;       // よく使うページ
    SmallPageMetaV6* partial_head;  // 空きありページの簡易リスト
} SmallClassHeapV6;

TLS heap context

ULTRA と CORE を併用することを想定し、クラス単位の TLS freelist を持つ:

#define SMALL_V6_TLS_CAP  32

typedef struct SmallHeapCtxV6 {
    SmallClassHeapV6 cls[NUM_SMALL_CLASSES_V6];

    // TLS freelist per hot class (例: C6, C5, 将来必要なクラスだけ)
    void*   tls_freelist_c6[SMALL_V6_TLS_CAP];
    uint8_t tls_count_c6;

    void*   tls_freelist_c5[SMALL_V6_TLS_CAP];
    uint8_t tls_count_c5;

    // TLS ownership check 用Hot segment は 1 つ)
    uintptr_t tls_seg_base;  // Segment base address
    uintptr_t tls_seg_end;   // base + SMALL_SEGMENT_V6_SIZE

    // 将来: 他の hot class 用の TLS freelist を追加可能
} SmallHeapCtxV6;

Core v6 HotPath のルール

allocCORE route, C6/C5 の例)

前段Front/Gateは v3/v5 同様に size→class を LUT で決め、snapshot から route_kind を読む。
CORE route の C6/C5 は SmallObject Core v6 に落とす。

void* small_alloc_fast_v6(size_t size, uint32_t class_idx, SmallHeapCtxV6* ctx,
                          const SmallPolicySnapshotV6* snap) {
    small_route_kind_t route = snap->route_kind[class_idx];

    if (route == SMALL_ROUTE_ULTRA) {
        return small_ultra_alloc_v6(size, class_idx, ctx, snap); // L0 lane
    }

    if (route != SMALL_ROUTE_CORE) {
        // pool v1 / legacy などにフォールバック
        return small_route_fallback_alloc(size, class_idx, snap);
    }

    // 例: C6
    if (class_idx == C6_CLASS_IDX) {
        if (likely(ctx->tls_count_c6 > 0)) {
            return ctx->tls_freelist_c6[--ctx->tls_count_c6];
        }
        return small_core_refill_v6(ctx, class_idx, snap);
    }

    // C5 など他クラスも同様のTLS freelistパターンで処理
    ...
}

freeCORE route, same-thread, class_idx ヒント付き)

Core v6 では free 時に毎回 page_meta を読むのではなく、

  • 前段header or size クラス判定)で算出済みの class_idx を引数として受け取り、
  • まずは TLS segment 所有範囲チェックだけで「自分の TLS に積めるか」を判定し、
  • TLS に積めなかった場合にのみ small_page_meta_v6_of(ptr) で page_meta を取得する。
static inline bool small_tls_owns_ptr_v6(const SmallHeapCtxV6* ctx, void* ptr) {
    uintptr_t addr = (uintptr_t)ptr;
    return addr >= ctx->tls_seg_base && addr < ctx->tls_seg_end;
}

void small_free_fast_v6(void* ptr, uint32_t class_idx,
                        SmallHeapCtxV6* ctx,
                        const SmallPolicySnapshotV6* snap) {
    small_route_kind_t route = snap->route_kind[class_idx];

    if (route == SMALL_ROUTE_ULTRA) {
        small_ultra_free_v6(ptr, ctx, snap);
        return;
    }

    if (route != SMALL_ROUTE_CORE) {
        small_route_fallback_free(ptr, snap);
        return;
    }

    // Fast path: TLS segment 所有範囲内かつ TLS slot に空きがあれば、page_meta を見ずに TLS に積む
    if (likely(small_tls_owns_ptr_v6(ctx, ptr))) {
        if (class_idx == C6_CLASS_IDX && ctx->tls_count_c6 < SMALL_V6_TLS_CAP) {
            ctx->tls_freelist_c6[ctx->tls_count_c6++] = ptr;
            return;
        }
        if (class_idx == C5_CLASS_IDX && ctx->tls_count_c5 < SMALL_V6_TLS_CAP) {
            ctx->tls_freelist_c5[ctx->tls_count_c5++] = ptr;
            return;
        }
        // TLS満杯 or TLS未対応クラス → slow pathへ
    }

    // Slow path: page_meta lookup + remote/retire 判定(頻度を下げる)
    SmallPageMetaV6* page = small_page_meta_v6_of(ptr);
    if (unlikely(page == NULL)) {
        small_route_fallback_free(ptr, snap); // v6管轄外 → legacy/poolへ
        return;
    }

    // same-thread 判定は ColdIface/RemoteBox 側で page->owner_tid を見る
    if (unlikely(!small_page_owned_by_self(page))) {
        small_cold_v6_remote_push(page, ptr, small_self_tid());
        return;
    }

    // TLSでは受けられなかった分だけ page freelist に戻す
    void* head = page->free_list;
    *(void**)ptr = head;
    page->free_list = ptr;
    page->used--; // retire 判定・統計は slow側で扱う
    if (unlikely(page->used == 0)) {
        small_cold_v6_retire_page(page);
    }
}

refill / drainL2 への委譲)

Core v6 の slow path では:

  • small_core_refill_v6small_cold_v6_refill_page(class_idx) を叩き、返ってきた page からまとめてブロックを carve して TLS freelist に積む。
  • small_cold_v6_retire_page が used==0 の page を Segment pool に返す。
  • remote push/drain は RemoteBox 経由で行い、L1 は remote free の存在を意識しない。

実装上の禁止事項HotBox側の約束

Core v6 HotBoxL1は:

  • ヘッダを書かない/読まない。
  • Superslab/Tiny/Pool v1 の関数を直接呼ばない(必ず ColdIface v6 経由)。
  • Stats/Learning/ENV を直接参照しないsnapshot の値を読むだけ)。
  • mid_desc_lookup / hak_super_lookup / classify_ptr などの lookup 系関数を呼ばない。

これらはすべて L2/L3 の責務とし、v6 以降の最適化でもこの境界は維持する。


L2→L3 Stats インターフェース

設計原則:

  • L2→L3 には page lifetime のサマリだけを渡す
  • HotPathalloc/freeから Stats を一切更新しない。

Stats 構造体と通知

typedef struct SmallPageStatsV6 {
    uint8_t  class_idx;
    uint32_t alloc_count;        // この page からの総 alloc 数
    uint32_t free_count;         // この page への総 free 数
    uint32_t remote_free_count;  // cross-thread free の数
    uint32_t lifetime_ns;        // carve → retire までの時間 (optional)
} SmallPageStatsV6;

// L2 (ColdIface) が retire/refill 時に L3 (Policy) へ通知
void small_policy_v6_on_page_retire(const SmallPageStatsV6* stats);
void small_policy_v6_on_page_refill(uint8_t class_idx);

通知タイミング:

イベント L2→L3 通知 データ
refill on_page_refill class_idx
retire on_page_retire SmallPageStatsV6 全体
remote_drain なしL2 内部完結) -

L3 側ではクラス別に集計し、次回 snapshot の TLS cap や partial limit を更新する:

typedef struct SmallPolicyStateV6 {
    uint64_t total_allocs[NUM_SMALL_CLASSES_V6];
    uint64_t total_frees[NUM_SMALL_CLASSES_V6];
    uint64_t remote_frees[NUM_SMALL_CLASSES_V6];

    uint32_t optimal_tls_cap[NUM_SMALL_CLASSES_V6];
    uint32_t optimal_partial_limit[NUM_SMALL_CLASSES_V6];
} SmallPolicyStateV6;

void small_policy_v6_update_snapshot(SmallPolicySnapshotV6* snap,
                                     const SmallPolicyStateV6* state);

レガシーとの橋渡しRegionIdBox

現状は header ベースの guard に依存しており、

  • free 時に header byte を読んで class_idx + magic を検証し、
  • どの allocatorTiny/Pool/v3/v5が所有者かを判定している。

v6 ではヘッダレス前提とするため、 page 単位で所有者を管理する RegionIdBox を導入し、段階的に header 依存を外す。

typedef struct RegionIdBox RegionIdBox;

// page_base → region_id のマッピングを管理
void region_id_box_register_page(void* page_base, uint32_t region_id);
void region_id_box_unregister_page(void* page_base);
uint32_t region_id_box_lookup(void* ptr);

移行フェーズのイメージ:

  • Phase 1 (v6-0/1):
    • v6 は header を書かないが、front は header を読んで class_idx を決めるv6 の外側の責務)。
    • v6 の page は RegionIdBox に登録しておき、fallback 判定に利用。
    • free 時: header→class_idx→route で v6 free へ入り、v6 内では header に触らない。
  • Phase 2 (v6-2+):
    • v6 alloc は完全ヘッダレス。
    • free 時: TLS hitsmall_tls_owns_ptr_v6==trueの場合は header 読みを skip。
    • TLS miss 時だけ RegionIdBox で所有者を確認し、v6 か legacy/pool かを決める。
  • Phase 3将来:
    • 全クラスが v6/新経路に乗った段階で header を完全廃止し、RegionIdBox が唯一の所有者判定手段になる。

互換性マトリクス(イメージ):

ptr の出所 free 判定 備考
v6 alloc (TLS hit) TLS owns → v6 free header 不使用
v6 alloc (TLS miss) RegionIdBox → v6 free header 不使用
legacy alloc header → legacy free 既存 guard 維持
pool v1 alloc header → pool free 既存 guard 維持

まとめ: v6 設計の固定事項

  1. class_idx ヒント

    • class_idx の決定責務:
      • alloc: front の size→class LUT。
      • free: front の header→class 読みv6 の外側)。
    • v6 への渡し方:
      • 関数引数で渡し、v6 側では header を一切触らない。
  2. TLS ownership check

    • Hot segment は常に TLS 上 1 つ(tls_seg_basetls_seg_end)。
    • free の fast path では range check2 CMPのみで所有判定する。
    • multi-segment化する場合も、segment[0] のみ fast path、他は slow path として扱う。
  3. L2→L3 Stats

    • retire/refill 時の page lifetime summarySmallPageStatsV6のみを渡す。
    • HotPathalloc/freeでは Stats を一切更新しない。
  4. RegionIdBox

    • page 単位で所有者v6 / legacy / poolを管理。
    • 段階的に header ベース guard から RegionIdBox に移行し、最終的には header を廃止可能な設計にする。

フェーズ案v6

  1. Phase v6-0: 設計ドキュメントと型・IF 追加(完全 OFF

    • 本ドキュメントを作成し、SmallPageMetaV6 / SmallClassHeapV6 / SmallHeapCtxV6 / SmallSegmentV6 / ColdIface_v6 の型とヘッダだけ追加。
    • ENV は HAKMEM_SMALL_HEAP_V6_ENABLED=0 デフォルトで route からは一切呼ばれない。
  2. Phase v6-1: C6-only CORE v6 route stub

    • C6 を route_snapshot で SMALL_ROUTE_CORE_V6 に振れるようにしつつ、中身は v1/pool に即フォールバック(動作は変えない)。
  3. Phase v6-2: C6-only Core v6 実装Segment + TLS freelist

    • C6 について ULTRA に似た TLS freelist + Segment ベースの Core v6 を実装。
    • C6-heavy で v1/pool と A/B、安定・回帰幅を確認。
  4. Phase v6-3: Mixed での段階的 CORE v6 昇格

    • C6 → C5 → 他クラスと、hot class から CORE v6 に載せ、Mixed 161024B の perf を確認。
    • C7 ULTRAL0と CORE v6L1の共存チューニング。
  5. Phase v6-4 以降C5/C4 拡張 + free hotpath 削減)

    • C6 で安定・baseline 同等が確認できたら、C5 / C4 を順次 CORE v6 に載せていき、free hotpath の ss_fast_lookup/slab_index_for 依存を削っていく。
    • 各クラスごとに:
      • heavy プロファイルC5-heavy, C4-heavyで v6 ON/OFF の A/Bまずは ±0〜数% を目標)。
      • Mixed 161024B で v6 ON 時の impactfree% の減少と ops/s の変化)を確認。
    • それでも free 側が支配的なら、最終的には front/gate の dispatcher 自体を薄くするフェーズfree dispatch 削減)に進む。

以降の Phase は、この「層」と「責務」を変えずに micro-optimization を繰り返すフェーズとする。


実装ステータス2025-12-11

Phase V6-HDR-0進行中

v6 を C6-only headerless 研究ラインとして再定義。主な変更:

タスク 状態 内容
1-1 CURRENT_TASK.md に V6-HDR-0 セクション追加
1-2 本ドキュメント更新(設計方針転換)
1-3 pending REGIONID_V6_DESIGN.md 新規作成
2-1 pending SmallTlsLaneV6 / SmallHeapCtxV6 型スケルトン
2-2 pending v6 TLS API シグネチャ定義
3-1 pending RegionIdBox 型と lookup API
3-2 pending OBSERVE モードfree 入口ログ)
4-1 pending PageStatsV6 箱追加
5-1 pending AGENTS.md に v6 ルール追記
5-2 pending サニティベンチ確認

過去フェーズ(参考)

  • v6-3: C6-only で baseline 同等まで改善。
    • C6-heavy A/B: v6 OFF 27.1M → v6-3 ON 27.1M ops/s±0%
    • TLS ownership check + batch header write + TLS batch refill の薄型化完了。
  • v6-4〜v6-6: C5/C4 拡張を試行したが回帰が大きく研究箱に留める。