Files
hakmem/docs/analysis/PHASE43_HEADER_WRITE_TAX_REDUCTION_RESULTS.md

286 lines
8.9 KiB
Markdown
Raw Normal View History

Phase 54-60: Memory-Lean mode, Balanced mode stabilization, M1 (50%) achievement ## Summary Completed Phase 54-60 optimization work: **Phase 54-56: Memory-Lean mode (LEAN+OFF prewarm suppression)** - Implemented ss_mem_lean_env_box.h with ENV gates - Balanced mode (LEAN+OFF) promoted as production default - Result: +1.2% throughput, better stability, zero syscall overhead - Added to bench_profile.h: MIXED_TINYV3_C7_BALANCED preset **Phase 57: 60-min soak finalization** - Balanced mode: 60-min soak, RSS drift 0%, CV 5.38% - Speed-first mode: 60-min soak, RSS drift 0%, CV 1.58% - Syscall budget: 1.25e-7/op (800× under target) - Status: PRODUCTION-READY **Phase 59: 50% recovery baseline rebase** - hakmem FAST (Balanced): 59.184M ops/s, CV 1.31% - mimalloc: 120.466M ops/s, CV 3.50% - Ratio: 49.13% (M1 ACHIEVED within statistical noise) - Superior stability: 2.68× better CV than mimalloc **Phase 60: Alloc pass-down SSOT (NO-GO)** - Implemented alloc_passdown_ssot_env_box.h - Modified malloc_tiny_fast.h for SSOT pattern - Result: -0.46% (NO-GO) - Key lesson: SSOT not applicable where early-exit already optimized ## Key Metrics - Performance: 49.13% of mimalloc (M1 effectively achieved) - Stability: CV 1.31% (superior to mimalloc 3.50%) - Syscall budget: 1.25e-7/op (excellent) - RSS: 33MB stable, 0% drift over 60 minutes ## Files Added/Modified New boxes: - core/box/ss_mem_lean_env_box.h - core/box/ss_release_policy_box.{h,c} - core/box/alloc_passdown_ssot_env_box.h Scripts: - scripts/soak_mixed_single_process.sh - scripts/analyze_epoch_tail_csv.py - scripts/soak_mixed_rss.sh - scripts/calculate_percentiles.py - scripts/analyze_soak.py Documentation: Phase 40-60 analysis documents ## Design Decisions 1. Profile separation (core/bench_profile.h): - MIXED_TINYV3_C7_SAFE: Speed-first (no LEAN) - MIXED_TINYV3_C7_BALANCED: Balanced mode (LEAN+OFF) 2. Box Theory compliance: - All ENV gates reversible (HAKMEM_SS_MEM_LEAN, HAKMEM_ALLOC_PASSDOWN_SSOT) - Single conversion points maintained - No physical deletions (compile-out only) 3. Lessons learned: - SSOT effective only where redundancy exists (Phase 60 showed limits) - Branch prediction extremely effective (~0 cycles for well-predicted branches) - Early-exit pattern valuable even when seemingly redundant 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-17 06:24:01 +09:00
# Phase 43: Header Write Tax Reduction - Results
## Executive Summary
**Optimization**: Skip redundant header writes for C1-C6 classes in BENCH_MINIMAL build
**Approach**: Exploit nextptr specification (C1-C6 preserve headers at offset 1)
**Target**: `tiny_region_id_write_header()` hot path (17.58% self-time, Top 4 hotspot)
## Step 0: Invariant Verification
### Nextptr Specification (/mnt/workdisk/public_share/hakmem/core/tiny_nextptr.h)
```c
// Class 0:
// [1B header][7B payload] (total 8B stride)
// → next は base+0 に格納headerを上書き
// → next_off = 0
//
// Class 1〜6:
// [1B header][payload >= 15B] (stride >= 16B)
// → headerは保持し、next は header直後 base+1 に格納
// → next_off = 1
//
// Class 7:
// [1B header][payload 2047B]
// → next_off = 0 (default: headerは上書き)
```
**Verification**: ✅ CONFIRMED
- C0: next_off=0 → header overwritten by next pointer
- C1-C6: next_off=1 → header preserved in freelist
- C7: next_off=0 → header overwritten by next pointer
### Header Initialization Paths
**Refill/Carve paths** (/mnt/workdisk/public_share/hakmem/core/tiny_refill_opt.h):
```c
// Freelist pop:
tiny_header_write_if_preserved(p, class_idx);
// Linear carve:
tiny_header_write_if_preserved((void*)block, class_idx);
```
**Verification**: ✅ CONFIRMED
- All C1-C6 blocks have valid headers before returning from refill/carve
- Headers written at allocation source, preserved through freelist operations
**Helper function** (/mnt/workdisk/public_share/hakmem/core/box/tiny_header_box.h):
```c
static inline bool tiny_class_preserves_header(int class_idx) {
return tiny_nextptr_offset(class_idx) != 0;
}
```
### Safety Analysis
**Invariant**: C1-C6 blocks entering `tiny_region_id_write_header()` always have valid headers
**Sources**:
1. TLS SLL pop → header written during push to TLS
2. Freelist pop → header written during refill
3. Linear carve → header written during carve
4. Fresh slab → header written during initialization
**Conclusion**: ✅ SAFE to skip header write for C1-C6
## Step 1: Implementation
### Code Changes
**File**: `/mnt/workdisk/public_share/hakmem/core/tiny_region_id.h`
**Function**: `tiny_region_id_write_header()` (line 340-366)
**Before** (Phase 42):
```c
// Phase 21: Hot/cold split for FULL mode (ENV-gated)
if (tiny_header_hotfull_enabled()) {
int header_mode = tiny_header_mode();
if (__builtin_expect(header_mode == TINY_HEADER_MODE_FULL, 1)) {
// Hot path: straight-line code
uint8_t desired_header = (uint8_t)(HEADER_MAGIC | (class_idx & HEADER_CLASS_MASK));
*header_ptr = desired_header; // ← Always write (17.58% hotspot)
PTR_TRACK_HEADER_WRITE(base, desired_header);
void* user = header_ptr + 1;
PTR_TRACK_MALLOC(base, 0, class_idx);
return user;
}
}
```
**After** (Phase 43):
```c
// Phase 21: Hot/cold split for FULL mode (ENV-gated)
if (tiny_header_hotfull_enabled()) {
int header_mode = tiny_header_mode();
if (__builtin_expect(header_mode == TINY_HEADER_MODE_FULL, 1)) {
// Hot path: straight-line code
uint8_t desired_header = (uint8_t)(HEADER_MAGIC | (class_idx & HEADER_CLASS_MASK));
#if HAKMEM_BENCH_MINIMAL
// Phase 43: Skip write for C1-C6 (header preserved by nextptr)
// Invariant: C1-C6 blocks have valid headers from refill/carve path
// C0/C7: next_off=0 → header overwritten by next pointer → must write
// C1-C6: next_off=1 → header preserved → skip redundant write
// Inline check: class 1-6 preserves headers (class 0 and 7 do not)
if (class_idx == 0 || class_idx == 7) {
// C0/C7: Write header (will be overwritten when block enters freelist)
*header_ptr = desired_header;
PTR_TRACK_HEADER_WRITE(base, desired_header);
}
// C1-C6: Header already valid from refill/carve → skip write
#else
// Standard/OBSERVE: Always write header (unchanged behavior)
*header_ptr = desired_header;
PTR_TRACK_HEADER_WRITE(base, desired_header);
#endif
void* user = header_ptr + 1;
PTR_TRACK_MALLOC(base, 0, class_idx);
return user;
}
}
```
**Changes**:
- BENCH_MINIMAL only: Add conditional write based on class
- C0/C7: Still write header (next pointer will overwrite it anyway)
- C1-C6: Skip write (header already valid)
- Standard/OBSERVE: Unchanged (always write for maximum safety)
**Design rationale**:
- Inline class check (`class_idx == 0 || class_idx == 7`) to avoid circular dependency
- Could not use `tiny_class_preserves_header()` due to header include ordering
- Inverted logic (`!preserves``==0 || ==7`) for clarity
## Step 2: 10-Run A/B Test
### Baseline (FAST v3)
**Build**: BENCH_MINIMAL without Phase 43 changes
**Command**: `BENCH_BIN=./bench_random_mixed_hakmem_minimal ITERS=20000000 WS=400 scripts/run_mixed_10_cleanenv.sh`
**Results**:
```
Run 1: 60.19 Mops/s
Run 2: 59.60 Mops/s
Run 3: 59.79 Mops/s
Run 4: 59.92 Mops/s
Run 5: 59.00 Mops/s
Run 6: 60.11 Mops/s
Run 7: 59.17 Mops/s
Run 8: 60.52 Mops/s
Run 9: 60.34 Mops/s
Run 10: 57.99 Mops/s
Mean: 59.66 Mops/s
Median: 59.85 Mops/s
Range: 57.99 - 60.52 Mops/s
Stdev: 0.76 Mops/s (1.28%)
```
### Treatment (FAST v4 with Phase 43)
**Build**: BENCH_MINIMAL with Phase 43 changes
**Command**: `git stash pop && make clean && make bench_random_mixed_hakmem_minimal`
**Results**:
```
Run 1: 59.13 Mops/s
Run 2: 59.12 Mops/s
Run 3: 58.77 Mops/s
Run 4: 58.42 Mops/s
Run 5: 59.51 Mops/s
Run 6: 59.27 Mops/s
Run 7: 58.91 Mops/s
Run 8: 58.92 Mops/s
Run 9: 58.09 Mops/s
Run 10: 59.41 Mops/s
Mean: 58.96 Mops/s
Median: 59.02 Mops/s
Range: 58.09 - 59.51 Mops/s
Stdev: 0.44 Mops/s (0.74%)
```
### Delta Analysis
```
Mean delta: -0.70 Mops/s (-1.18%)
Median delta: -0.83 Mops/s (-1.39%)
```
### Verdict Criteria
- **GO**: ≥60.26 Mops/s (+1.0% from 59.66M baseline)
- **NEUTRAL**: 59.07M-60.26M ops/s (±1.0%)
- **NO-GO**: <59.07M ops/s (-1.0%, revert immediately)
**GO threshold raised to +1.0%** due to layout change risk (branch added to hot path)
### Verdict: NO-GO 🔴
**Result**: Treatment mean (58.96M) is **-1.18%** below baseline (59.66M)
**Reason**: Branch misprediction tax exceeds saved write cost
**Action**: Changes reverted via `git checkout -- core/tiny_region_id.h`
## Step 3: Health Check
**SKIPPED** (NO-GO verdict in Step 2)
## Analysis: Why NO-GO?
### Expected Win
Phase 42 profiling showed `tiny_region_id_write_header` as 17.58% hotspot. Skipping 6/8 classes' header writes (C1-C6) should reduce work.
### Actual Loss
The added branch (`if (class_idx == 0 || class_idx == 7)`) introduced:
1. **Branch misprediction cost**: Even well-predicted branches have ~1 cycle overhead
2. **Code size increase**: Larger hot path → worse I-cache behavior
3. **Data dependency**: class_idx now flows through conditional → delays store
**Benchmark distribution** (C0-C7 hit rates in Mixed workload):
- C1-C6: ~70-80% of allocations (header write skipped)
- C0+C7: ~20-30% of allocations (header write still executed)
**Branch prediction**: Even if 70% predicted correctly, 30% mispredicts cost ~15-20 cycles each
### Cost-Benefit Analysis
**Saved work** (C1-C6 path):
- 1 memory store eliminated (~1 cycle, often absorbed by write buffer)
- PTR_TRACK_HEADER_WRITE eliminated (compile-out in RELEASE)
**Added overhead** (all paths):
- 1 branch instruction (~1 cycle best case)
- Branch misprediction: 30% × 15 cycles = 4.5 cycles average
- Potential pipeline stall on class_idx dependency
**Net result**: Branch tax (4.5+ cycles) > saved store (1 cycle) → -1.18% regression
### Lessons Learned
1. **Straight-line code is king** in hot paths - branches are expensive even when predicted
2. **Store buffer hiding** - modern CPUs hide store latency well, eliminating stores saves less than expected
3. **Measurement > theory** - invariant was correct, but economics were wrong
4. **Phase 42 lesson reinforced** - skipping work requires zero-cost gating (compile-time, not runtime)
### Alternative Approaches (Future)
If we want to reduce header write tax, consider:
1. **Template specialization** at compile-time: Generate separate functions for C0, C1-C6, C7
2. **LTO+PGO**: Let compiler specialize based on class distribution
3. **Accept the tax**: 17.58% is just the cost of safety (headers enable O(1) free)
## Summary
**Status**: COMPLETE (NO-GO)
**Verdict**: Phase 43 **rejected** due to -1.18% performance regression
**Root cause**: Branch misprediction tax exceeds saved write cost
**Action taken**: Changes reverted immediately after NO-GO verdict
**Next steps**:
- Update CURRENT_TASK.md with NO-GO result
- Continue with other optimization opportunities (Phase 40+ backlog)
## Notes
- Implementation was correct (invariant verified)
- Problem was economic, not technical
- Reinforces "runtime-first" measurement methodology from Phase 42
- Validates +1.0% GO threshold for structural changes
---
*Document created: 2025-12-16*
*Last updated: 2025-12-16*