SmallObject HotBox v3 Design (Tiny + mid/smallmid 統合案) ======================================================== 目的と背景 ---------- - 現状の性能スナップショット: - Mixed 16–1024B (1t, ws=400, iters=1M, PROFILE=C7_SAFE, Tiny v2 OFF): - HAKMEM ≈ 40–50M ops/s - mimalloc ≈ 110–120M ops/s - system ≈ 90M ops/s - mid/smallmid (bench_mid_large_mt, 1t, ws=400, iters=1M): - HAKMEM ≈ 28–29M ops/s - mimalloc ≈ 54M ops/s - system ≈ 15M ops/s - これまでの Tiny/Pool v2 の試行から見えたこと: - C7 v2 (TinyHotHeap v2) は C7-only / Mixed 長尺では v1 C7_SAFE と同等以上まで到達したが、C6/C5 への水平展開や pool v2 は現フェーズでは perf 未達で凍結。 - pool v2 は page_of O(1) 化+Cold IF=v1 pool/Superslab まで実装したが、C6-heavy 長尺で極端な遅さが発生し、現行設計のまま押し上げるのは難しい。 - 結論: - mimalloc に 7〜8割で迫るには、「Tiny (16〜1KiB) と mid/smallmid の両方」を一体の SmallObject HotBox として設計し直す必要がある。 - Superslab/Segment/Tier/Guard/Remote といった Cold/Safety 層は Box として残しつつ、SmallObject 側の Hot Box を 1 枚に集約する方向で v3 を設計する。 進捗サマリ (Phase A/B 通電) --------------------------- - ENV: `HAKMEM_SMALL_HEAP_V3_ENABLED` / `HAKMEM_SMALL_HEAP_V3_CLASSES` を追加(デフォルトは C7-only ON: ENABLED=1, CLASSES=0x80 相当)。 - 型/IF: `core/box/smallobject_hotbox_v3_box.h` に so_page/class/ctx と stats を定義。TLS 初期化でクラス別 stride/max_partial をセット。 - Cold IF: `smallobject_cold_iface_v1.h` で C7 専用の v1 Tiny ラッパを実装。refill で tiny_heap_prepare_page(7) を借り、retire で tiny_heap_page_becomes_empty に返す。 - Hot: `core/smallobject_hotbox_v3.c` で so_alloc/so_free を実装(current/partial freelist を v3 で管理、refill 失敗は v1 fallback)。ページ内 freelist carve は v3 側で実施。 - Route: `tiny_route_env_box.h` に `TINY_ROUTE_SMALL_HEAP_V3` を追加。クラスビットが立っているときだけ route snapshot で v3 に振り分け。 - Front: malloc/free で v3 route を試し、失敗時は v2/v1/legacy に落とす直線パス。デフォルトは OFF なので挙動は従来通り。 ### Phase S1: C6 v3 研究箱(C7 を壊さずにベンチ限定で解禁) - Gate: `HAKMEM_SMALL_HEAP_V3_ENABLED`/`CLASSES` の bit7=C7(デフォルト ON=0x80)、bit6=C6(research-only、デフォルト OFF)。C6 を叩くときは `HAKMEM_TINY_C6_HOT=1` を併用して tiny front を確実に通す。 - Cold IF: `smallobject_cold_iface_v1.h` を C6 にも適用し、`tiny_heap_prepare_page`/`page_becomes_empty` を C7 と同じ形で使う。v3 stats に `page_of_fail` を追加し、free 側の page_of ミスを計測。 - Bench (Release, Tiny/Pool v2 OFF, ws=400, iters=1M): - C6-heavy A/B: `MIN_SIZE=257 MAX_SIZE=768`。`CLASSES=0x80`(C6 v1)→ **47.71M ops/s**、`CLASSES=0x40`(C6 v3, stats ON)→ **36.77M ops/s**(cls6 `route_hits=266,930 alloc_refill=5 fb_v1=0 page_of_fail=0`)。v3 は約 -23%。 - Mixed 16–1024B: `CLASSES=0x80`(C7-only)→ **47.45M ops/s**、`CLASSES=0xC0`(C6+C7 v3, stats ON)→ **44.45M ops/s**(cls6 `route_hits=137,307 alloc_refill=1 fb_v1=0 page_of_fail=0` / cls7 `alloc_refill=2,446`)。約 -6%。 - 運用方針: 標準プロファイルは `HAKMEM_SMALL_HEAP_V3_CLASSES=0x80`(C7-only v3)に確定。C6 v3 は bench/研究のみ明示 opt-in とし、C6-heavy/Mixed の本線には乗せない。性能が盛り返すまで研究箱据え置き。 - C6-heavy を v1 固定で走らせる推奨プリセット(研究と混同しないための明示例): ``` HAKMEM_BENCH_MIN_SIZE=257 HAKMEM_BENCH_MAX_SIZE=768 HAKMEM_TINY_HEAP_PROFILE=C7_SAFE HAKMEM_TINY_C6_HOT=1 HAKMEM_SMALL_HEAP_V3_ENABLED=1 HAKMEM_SMALL_HEAP_V3_CLASSES=0x80 # C7-only v3 ``` 設計ゴール (SmallObjectHotBox v3) --------------------------------- - 対象サイズ帯: - 16〜約 2KiB 程度の「SmallObject」領域(既存 Tiny C0〜C7 + mid/smallmid の一部)。 - それより大きいサイズは現行 mid/large ルート(pool v1 / direct mmap)を継続使用。 - Box 構造: - Hot Box: `SmallObjectHotBox` (per-thread) - 責務: size→class→page→block のみを扱う。ページ内 freelist pop/push、current/partial 管理。 - Tiny v2 / pool v2 で学んだ「page_of O(1)、current/partial/retire ポリシー」を統合。 - Cold Box: Superslab/Segment/Tier/Guard/Remote - 責務: Superslab/Segment の割当て・解放、Tier/HOT/DRAINING/FREE 管理、Remote Queue、Guard/Budget。 - Hot からは refill/retire の 2 箇所でのみ触れる。 - Policy Box: `SmallObjectPolicySnapshot` - 責務: クラスごとの有効/無効、max_partial_pages、warm_cap などのポリシーを `_Atomic` スナップショットで保持。 - Hot は snapshot を読むだけ。Learner/ENV は snapshot の更新のみ。 - Stats Box / Learning Box: - 責務: page/event 単位の delta を受け取り、Cold 側で集計・観測・学習を行う。 - Hot は「page refill/retire 時」「alloc/free のカウンタ更新」以外では触らない。 - 境界: - Hot → Cold は 2 箇所に集約: - `so_refill_page(cold_ctx, class_idx)` … SmallObjectHotBox が page を 1 枚借りる。 - `so_page_retire(cold_ctx, class_idx, page)` … empty page を Cold 側に返却。 - 現行 TinyColdIface / PoolColdIface の経験を活かし、SmallObject 用 Cold IF を 1 枚設計する。 データ構造案 ------------ ### Page メタ (`so_page_t`) ```c typedef struct so_page_t { void* freelist; // ページ内 block 単位の LIFO freelist uint32_t used; // 使用中 block 数 uint32_t capacity; // ページ内 block 総数 uint16_t class_idx; // SmallObject クラス ID uint16_t flags; // HOT/PARTIAL/FULL などの軽量フラグ uint32_t block_size; // 1 block のバイト数 void* base; // ページ base アドレス void* slab_ref; // Superslab/Segment 側 token (Cold Box 用) struct so_page_t* next; } so_page_t; ``` ポイント: - Tiny v2 / pool v2 のページ構造を統一し、「SmallObject 全体で同じ page 型」を使う。 - page_of は `POOL_PAGE_SIZE` や Superslab サイズに合わせた mask + header で O(1) を前提とする(pool v2 で得た知見)。 ### クラス状態 (`so_class_t`) ```c typedef struct so_class_t { so_page_t* current; // ホットページ so_page_t* partial; // 空きありページのリスト uint16_t max_partial; // partial に保持する上限枚数 uint16_t partial_count; // 現在の partial 枚数 uint32_t block_size; // クラスの block サイズ } so_class_t; ``` - Tiny C7 Safe / TinyHotHeap v2 / pool v2 の current/partial/retire ポリシーを統合。 - full リストは v3 初期段階では不要(必要になったら追加)。 ### TLS コンテキスト (`so_ctx_t`) ```c typedef struct so_ctx_t { so_class_t cls[SMALLOBJECT_NUM_CLASSES]; } so_ctx_t; ``` - TLS (`__thread`) で per-thread の SmallObject コンテキストを保持。 - 初期化時にクラスごとの `block_size` / `max_partial` / ポリシー値をセットする。 Cold IF (SmallObjectColdIface) のイメージ ---------------------------------------- ```c typedef struct SmallObjectColdIface { so_page_t* (*refill_page)(void* cold_ctx, uint32_t class_idx); void (*retire_page)(void* cold_ctx, uint32_t class_idx, so_page_t* page); } SmallObjectColdIface; ``` - `refill_page`: - Cold Box が Superslab/Segment から SmallObject 用ページを 1 枚切り出し、 - `base` / `block_size` / `capacity` / `slab_ref` を設定した `so_page_t` を返す。 - v3 では `so_page_t` 自体を Cold 側で確保する案と、Hot 側 node を再利用する案のどちらかを選べるようにしておく。 - `retire_page`: - `used==0` のページを Cold Box に返却し、Tier/Guard/Remote の扱いは Cold 側に委譲。 - 現行の TinyColdIface / pool Cold IF をラップする形で、SmallObject 用 Cold IF を段階的に導入する。 Hot パス設計(alloc/free) ------------------------- ### alloc (Hot パス) ```c void* so_alloc_fast(so_ctx_t* ctx, uint32_t ci) { so_class_t* hc = &ctx->cls[ci]; so_page_t* p = hc->current; if (likely(p && p->freelist && p->used < p->capacity)) { void* blk = p->freelist; p->freelist = *(void**)blk; p->used++; return blk; } if (hc->partial) { p = hc->partial; hc->partial = p->next; p->next = NULL; hc->current = p; if (p->freelist && p->used < p->capacity) { void* blk = p->freelist; p->freelist = *(void**)blk; p->used++; return blk; } } return NULL; // slow_refill へ } ``` - Slow パス (`so_alloc_refill_slow`) では: - `SmallObjectColdIface.refill_page()` で Cold Box からページを 1 枚借りる。 - `so_page_t` に geometry を設定し、ページ内 freelist を Hot Box 側で carve。 - `hc->current` にセットしてから `so_alloc_fast` で pop。 ### free (Hot パス) ```c void so_free_fast(so_ctx_t* ctx, uint32_t ci, void* ptr) { so_class_t* hc = &ctx->cls[ci]; so_page_t* p = so_page_of(ptr); // O(1) page_of *(void**)ptr = p->freelist; p->freelist = ptr; p->used--; if (p->used == 0) { if (hc->partial_count < hc->max_partial) { p->next = hc->partial; hc->partial = p; hc->partial_count++; } else { so_page_retire_slow(ctx, ci, p); // Cold IF 経由 } if (hc->current == p) hc->current = NULL; } else { if (!hc->current) hc->current = p; } } ``` - Tiny v2 / pool v2 で使った「空ページ温存 or retire」のポリシーを、クラス別 `max_partial` で制御する。 - page_of は pool v2 と同様に O(1) で実装し、Fail-Fast ではなく統計+前段 fallback で診断できるようにする。 Front/Gate/Route の統合方針 -------------------------- - size→class→route の LUT は既存 TinyRoute Box を流用しつつ、「SmallObjectHotBox v3 対応 route」を追加する。 - 例: - `ROUTE_SMALL_HEAP_V3` … SmallObjectHotBox v3。 - `ROUTE_TINY_V1` / `ROUTE_POOL_V1` / `ROUTE_LEGACY` … 現行のまま。 - PolicySnapshot で: ```c enum SmallObjectHeapVersion { SO_HEAP_V1 = 0, SO_HEAP_V3 = 1, }; typedef struct SmallObjectPolicySnapshot { uint8_t heap_version[SMALLOBJECT_NUM_CLASSES]; // V1/V3 uint8_t enabled_mask[SMALLOBJECT_NUM_CLASSES]; // クラスごとの ON/OFF uint16_t max_partial[SMALLOBJECT_NUM_CLASSES]; } SmallObjectPolicySnapshot; ``` - Front からは: - `class_idx = size_to_smallobject_class(size);` - `route = g_smallobject_route[class_idx];` - `switch (route) { SMALL_HEAP_V3 / TINY_V1 / POOL_V1 / LEGACY }` という 1 LUT + 1 switch で決定。C7 v2 / Tiny v1 / pool v1 など既存経路もここで選べるようにする。 段階的 rollout 戦略 ------------------- 1. Phase A: 設計・骨格導入(bench/実験専用) - `SmallObjectHotBox` 型・SmallObjectColdIface・PolicySnapshot を導入。 - まずは Tiny C7-only を SmallObjectHotBox v3 経由に差し替え(現行 C7 v2 を v3 枠に移すイメージ)。 - ENV (`HAKMEM_SMALL_HEAP_V3_ENABLED`, `HAKMEM_SMALL_HEAP_V3_CLASSES`) で C7-only を v3 に切り替え可能に。 2. Phase B: C6/C5 など Tiny クラスを v3 に拡張 - C6-heavy / C5-heavy ベンチで C6/C5 を v3 に載せ、v1 vs v3 の perf A/B を取得。 - Mixed 16–1024B で C7-only v3 vs C6+C7 v3 を比較。 - ここまでは pool v1 をそのまま使い、SmallObjectHotBox v3 側で Tiny 相当をまとめて扱う。 3. Phase C: mid/smallmid pool を SmallObject に寄せる - mid/smallmid サイズクラスを SmallObjectHotBox v3 のクラスとして増やし、pool v1 経路の一部を v3 に移管する。 - Cold IF は Superslab/Segment 共通のまま、SmallObject クラスの範囲だけ v3 で扱う。 4. Phase D: v1/v2/v3 の役割を整理 - v1 TinyHeap / pool v1 は完全な fallback/研究箱とし、標準は SmallObjectHotBox v3 をメインにする。 - v2 系(TinyHotHeap v2 / pool v2)は v3 開発の "失敗を記録した箱" として docs/analysis に残す。 非ゴール(この設計フェーズでやらないこと) ----------------------------------------- - Superslab/Segment/Tier/Guard/Remote のフル再設計(Segment サイズや Tier ポリシー変更など)は v3 後半〜v4 テーマとする。 - first-touch/pf/HugePage/NUMA 最適化は SmallObjectHotBox v3 の上に乗る別箱として扱い、この設計では触らない。 - 学習層 (ACE/ELO) の仕様変更は行わず、PolicySnapshot の更新だけを学習側が持ち、Hot パスは snapshot を読むだけにする。 まとめ ------ - SmallObjectHotBox v3 は、TinyHotHeap v2 / pool v2 で得た知見を統合し、「SmallObject 全体を 1 枚の Hot Box」として扱う設計。 - Hot Box と Cold Box の境界を 2 箇所(refill/retire)に絞り、Policy/Stats/Learning を別箱に押し出すことで、Box Theory に沿った形で mimalloc に近い構造を目指す。 - 実装は `docs/design/SMALLOBJECT_HOTBOX_V3_IMPLEMENTATION_GUIDE.md` に従って段階的に行い、常に v1/v2 への rollback path を維持する。 ## Phase65 簡易ベンチメモ(C7-only v3, Tiny/Pool v2 OFF) - 短尺 20k/ws=64: v3 OFF 41.26M ops/s → v3 ON 57.55M ops/s(alloc_refill=49, fallback_v1=0, page_of_fail=0)。 - 長尺 1M/ws=400: v3 OFF 38.26M ops/s → v3 ON 50.24M ops/s(alloc_refill=5077, fallback_v1=0)。 - Mixed 16–1024B 1M/ws=400: v3 OFF 41.56M ops/s → v3 ON 49.40M ops/s(alloc_refill=2446, fallback_v1=0)。 - デフォルトは C7-only ON (`HAKMEM_SMALL_HEAP_V3_ENABLED` 未指定 / `HAKMEM_SMALL_HEAP_V3_CLASSES` 未指定で class7 のみ v3)。明示的に `ENABLED=0` または CLASSES から bit7 を外すことで v1 経路に戻せる。 ## Phase65-HEAP_STATS 追加観測(C7-only v3 A/B, Tiny/Pool v2 OFF) - 短尺 20k/ws=64: - v3 OFF: 40.91M ops/s, `HEAP_STATS[7] fast=11015 slow=1`。 - v3 ON: 56.43M ops/s, v3 stats `alloc_refill=49 fb_v1=0 page_of_fail=0`(短尺ウォームアップ由来の refill)。HEAP_STATS は Tiny v1 経路のみ出力。 - 長尺 1M/ws=400: - v3 OFF: 38.29M ops/s, `HEAP_STATS[7] fast=550099 slow=1`。 - v3 ON: 50.25M ops/s, v3 stats `alloc_refill=5077 fb_v1=0 page_of_fail=0`。 - Mixed 16–1024B 1M/ws=400(参考): - v3 OFF: 42.35M ops/s (`HEAP_STATS[7] fast=283169 slow=1`)。 - v3 ON: 49.60M ops/s (`alloc_refill=2446 fb_v1=0 page_of_fail=0`)。 - まとめ: HEAP_STATS で slow≈1 を維持したまま v3 ON は C7-only/Mixed とも大幅プラス。デフォルトでは C7-only v3 を ON(ENABLED=1, CLASSES デフォルト=0x80)としつつ、混乱を避けるため `HAKMEM_SMALL_HEAP_V3_ENABLED=0` / クラスマスクでいつでも v1 に戻せるようにしている。*** ***