- 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>
19 KiB
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 研究ライン として再定義する。
重要な設計変更
-
C7 ULTRA は frozen 箱として独立
- TinyC7UltraBox / C7UltraSegmentBox はそのまま維持
- v6 は C7 に一切触らない(C6-only)
-
ptr 分類は RegionIdBox に一元化
- 従来: classify_ptr / hak_super_lookup / ss_fast_lookup が分散
- v6:
region_id_lookup_v6(ptr)で (kind, region_id, page_meta*) を返す
-
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 層に固定する。
-
L0: ULTRA lane
- C7・ごく少数の超ホットクラス専用。
- TLS freelist + small ULTRA segment(2MiB / 64KiB page)+ mask 判定のみを HotPath とする。
- ヘッダレス or side-meta 前提。header 書き・学習はすべて slow/refill 側。
-
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 側に持つ。
-
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 は、この層の内部で完結させる。
- Segment v6: 2MiB Segment / 64KiB Page +
-
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/ページメタ
#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 のルール
alloc(CORE 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パターンで処理
...
}
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 を取得する。
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 構造体と通知
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 を検証し、
- どの allocator(Tiny/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 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 設計の固定事項
-
class_idx ヒント
- class_idx の決定責務:
- alloc: front の size→class LUT。
- free: front の header→class 読み(v6 の外側)。
- v6 への渡し方:
- 関数引数で渡し、v6 側では header を一切触らない。
- class_idx の決定責務:
-
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 として扱う。
- Hot segment は常に TLS 上 1 つ(
-
L2→L3 Stats
- retire/refill 時の page lifetime summary(SmallPageStatsV6)のみを渡す。
- HotPath(alloc/free)では Stats を一切更新しない。
-
RegionIdBox
- page 単位で所有者(v6 / legacy / pool)を管理。
- 段階的に header ベース guard から RegionIdBox に移行し、最終的には header を廃止可能な設計にする。
フェーズ案(v6)
-
Phase v6-0: 設計ドキュメントと型・IF 追加(完全 OFF)
- 本ドキュメントを作成し、SmallPageMetaV6 / SmallClassHeapV6 / SmallHeapCtxV6 / SmallSegmentV6 / ColdIface_v6 の型とヘッダだけ追加。
- ENV は
HAKMEM_SMALL_HEAP_V6_ENABLED=0デフォルトで route からは一切呼ばれない。
-
Phase v6-1: C6-only CORE v6 route stub
- C6 を route_snapshot で
SMALL_ROUTE_CORE_V6に振れるようにしつつ、中身は v1/pool に即フォールバック(動作は変えない)。
- C6 を route_snapshot で
-
Phase v6-2: C6-only Core v6 実装(Segment + TLS freelist)
- C6 について ULTRA に似た TLS freelist + Segment ベースの Core v6 を実装。
- C6-heavy で v1/pool と A/B、安定・回帰幅を確認。
-
Phase v6-3: Mixed での段階的 CORE v6 昇格
- C6 → C5 → 他クラスと、hot class から CORE v6 に載せ、Mixed 16–1024B の perf を確認。
- C7 ULTRA(L0)と CORE v6(L1)の共存チューニング。
-
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 削減)に進む。
- C6 で安定・baseline 同等が確認できたら、C5 / C4 を順次 CORE v6 に載せていき、free hotpath の
以降の 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 拡張を試行したが回帰が大きく研究箱に留める。