# 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 16–1024B を 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 segment(2MiB / 64KiB page)+ mask 判定のみを HotPath とする。 - ヘッダレス or side-meta 前提。header 書き・学習はすべて slow/refill 側。 2. **L1: SmallObject Core v6(新 HotBox)** - 16〜2KiB の大半を扱う per-thread heap。 - 責務: - size→class 決定後の alloc/free(same-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_idx` や `region_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 v6(L1)の型 ### Segment/ページメタ ```c #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) で行う: ```c 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 状態 ```c typedef struct SmallClassHeapV6 { SmallPageMetaV6* current; // よく使うページ SmallPageMetaV6* partial_head; // 空きありページの簡易リスト } SmallClassHeapV6; ``` ### TLS heap context ULTRA と CORE を併用することを想定し、クラス単位の TLS freelist を持つ: ```c #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 のルール ### alloc(CORE route, C6/C5 の例) 前段(Front/Gate)は v3/v5 同様に size→class を LUT で決め、snapshot から route_kind を読む。 CORE route の C6/C5 は SmallObject Core v6 に落とす。 ```c 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パターンで処理 ... } ``` ### free(CORE 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 を取得する。 ```c 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 / drain(L2 への委譲) Core v6 の slow path では: - `small_core_refill_v6` が `small_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 HotBox(L1)は: - ヘッダを書かない/読まない。 - 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 のサマリだけを渡す**。 - HotPath(alloc/free)から Stats を一切更新しない。 ### Stats 構造体と通知 ```c 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 を更新する: ```c 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 を検証し、 - どの allocator(Tiny/Pool/v3/v5)が所有者かを判定している。 v6 ではヘッダレス前提とするため、 **page 単位で所有者を管理する RegionIdBox を導入し、段階的に header 依存を外す。** ```c 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 hit(small_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_base`〜`tls_seg_end`)。 - free の fast path では range check(2 CMP)のみで所有判定する。 - multi-segment化する場合も、segment[0] のみ fast path、他は slow path として扱う。 3. **L2→L3 Stats** - retire/refill 時の page lifetime summary(SmallPageStatsV6)のみを渡す。 - HotPath(alloc/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 16–1024B の perf を確認。 - C7 ULTRA(L0)と CORE v6(L1)の共存チューニング。 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 16–1024B で v6 ON 時の impact(free% の減少と 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 拡張を試行したが回帰が大きく研究箱に留める。