286 lines
8.9 KiB
Markdown
286 lines
8.9 KiB
Markdown
|
|
# 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*
|