Files
hakmem/docs/analysis/LARSON_PERFORMANCE_ANALYSIS_2025_11_05.md
Moe Charm (CI) 67fb15f35f Wrap debug fprintf in !HAKMEM_BUILD_RELEASE guards (Release build optimization)
## Changes

### 1. core/page_arena.c
- Removed init failure message (lines 25-27) - error is handled by returning early
- All other fprintf statements already wrapped in existing #if !HAKMEM_BUILD_RELEASE blocks

### 2. core/hakmem.c
- Wrapped SIGSEGV handler init message (line 72)
- CRITICAL: Kept SIGSEGV/SIGBUS/SIGABRT error messages (lines 62-64) - production needs crash logs

### 3. core/hakmem_shared_pool.c
- Wrapped all debug fprintf statements in #if !HAKMEM_BUILD_RELEASE:
  - Node pool exhaustion warning (line 252)
  - SP_META_CAPACITY_ERROR warning (line 421)
  - SP_FIX_GEOMETRY debug logging (line 745)
  - SP_ACQUIRE_STAGE0.5_EMPTY debug logging (line 865)
  - SP_ACQUIRE_STAGE0_L0 debug logging (line 803)
  - SP_ACQUIRE_STAGE1_LOCKFREE debug logging (line 922)
  - SP_ACQUIRE_STAGE2_LOCKFREE debug logging (line 996)
  - SP_ACQUIRE_STAGE3 debug logging (line 1116)
  - SP_SLOT_RELEASE debug logging (line 1245)
  - SP_SLOT_FREELIST_LOCKFREE debug logging (line 1305)
  - SP_SLOT_COMPLETELY_EMPTY debug logging (line 1316)
- Fixed lock_stats_init() for release builds (lines 60-65) - ensure g_lock_stats_enabled is initialized

## Performance Validation

Before: 51M ops/s (with debug fprintf overhead)
After:  49.1M ops/s (consistent performance, fprintf removed from hot paths)

## Build & Test

```bash
./build.sh larson_hakmem
./out/release/larson_hakmem 1 5 1 1000 100 10000 42
# Result: 49.1M ops/s
```

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 13:14:18 +09:00

9.2 KiB
Raw Blame History

Larson Benchmark Performance Analysis - 2025-11-05

🎯 Executive Summary

HAKMEM は system malloc の 25% (threads=4) / 10.7% (threads=1) しか出ていない

  • Root Cause: Fast Path 自体が複雑(シングルスレッドで既に 10倍遅い
  • Bottleneck: malloc() エントリーポイントの 8+ 分岐チェック
  • Impact: Larson benchmark で致命的な性能低下

📊 測定結果

性能比較 (Larson benchmark, size=8-128B)

測定条件 HAKMEM system malloc HAKMEM/system
Single-thread (threads=1) 0.46M ops/s 4.29M ops/s 10.7% 💀
Multi-thread (threads=4) 1.81M ops/s 7.23M ops/s 25.0%
Performance Gap - - -75% @ MT, -89% @ ST

A/B テスト結果 (threads=4)

Profile Throughput vs system 設定の違い
tinyhot_tput 1.81M ops/s 25.0% Fast Cap 64, Adopt ON
tinyhot_best 1.76M ops/s 24.4% Fast Cap 16, TLS List OFF
tinyhot_noadopt 1.73M ops/s 23.9% Adopt OFF
tinyhot_sll256 1.38M ops/s 19.1% SLL Cap 256
tinyhot_optimized 1.23M ops/s 17.0% Fast Cap 16, Magazine OFF

結論: プロファイル調整では改善せず(-3.9% ~ +0.6% の微差)


🔬 Root Cause Analysis

問題1: malloc() エントリーポイントが複雑 (Primary Bottleneck)

Location: core/hakmem.c:1250-1316

System tcache との比較:

System tcache HAKMEM malloc()
0 branches 8+ branches (毎回実行)
3-4 instructions 50+ instructions
直接 tcache pop 多段階チェック → Fast Path

Overhead 分析:

void* malloc(size_t size) {
    // Branch 1: Recursion guard
    if (g_hakmem_lock_depth > 0) { return __libc_malloc(size); }

    // Branch 2: Initialization guard
    if (g_initializing != 0) { return __libc_malloc(size); }

    // Branch 3: Force libc check
    if (hak_force_libc_alloc()) { return __libc_malloc(size); }

    // Branch 4: LD_PRELOAD mode check (getenv呼び出しの可能性)
    int ld_mode = hak_ld_env_mode();

    // Branch 5-8: jemalloc, initialization, LD_SAFE, size check...

    // ↓ ようやく Fast Path
    #ifdef HAKMEM_TINY_FAST_PATH
        void* ptr = tiny_fast_alloc(size);
    #endif
}

推定コスト: 8 branches × 5 cycles/branch = 40 cycles overhead (system tcache は 0)


問題2: Fast Path の階層が深い

HAKMEM 呼び出し経路:

malloc()                         [8+ branches]
  ↓
tiny_fast_alloc()                [class mapping]
  ↓
g_tiny_fast_cache[class] pop     [3-4 instructions]
  ↓ (cache miss)
tiny_fast_refill()               [function call overhead]
  ↓
for (i=0; i<16; i++)            [loop]
    hak_tiny_alloc()             [複雑な内部処理]

System tcache 呼び出し経路:

malloc()
  ↓
tcache[class] pop                [3-4 instructions]
  ↓ (cache miss)
_int_malloc()                    [chunk from bin]

差分: HAKMEM は 4-5 階層、system は 2 階層


問題3: Refill コストが高い

Location: core/tiny_fastcache.c:58-78

現在の実装:

// Batch refill: 16個を個別に取得
for (int i = 0; i < TINY_FAST_REFILL_BATCH; i++) {
    void* ptr = hak_tiny_alloc(size);  // 関数呼び出し × 16
    *(void**)ptr = g_tiny_fast_cache[class_idx];
    g_tiny_fast_cache[class_idx] = ptr;
}

問題点:

  • hak_tiny_alloc() を 16 回呼ぶ(関数呼び出しオーバーヘッド)
  • 各呼び出しで内部の Magazine/SuperSlab を経由
  • Larson は malloc/free が頻繁 → refill も頻繁 → コスト増大

推定コスト: 16 calls × 100 cycles/call = 1,600 cycles (system tcache は ~200 cycles)


💡 改善案

Option A: malloc() ガードチェック最適化

Goal: 分岐数を 8+ → 2-3 に削減

Implementation:

void* malloc(size_t size) {
    // Fast path: 初期化済み & Tiny サイズ
    if (__builtin_expect(g_initialized && size <= 128, 1)) {
        // Direct inline TLS cache access (0 extra branches!)
        int cls = size_to_class_inline(size);
        void* head = g_tls_cache[cls];
        if (head) {
            g_tls_cache[cls] = *(void**)head;
            return head;  // 🚀 3-4 instructions total
        }
        // Cache miss → refill
        return tiny_fast_refill(cls);
    }

    // Slow path: 既存のチェック群 (初回のみ or 非 Tiny サイズ)
    if (g_hakmem_lock_depth > 0) { return __libc_malloc(size); }
    // ... 他のチェック
}

Expected Improvement: +200-400% (0.46M → 1.4-2.3M ops/s @ threads=1)

Risk: Low (分岐を並び替えるだけ)

Effort: 3-5 days


Option B: Refill 効率化

Goal: Refill コストを 1,600 cycles → 200 cycles に削減

Implementation:

void* tiny_fast_refill(int class_idx) {
    // Before: hak_tiny_alloc() を 16 回呼ぶ
    // After: SuperSlab から直接 batch 取得
    void* batch[64];
    int count = superslab_batch_alloc(class_idx, batch, 64);

    // Push to cache in one pass
    for (int i = 0; i < count; i++) {
        *(void**)batch[i] = g_tls_cache[class_idx];
        g_tls_cache[class_idx] = batch[i];
    }

    // Pop one for caller
    void* result = g_tls_cache[class_idx];
    g_tls_cache[class_idx] = *(void**)result;
    return result;
}

Expected Improvement: +30-50% (追加効果)

Risk: Medium (SuperSlab への batch API 追加が必要)

Effort: 5-7 days


Option C: Fast Path 完全単純化 (Ultimate)

Goal: System tcache と同等の設計 (3-4 instructions)

Implementation:

// 1. malloc() を完全に書き直し
void* malloc(size_t size) {
    // Ultra-fast path: 条件チェック最小化
    if (__builtin_expect(size <= 128, 1)) {
        return tiny_ultra_fast_alloc(size);
    }

    // Slow path (非 Tiny)
    return hak_alloc_at(size, HAK_CALLSITE());
}

// 2. Ultra-fast allocator (inline)
static inline void* tiny_ultra_fast_alloc(size_t size) {
    int cls = size_to_class_inline(size);
    void* head = g_tls_cache[cls];

    if (__builtin_expect(head != NULL, 1)) {
        g_tls_cache[cls] = *(void**)head;
        return head;  // HIT: 3-4 instructions
    }

    // MISS: refill
    return tiny_ultra_fast_refill(cls);
}

Expected Improvement: +400-800% (0.46M → 2.3-4.1M ops/s @ threads=1)

Risk: Medium-High (malloc() 全体の再設計)

Effort: 1-2 weeks


🎯 推奨アクション

Phase 1 (1週間): Option A (ガードチェック最適化)

Priority: High Impact: High (+200-400%) Risk: Low

Steps:

  1. g_initialized をキャッシュ化TLS 変数)
  2. Fast path を最優先に移動
  3. 分岐予測ヒントを追加 (__builtin_expect)

Success Criteria: 0.46M → 1.4M ops/s @ threads=1 (+200%)


Phase 2 (3-5日): Option B (Refill 効率化)

Priority: Medium Impact: Medium (+30-50%) Risk: Medium

Steps:

  1. superslab_batch_alloc() API を実装
  2. tiny_fast_refill() を書き直し
  3. A/B テストで効果確認

Success Criteria: 追加 +30% (1.4M → 1.8M ops/s @ threads=1)


Phase 3 (1-2週間): Option C (Fast Path 完全単純化)

Priority: High (Long-term) Impact: Very High (+400-800%) Risk: Medium-High

Steps:

  1. malloc() を完全に書き直し
  2. System tcache と同等の設計
  3. 段階的リリースfeature flag で切り替え)

Success Criteria: 2.3-4.1M ops/s @ threads=1 (system の 54-95%)


📚 参考資料

既存の最適化 (CLAUDE.md より)

Phase 6-1.7 (Box Refactor):

  • 達成: 1.68M → 2.75M ops/s (+64%)
  • 手法: TLS freelist 直接 pop、Batch Refill
  • しかし: これでも system の 25% しか出ていない

Phase 6-2.1 (P0 Optimization):

  • 達成: superslab_refill の O(n) → O(1) 化
  • 効果: 内部 -12% だが全体効果は限定的
  • 教訓: Bottleneck は malloc() エントリーポイント

System tcache 仕様

GNU libc tcache (per-thread cache):

  • 64 bins (16B - 1024B)
  • 7 blocks per bin (default)
  • Fast path: 3-4 instructions (no lock, no branch)
  • Refill: _int_malloc() から chunk を取得

mimalloc:

  • Free list per size class
  • Thread-local pages
  • Fast path: 4-5 instructions
  • Refill: Page から batch 取得

🔍 関連ファイル

  • core/hakmem.c:1250-1316 - malloc() エントリーポイント
  • core/tiny_fastcache.c:41-88 - Fast Path refill
  • core/tiny_alloc_fast.inc.h - Box 5 Fast Path 実装
  • scripts/profiles/tinyhot_*.env - A/B テスト用プロファイル

📝 結論

HAKMEM の Larson 性能低下(-75%は、Fast Path の構造的な問題が原因。

  1. Root Cause 特定: シングルスレッドで 10.7% しか出ていない
  2. Bottleneck 特定: malloc() エントリーポイントの 8+ 分岐
  3. 解決策提案: Option A (分岐削減) で +200-400% 改善可能

次のステップ: Option A の実装を開始 → Phase 1 で 0.46M → 1.4M ops/s を達成


Date: 2025-11-05 Author: Claude (Ultrathink Analysis Mode) Status: Analysis Complete