Files
hakmem/docs/analysis/PHASE4_REGRESSION_ANALYSIS.md

546 lines
15 KiB
Markdown
Raw Normal View History

# Phase 4 性能退行の原因分析と改善戦略
## Executive Summary
**Phase 4 実装結果**:
- Phase 3: 391 M ops/sec
- Phase 4: 373-380 M ops/sec
- **退行**: -3.6%
**根本原因**:
> "free で先払いpush型" は spill 頻発系で負ける。"必要時だけ取るpull型" に切り替えるべき
**解決策(優先順)**:
1. **Option E**: ゲーティング+バッチ化(構造改善)
2. **Option D**: Trade-off 測定(科学的検証)
3. **Option A+B**: マイクロ最適化Quick Win
4. **Pull型反転**: 根本的アーキテクチャ変更
---
## Phase 4 で実装した内容
### 目的
TLS Magazine から slab への spill 時に、TLS-active な slab の場合は mini-magazine に優先的に戻すことで、**次回の allocation を高速化**する。
### 実装hakmem_tiny.c:890-922
```c
// Phase 4: TLS Magazine spill logic (hak_tiny_free_with_slab 関数内)
for (int i = 0; i < mag->count; i++) {
TinySlab* owner = hak_tiny_owner_slab(it.ptr);
// 追加されたチェック(ここが overhead になっている)
int is_tls_active = (owner == g_tls_active_slab_a[owner->class_idx] ||
owner == g_tls_active_slab_b[owner->class_idx]);
if (is_tls_active && !mini_mag_is_full(&owner->mini_mag)) {
// Fast path: mini-magazine に戻すbitmap 触らない)
mini_mag_push(&owner->mini_mag, it.ptr);
stats_record_free(owner->class_idx);
continue;
}
// Slow path: bitmap 直接書き込み(既存ロジック)
// ... bitmap operations ...
}
```
### 設計意図
**Trade-off**:
- **Free path**: わずかな overhead を追加is_tls_active チェック)
- **Alloc path**: mini-magazine から取れるので高速化bitmap scan 回避)
**期待シナリオ**:
- Spill は稀TLS Magazine が満杯になる頻度は低い)
- Mini-magazine にアイテムがあれば次回 allocation が速い5-6ns → 1-2ns
---
## 問題分析
### Overhead の内訳
**毎アイテムごとに実行されるコスト**:
```c
int is_tls_active = (owner == g_tls_active_slab_a[owner->class_idx] ||
owner == g_tls_active_slab_b[owner->class_idx]);
```
1. `owner->class_idx` メモリアクセス × **2回**
2. `g_tls_active_slab_a[...]` TLS アクセス
3. `g_tls_active_slab_b[...]` TLS アクセス
4. ポインタ比較 × 2回
5. `mini_mag_is_full()` チェック
**推定コスト**: 約 2-3 ns per item
### Benchmark 特性bench_tiny
**ワークロード**:
- 100 alloc → 100 free を 10M 回繰り返す
- TLS Magazine capacity: 2048 items
- Spill trigger: Magazine が満杯2048 items
- Spill size: 256 items
**Spill 頻度**:
- 100 alloc × 10M = 1B allocations
- Spill 回数: 1B / 2048 ≈ 488k spills
- Total spill items: 488k × 256 = 125M items
**Phase 4 総コスト**:
- 125M items × 2.5 ns = **312.5 ms overhead**
- Total time: ~5.3 sec
- Overhead 比率: 312.5 / 5300 = **5.9%**
**Phase 4 による恩恵**:
- TLS Magazine が高水位≥75%のとき、mini-magazine からの allocation は**発生しない**
-**恩恵ゼロ、コストだけ可視化**
### 根本的な設計ミス
> **「free で加速の仕込みをするpush型」は、spill が頻発する系bench_tinyではコスト先払いになり負けやすい。**
**問題点**:
1. **Spill が頻繁**: bench_tiny では 488k spills
2. **TLS Magazine が高水位**: 次回 alloc は TLS から出るmini-mag 不要)
3. **先払いコスト**: すべての spill item に overhead
4. **恩恵なし**: Mini-mag からの allocation が発生しない
**正しいアプローチ**:
- **Pull型**: Allocation 側で必要時だけ mini-mag から取る
- **ゲーティング**: TLS Magazine 高水位時は Phase 4 スキップ
- **バッチ化**: Slab 単位で判定(アイテム単位ではなく)
---
## ChatGPT Pro のアドバイス
### 1. 最優先で実装すべき改善案
#### **Option E: ゲーティング+バッチ化**(最重要・新提案)
**E-1: High-water ゲート**
```c
// spill 開始前に一度だけ判定
int tls_occ = tls_mag_occupancy();
if (tls_occ >= TLS_MAG_HIGH_WATER) {
// 全件 bitmap へ直書きPhase 4 無効)
fast_spill_all_to_bitmap(mag);
return;
}
```
**効果**:
- TLS Magazine が高水位≥75%のとき、Phase 4 を丸ごとスキップ
- 「どうせ次回 alloc は TLS から出る」局面での無駄仕事を**ゼロ化**
**E-2: Per-slab バッチ**
```c
// Spill 256 items を slab 単位でグルーピング32 バケツ線形プローブ)
// is_tls_active 判定: 256回 → slab数回通常 1-8回に激減
Bucket bk[BUCKETS] = {0};
// 1st pass: グルーピング
for (int i = 0; i < mag->count; ++i) {
TinySlab* owner = hak_tiny_owner_slab(mag->items[i]);
size_t h = ((uintptr_t)owner >> 6) & (BUCKETS-1);
while (bk[h].owner && bk[h].owner != owner) h = (h+1) & (BUCKETS-1);
if (!bk[h].owner) bk[h].owner = owner;
bk[h].ptrs[bk[h].n++] = mag->items[i];
}
// 2nd pass: slab 単位で処理(判定は slab ごとに 1 回)
for (int b = 0; b < BUCKETS; ++b) if (bk[b].owner) {
TinySlab* s = bk[b].owner;
uint8_t cidx = s->class_idx;
TinySlab* tls_a = g_tls_active_slab_a[cidx];
TinySlab* tls_b = g_tls_active_slab_b[cidx];
int is_tls_active = (s == tls_a || s == tls_b);
int room = mini_capacity(&s->mini_mag) - mini_count(&s->mini_mag);
int take = is_tls_active ? min(room, bk[b].n) : 0;
// mini へ一括 push
for (int i = 0; i < take; ++i) mini_push_bulk(&s->mini_mag, bk[b].ptrs[i]);
// 余りは bitmap を word 単位で一括更新
for (int i = take; i < bk[b].n; ++i) bitmap_set_free(s, bk[b].ptrs[i]);
}
```
**効果**:
- `is_tls_active` 判定: 256回 → **slab数回1-8回に激減**
- `mini_mag_is_full()`: 256回 → **1回の room 計算に置換**
- ループ内の負担(ロード/比較/分岐)が**桁で削減**
**期待効果**: 退行 3.6% の主因を根こそぎ排除
---
#### **Option D: Trade-off 測定**(必須)
**測定すべき指標**:
**Free 側コスト**:
- `cost_check_per_item`: is_tls_active の平均コストns
- `spill_items_per_sec`: Spill 件数/秒
**Allocation 側便益**:
- `mini_hit_ratio`: Phase 4 投入分に対する mini-mag からの実消費率
- `delta_alloc_ns`: Bitmap → mini-mag により縮んだ ns~3-4ns
**損益分岐計算**:
```
便益/秒 = mini_hit_ratio × delta_alloc_ns × alloc_from_mini_per_sec
コスト/秒 = cost_check_per_item × spill_items_per_sec
便益 - コスト > 0 のときだけ Phase 4 有効化
```
**簡易版**:
```c
if (mini_hit_ratio < 10% || tls_occupancy > 75%) {
// Phase 4 を一時停止
}
```
---
#### **Option A+B: マイクロ最適化**(ローコスト・即入れる)
**Option A**: 重複メモリアクセスの削減
```c
// Before: owner->class_idx を2回読む
int is_tls_active = (owner == g_tls_active_slab_a[owner->class_idx] ||
owner == g_tls_active_slab_b[owner->class_idx]);
// After: 1回だけ読んで再利用
uint8_t cidx = owner->class_idx;
TinySlab* tls_a = g_tls_active_slab_a[cidx];
TinySlab* tls_b = g_tls_active_slab_b[cidx];
if ((owner == tls_a || owner == tls_b) &&
!mini_mag_is_full(&owner->mini_mag)) {
// ...
}
```
**Option B**: Branch prediction hint
```c
if (__builtin_expect((owner == tls_a || owner == tls_b) &&
!mini_mag_is_full(&owner->mini_mag), 1)) {
// Fast path - likely taken
}
```
**期待効果**: +1-2%(退行解消には不十分)
---
#### **Option C: Locality caching**(状況依存)
```c
TinySlab* last_owner = NULL;
int last_is_tls = 0;
for (...) {
TinySlab* owner = hak_tiny_owner_slab(it.ptr);
int is_tls_active;
if (owner == last_owner) {
is_tls_active = last_is_tls; // Cached!
} else {
uint8_t cidx = owner->class_idx;
is_tls_active = (owner == g_tls_active_slab_a[cidx] ||
owner == g_tls_active_slab_b[cidx]);
last_owner = owner;
last_is_tls = is_tls_active;
}
if (is_tls_active && !mini_mag_is_full(&owner->mini_mag)) {
// ...
}
}
```
**期待効果**: Locality が高い場合 2-3%Option E で自然に内包される)
---
### 2. 見落としている最適化手法
#### **Pull 型への反転**(根本改善)
**現状Push型**:
- Free 側spillで "先に" mini-mag へ押し戻す
- すべての spill item に overhead
- 恩恵は allocation 側で発生するが、発生しないこともある
**改善Pull型**:
```c
// alloc_slow() で bitmap に降りる"直前"
TinySlab* s = g_tls_active_slab_a[class_idx];
if (s && !mini_mag_is_empty(&s->mini_mag)) {
int pulled = mini_pull_batch(&s->mini_mag, tls_mag, PULL_BATCH);
if (pulled > 0) return tls_mag_pop();
}
```
**効果**:
- Free 側から is_tls_active 判定を**完全に外せる**
- Free レイテンシを確実に守れる
- Allocation 側で必要時だけ取るoverhead の先払いなし)
---
#### **2段 bitmap + word 一括操作**
**現状**:
- Bit 単位で set/clear
**改善**:
```c
// Summary bitmap (2nd level): 非空 word のビットセット
uint64_t bm_top; // 各ビットが 1 word (64 items) を表す
uint64_t bm_word[N]; // 実際の bitmap
// Spill 時: word 単位で一括 OR
for (int i = 0; i < group_count; i += 64) {
int word_idx = block_idx / 64;
bm_word[word_idx] |= free_mask; // 一括 OR
if (bm_word[word_idx]) bm_top |= (1ULL << (word_idx / 64));
}
```
**効果**:
- 空 word のスキャンをゼロに
- キャッシュ効率向上
---
#### **事前容量の読み切り**
```c
// Before: mini_mag_is_full() を毎回呼ぶ
if (!mini_mag_is_full(&owner->mini_mag)) {
mini_mag_push(...);
}
// After: room を一度計算
int room = mini_capacity(&s->mini_mag) - mini_count(&s->mini_mag);
if (room == 0) {
// Phase 4 スキップmini へは push しない)
}
int take = min(room, group_count);
for (int i = 0; i < take; ++i) {
mini_mag_push(...); // is_full チェック不要
}
```
---
#### **High/Low-water 二段制御**
```c
int tls_occ = tls_mag_occupancy();
if (tls_occ >= HIGH_WATER) {
// Phase 4 全 skip
} else if (tls_occ <= LOW_WATER) {
// Phase 4 積極採用
} else {
// 中間域: Slab バッチのみ(細粒度チェックなし)
}
```
---
### 3. 設計判断の妥当性
#### 一般論
> "Free で小さな負担を追加して alloc を速くする" は**条件付きで有効**
**有効な条件**:
1. Free の上振れ頻度が低いspill が稀)
2. Alloc が実際に恩恵を受けるhit 率が高い)
3. 先払いコスト < 後払い便益
#### bench_tiny での失敗理由
- ❌ Spill が頻繁488k spills
- ❌ TLS Magazine が高水位hit 率ゼロ)
- ❌ 先払いコスト > 後払い便益(コストだけ可視化)
#### Real-world での可能性
**有利なシナリオ**:
- Burst allocation短時間に大量 alloc → しばらく静穏 → 大量 free
- TLS Magazine が低水位mini-mag からの allocation が発生)
- Spill が稀(コストが amortize される)
**不利なシナリオ**:
- Steady-statealloc/free が均等に発生)
- TLS Magazine が常に高水位
- Spill が頻繁
---
## 実装計画
### Phase 4.1: Quick WinOption A+B
**目標**: 5分で +1-2% 回収
**実装**:
```c
// hakmem_tiny.c:890-922 を修正
uint8_t cidx = owner->class_idx; // 1回だけ読む
TinySlab* tls_a = g_tls_active_slab_a[cidx];
TinySlab* tls_b = g_tls_active_slab_b[cidx];
if (__builtin_expect((owner == tls_a || owner == tls_b) &&
!mini_mag_is_full(&owner->mini_mag), 1)) {
mini_mag_push(&owner->mini_mag, it.ptr);
stats_record_free(cidx);
continue;
}
```
**検証**:
```bash
make bench_tiny && ./bench_tiny
# 期待: 380 → 385-390 M ops/sec
```
---
### Phase 4.2: High-water ゲートOption E-1
**目標**: 10-20分で構造改善
**実装**:
```c
// hak_tiny_free_with_slab() の先頭に追加
int tls_occ = mag->count; // TLS Magazine 占有数
if (tls_occ >= TLS_MAG_HIGH_WATER) {
// Phase 4 無効: 全件 bitmap へ直書き
for (int i = 0; i < mag->count; i++) {
TinySlab* owner = hak_tiny_owner_slab(mag->items[i]);
// ... 既存の bitmap spill ロジック ...
}
return;
}
// tls_occ < HIGH_WATER の場合のみ Phase 4 実行
// ... 既存の Phase 4 ロジック ...
```
**定数**:
```c
#define TLS_MAG_HIGH_WATER (TLS_MAG_CAPACITY * 3 / 4) // 75%
```
**検証**:
```bash
make bench_tiny && ./bench_tiny
# 期待: 385 → 390-395 M ops/secPhase 3 レベルに回復)
```
---
### Phase 4.3: Per-slab バッチOption E-2
**目標**: 30-40分で根本解決
**実装**: 上記の E-2 コード例を参照
**検証**:
```bash
make bench_tiny && ./bench_tiny
# 期待: 390 → 395-400 M ops/secPhase 3 を超える)
```
---
### Phase 4.4: Pull 型反転(将来)
**目標**: 根本的アーキテクチャ変更
**実装箇所**: `hak_tiny_alloc()` の bitmap scan 直前
**検証**: Real-world benchmarks で評価
---
## 測定フレームワーク
### 追加する統計
```c
// hakmem_tiny.h
typedef struct {
// 既存
uint64_t alloc_count[TINY_NUM_CLASSES];
uint64_t free_count[TINY_NUM_CLASSES];
uint64_t slab_count[TINY_NUM_CLASSES];
// Phase 4 測定用
uint64_t phase4_spill_count[TINY_NUM_CLASSES]; // Phase 4 実行回数
uint64_t phase4_mini_push[TINY_NUM_CLASSES]; // Mini-mag へ push した件数
uint64_t phase4_bitmap_spill[TINY_NUM_CLASSES]; // Bitmap へ spill した件数
uint64_t phase4_gate_skip[TINY_NUM_CLASSES]; // High-water でスキップした回数
} TinyPool;
```
### 損益計算
```c
void hak_tiny_print_phase4_stats(void) {
for (int i = 0; i < TINY_NUM_CLASSES; i++) {
uint64_t total_spill = g_tiny_pool.phase4_spill_count[i];
uint64_t mini_push = g_tiny_pool.phase4_mini_push[i];
uint64_t gate_skip = g_tiny_pool.phase4_gate_skip[i];
double mini_ratio = (double)mini_push / total_spill;
double gate_ratio = (double)gate_skip / total_spill;
printf("Class %d: mini_ratio=%.2f%%, gate_ratio=%.2f%%\n",
i, mini_ratio * 100, gate_ratio * 100);
}
}
```
---
## 結論
### 優先順位
1. **Short-term**: Option A+B → High-water ゲート
2. **Mid-term**: Per-slab バッチ
3. **Long-term**: Pull 型反転
### 成功基準
- Phase 4.1A+B: 385-390 M ops/sec+1-2%
- Phase 4.2(ゲート): 390-395 M ops/secPhase 3 レベル回復)
- Phase 4.3(バッチ): 395-400 M ops/secPhase 3 超え)
### Revert 判断
Phase 4.2(ゲート)を実装しても Phase 3 レベル391 M ops/secに戻らない場合:
- Phase 4 全体を revert
- Pull 型アプローチを検討
---
## References
- ChatGPT Pro アドバイス2025-10-26
- HYBRID_IMPLEMENTATION_DESIGN.md
- TINY_POOL_OPTIMIZATION_ROADMAP.md