Files
hakmem/docs/status/PHASE_6.25_6.27_IMPLEMENTATION_PLAN.md

1106 lines
36 KiB
Markdown
Raw Normal View History

# Phase 6.25-6.27: Implementation Plan - Catching Up with mimalloc
**Date**: 2025-10-24
**Status**: 📋 Planning
**Target**: Reach 60-75% of mimalloc performance for Mid Pool
---
## 📊 Current Baseline (Phase 6.21 Results)
### Performance vs mimalloc
| Workload | Threads | hakmem | mimalloc | Ratio | Gap |
|----------|---------|--------|----------|-------|-----|
| **Mid** | 1T | 4.0 M/s | 14.6 M/s | **28%** | -72% |
| **Mid** | 4T | 13.8 M/s | 29.5 M/s | **47%** | -53% |
| Tiny | 1T | 19.4 M/s | 32.6 M/s | 59% | -41% |
| Tiny | 4T | 48.0 M/s | 65.7 M/s | 73% | -27% |
| Large | 1T | 0.6 M/s | 2.1 M/s | 29% | -71% |
**Key Insights**:
-**Phase 6.25 Quick Wins achieved +37.8% for Mid 4T** (10.0 → 13.8 M/s)
- ❌ Mid Pool still significantly behind mimalloc (28% 1T, 47% 4T)
- 🎯 Target: 60-75% of mimalloc = **8.8-11.0 M/s (1T), 17.7-22.1 M/s (4T)**
### Current Mid Pool Architecture
```
┌─────────────────────────────────────────────────────────┐
│ TLS Fast Path (Lock-Free) │
├─────────────────────────────────────────────────────────┤
│ 1. TLS Ring Buffer (RING_CAP=32) │
│ - LIFO cache for recently freed blocks │
│ - Per-class, per-thread │
│ - Phase 6.25: 16→32 increased hit rate │
│ │
│ 2. TLS Active Pages (x2: page_a, page_b) │
│ - Bump-run allocation (no per-block links) │
│ - Owner-thread private (lock-free) │
│ - 64KB pages, split on-demand │
├─────────────────────────────────────────────────────────┤
│ Shared State (Lock-Based) │
├─────────────────────────────────────────────────────────┤
│ 3. Per-class Freelist (64 shards) │
│ - Mutex-protected per (class, shard) │
│ - Site-based sharding (reduce contention) │
│ - Refill on demand via refill_freelist() │
│ │
│ 4. Remote Stack (MPSC, lock-free push) │
│ - Cross-thread free target │
│ - Drained into freelist under lock │
│ │
│ 5. Transfer Cache (TC, Phase 6.20) │
│ - Per-thread inbox (atomic CAS) │
│ - Owner-aware routing │
│ - Drain trigger: ring->top < 2
└─────────────────────────────────────────────────────────┘
Refill Flow (Current):
Ring empty → Check Active Pages → Lock Shard → Pop freelist
→ Drain remote → Shard steal (if CAP reached) → **refill_freelist()**
Refill Implementation:
- Allocates **1 page** (64KB) via mmap
- Splits into blocks, links into freelist
- ACE bundle factor: 1-4 pages (adaptive)
```
### Bottlenecks Identified
**From Phase 6.20 Analysis**:
1. **Refill Latency** (Primary)
- Single-page refill: 1 mmap syscall per refill
- Freelist rebuilding overhead (linking blocks)
- Mutex hold time during refill (~100-150 cycles)
- **Impact**: ~40% of alloc time in Mid 1T
2. **Lock Contention** (Secondary)
- 64 shards × 7 classes = 448 mutexes
- Even with sharding, 4T shows contention
- Trylock success rate: ~60-70% (Phase 6.25 data)
- **Impact**: ~25% of alloc time in Mid 4T
3. **CAP/W_MAX Sub-optimal** (Tertiary)
- Static configuration (no runtime adaptation)
- W_MAX=1.60 (Mid), 1.30 (Large) → some fallback to L1
- CAP={64,64,64,32,16} → conservative, low hit rate
- **Impact**: ~10-15% missed pool opportunities
---
## 🎯 Phase 6.25 本体: Refill Batching
### Goal
**Reduce refill latency by allocating multiple pages at once**
**Target**: Mid 1T: +10-15% (4.0 → 4.5-5.0 M/s)
### Problem Statement
Current `refill_freelist()` allocates **1 page per call**:
- 1 mmap syscall (~200-300 cycles)
- 1 page split + freelist rebuild (~100-150 cycles)
- Held under mutex lock (blocks other threads)
- Amortized cost per block: **HIGH** for small classes (e.g., 2KB = 32 blocks/page)
**Opportunity**: Allocate **2-4 pages in batch** to amortize costs:
- mmap overhead: 300 cycles → 75-150 cycles/page (batched)
- Freelist rebuild: done in parallel or optimized
- Fill multiple TLS page slots + Ring buffer aggressively
### Implementation Approach
#### 1. Create `alloc_tls_page_batch()` Function
**Location**: `hakmem_pool.c` (after `alloc_tls_page()`, line ~486)
**Signature**:
```c
// Allocate multiple pages in batch and distribute to TLS structures
// Returns: number of pages successfully allocated (0-batch_size)
static int alloc_tls_page_batch(int class_idx, int batch_size,
PoolTLSPage* slots[], int num_slots,
PoolTLSRing* ring, PoolTLSBin* bin);
```
**Pseudocode**:
```c
static int alloc_tls_page_batch(int class_idx, int batch_size,
PoolTLSPage* slots[], int num_slots,
PoolTLSRing* ring, PoolTLSBin* bin) {
size_t user_size = g_class_sizes[class_idx];
size_t block_size = HEADER_SIZE + user_size;
int blocks_per_page = POOL_PAGE_SIZE / block_size;
if (blocks_per_page <= 0) return 0;
int allocated = 0;
// Allocate pages in batch (strategy: multiple mmaps or single large mmap)
// Option A: Multiple mmaps (simpler, compatible with existing infra)
for (int i = 0; i < batch_size; i++) {
void* page = mmap(NULL, POOL_PAGE_SIZE, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (!page) break;
// Prefault (Phase 6.25 quick win)
for (size_t j = 0; j < POOL_PAGE_SIZE; j += 4096) {
((volatile char*)page)[j] = 0;
}
// Strategy: Fill TLS slots first, then fill Ring/LIFO
if (allocated < num_slots && slots[allocated]) {
// Assign to TLS active page slot (bump-run init)
PoolTLSPage* ap = slots[allocated];
ap->page = page;
ap->bump = (char*)page;
ap->end = (char*)page + POOL_PAGE_SIZE;
ap->count = blocks_per_page;
// Register page descriptor
mid_desc_register(page, class_idx, (uint64_t)(uintptr_t)pthread_self());
} else {
// Fill Ring + LIFO from this page
char* bump = (char*)page;
char* end = (char*)page + POOL_PAGE_SIZE;
for (int k = 0; k < blocks_per_page; k++) {
PoolBlock* b = (PoolBlock*)(void*)bump;
// Try Ring first, then LIFO
if (ring && ring->top < POOL_TLS_RING_CAP) {
ring->items[ring->top++] = b;
} else if (bin) {
b->next = bin->lo_head;
bin->lo_head = b;
bin->lo_count++;
}
bump += block_size;
if (bump >= end) break;
}
mid_desc_register(page, class_idx, (uint64_t)(uintptr_t)pthread_self());
}
allocated++;
g_pool.total_pages_allocated++;
g_pool.pages_by_class[class_idx]++;
g_pool.total_bytes_allocated += POOL_PAGE_SIZE;
}
if (allocated > 0) {
g_pool.refills[class_idx]++;
}
return allocated;
}
```
#### 2. Modify Refill Call Sites
**Location**: `hakmem_pool.c:931` (inside `hak_pool_try_alloc`, refill path)
**Before**:
```c
if (alloc_tls_page(class_idx, tap)) {
// ... use newly allocated page
}
```
**After**:
```c
// Determine batch size from env var (default 2-4)
int batch = g_pool_refill_batch_size; // new global config
if (batch < 1) batch = 1;
if (batch > 4) batch = 4;
// Prepare slot array (up to 2 TLS slots)
PoolTLSPage* slots[2] = {NULL, NULL};
int num_slots = 0;
if (g_tls_active_page_a[class_idx].page == NULL || g_tls_active_page_a[class_idx].count == 0) {
slots[num_slots++] = &g_tls_active_page_a[class_idx];
}
if (g_tls_active_page_b[class_idx].page == NULL || g_tls_active_page_b[class_idx].count == 0) {
slots[num_slots++] = &g_tls_active_page_b[class_idx];
}
// Call batch allocator
int allocated = alloc_tls_page_batch(class_idx, batch, slots, num_slots,
&g_tls_bin[class_idx].ring,
&g_tls_bin[class_idx]);
if (allocated > 0) {
pthread_mutex_unlock(lock);
// Use ring or active page as usual
// ...
}
```
#### 3. Add Environment Variable
**Global Config** (add to `hakmem_pool.c` globals, ~line 316):
```c
static int g_pool_refill_batch_size = 2; // env: HAKMEM_POOL_REFILL_BATCH (1-4)
```
**Init** (add to `hak_pool_init()`, ~line 716):
```c
const char* e_batch = getenv("HAKMEM_POOL_REFILL_BATCH");
if (e_batch) {
int v = atoi(e_batch);
if (v >= 1 && v <= 4) g_pool_refill_batch_size = v;
}
```
#### 4. Extend TLS Active Page Slots (Optional)
**Current**: 2 slots (page_a, page_b)
**Proposal**: Add page_c, page_d for batch_size=4 (if beneficial)
**Trade-off**:
- ✅ Pro: More TLS-local inventory, fewer shared accesses
- ❌ Con: Increased TLS memory footprint (~256 bytes/class)
**Recommendation**: Start with 2 slots, measure, then extend if needed.
---
### File Changes Required
| File | Function | Change Type | Est. LOC |
|------|----------|-------------|----------|
| `hakmem_pool.c` | `alloc_tls_page_batch()` | **New function** | +80 |
| `hakmem_pool.c` | `hak_pool_try_alloc()` | Modify refill path | +30 |
| `hakmem_pool.c` | Globals | Add `g_pool_refill_batch_size` | +1 |
| `hakmem_pool.c` | `hak_pool_init()` | Parse env var | +5 |
| `hakmem_pool.h` | (none) | No public API change | 0 |
| **Total** | | | **~116 LOC** |
---
### Testing Strategy
#### Unit Test
```bash
# Test batch allocation works
HAKMEM_POOL_REFILL_BATCH=4 ./test_pool_refill
# Verify TLS slots filled correctly
# Check Ring buffer populated
# Check no memory leaks
```
#### Benchmark Test
```bash
# Baseline (batch=1, current behavior)
HAKMEM_POOL_REFILL_BATCH=1 RUNTIME=10 THREADS=1 ./scripts/run_bench_suite.sh
# Batch=2 (conservative)
HAKMEM_POOL_REFILL_BATCH=2 RUNTIME=10 THREADS=1 ./scripts/run_bench_suite.sh
# Batch=4 (aggressive)
HAKMEM_POOL_REFILL_BATCH=4 RUNTIME=10 THREADS=1 ./scripts/run_bench_suite.sh
# Expected: +10-15% on Mid 1T (4.0 → 4.5-5.0 M/s)
```
#### Failure Modes to Watch
1. **Memory bloat**: Batch too large → excessive pre-allocation
- **Monitor**: RSS growth, pages_allocated counter
- **Mitigation**: Cap batch_size at 4, respect CAP limits
2. **Ring overflow**: Batch fills Ring, blocks get lost
- **Monitor**: Ring underflow counter (should decrease)
- **Mitigation**: Properly route overflow to LIFO
3. **TLS slot contention**: Multiple threads allocating same class
- **Monitor**: Active page descriptor conflicts
- **Mitigation**: Per-thread ownership (already enforced)
---
### Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|------------|--------|------------|
| Memory bloat (over-allocation) | Medium | High | Cap at batch=4, respect CAP limits |
| Complexity (harder to debug) | Low | Medium | Extensive logging, unit tests |
| Backward compat (existing workloads) | Low | Low | Default batch=2 (conservative) |
| Regression (slower than 1-page) | Low | Medium | A/B test, fallback to batch=1 |
**Rollback Plan**: Set `HAKMEM_POOL_REFILL_BATCH=1` to restore original behavior (zero code change).
---
### Estimated Time
- **Implementation**: 3-4 hours
- Core function: 2 hours
- Integration: 1 hour
- Testing: 1 hour
- **Benchmarking**: 2 hours
- Run suite 3x (batch=1,2,4)
- Analyze results
- **Total**: **5-6 hours**
---
## 🔓 Phase 6.26: Lock-Free Refill
### Goal
**Eliminate lock contention on freelist access**
**Target**: Mid 4T: +15-20% (13.8 → 16-18 M/s)
### Problem Statement
Current freelist uses **per-shard mutexes** (`pthread_mutex_t`):
- 64 shards × 7 classes = **448 mutexes**
- Contention on hot shards (4T workload)
- Trylock success rate: ~60-70% (Phase 6.25 data)
- Each lock/unlock: ~20-40 cycles overhead
**Opportunity**: Replace mutex with **lock-free stack** (CAS-based):
- Atomic compare-and-swap: ~10-15 cycles
- No blocking (always forward progress)
- Better scalability under contention
### Implementation Approach
#### 1. Replace Freelist Mutex with Atomic Head
**Current Structure** (`hakmem_pool.c:276-280`):
```c
static struct {
PoolBlock* freelist[POOL_NUM_CLASSES][POOL_NUM_SHARDS];
PaddedMutex freelist_locks[POOL_NUM_CLASSES][POOL_NUM_SHARDS];
// ...
} g_pool;
```
**New Structure**:
```c
static struct {
// Lock-free freelist head (atomic pointer)
atomic_uintptr_t freelist_head[POOL_NUM_CLASSES][POOL_NUM_SHARDS];
// Lock-free counter (for non-empty bitmap update)
atomic_uint freelist_count[POOL_NUM_CLASSES][POOL_NUM_SHARDS];
// Keep nonempty_mask (atomic already)
atomic_uint_fast64_t nonempty_mask[POOL_NUM_CLASSES];
// Remote stack (already lock-free)
atomic_uintptr_t remote_head[POOL_NUM_CLASSES][POOL_NUM_SHARDS];
atomic_uint remote_count[POOL_NUM_CLASSES][POOL_NUM_SHARDS];
// ... (rest unchanged)
} g_pool;
```
#### 2. Implement Lock-Free Push/Pop
**Lock-Free Pop** (replace mutex-based pop):
```c
// Pop block from lock-free freelist
// Returns: block pointer, or NULL if empty
static inline PoolBlock* freelist_pop_lockfree(int class_idx, int shard_idx) {
uintptr_t old_head;
PoolBlock* block;
do {
old_head = atomic_load_explicit(&g_pool.freelist_head[class_idx][shard_idx],
memory_order_acquire);
if (!old_head) {
return NULL; // Empty
}
block = (PoolBlock*)old_head;
// Try CAS: freelist_head = block->next
} while (!atomic_compare_exchange_weak_explicit(
&g_pool.freelist_head[class_idx][shard_idx],
&old_head, (uintptr_t)block->next,
memory_order_release, memory_order_acquire));
// Update count
unsigned old_count = atomic_fetch_sub_explicit(
&g_pool.freelist_count[class_idx][shard_idx], 1, memory_order_relaxed);
// Clear nonempty bit if now empty
if (old_count <= 1) {
clear_nonempty_bit(class_idx, shard_idx);
}
return block;
}
```
**Lock-Free Push** (for refill path):
```c
// Push block onto lock-free freelist
static inline void freelist_push_lockfree(int class_idx, int shard_idx, PoolBlock* block) {
uintptr_t old_head;
do {
old_head = atomic_load_explicit(&g_pool.freelist_head[class_idx][shard_idx],
memory_order_acquire);
block->next = (PoolBlock*)old_head;
} while (!atomic_compare_exchange_weak_explicit(
&g_pool.freelist_head[class_idx][shard_idx],
&old_head, (uintptr_t)block,
memory_order_release, memory_order_acquire));
// Update count and nonempty bit
atomic_fetch_add_explicit(&g_pool.freelist_count[class_idx][shard_idx], 1,
memory_order_relaxed);
set_nonempty_bit(class_idx, shard_idx);
}
```
**Lock-Free Batch Push** (for refill, optimization):
```c
// Push multiple blocks atomically (amortize CAS overhead)
static inline void freelist_push_batch_lockfree(int class_idx, int shard_idx,
PoolBlock* head, PoolBlock* tail,
int count) {
uintptr_t old_head;
do {
old_head = atomic_load_explicit(&g_pool.freelist_head[class_idx][shard_idx],
memory_order_acquire);
tail->next = (PoolBlock*)old_head;
} while (!atomic_compare_exchange_weak_explicit(
&g_pool.freelist_head[class_idx][shard_idx],
&old_head, (uintptr_t)head,
memory_order_release, memory_order_acquire));
atomic_fetch_add_explicit(&g_pool.freelist_count[class_idx][shard_idx], count,
memory_order_relaxed);
set_nonempty_bit(class_idx, shard_idx);
}
```
#### 3. Refill Path Integration
**Modify `refill_freelist()`** (now lock-free):
```c
static int refill_freelist(int class_idx, int shard_idx) {
// ... (allocate page, split into blocks)
// OLD: lock → push to freelist → unlock
// pthread_mutex_lock(lock);
// block->next = g_pool.freelist[class_idx][shard_idx];
// g_pool.freelist[class_idx][shard_idx] = freelist_head;
// pthread_mutex_unlock(lock);
// NEW: lock-free batch push
PoolBlock* tail = freelist_head;
int count = blocks_per_page;
while (tail->next) {
tail = tail->next;
}
freelist_push_batch_lockfree(class_idx, shard_idx, freelist_head, tail, count);
return 1;
}
```
#### 4. Remote Stack Drain (Lock-Free)
**Current**: `drain_remote_locked()` called under mutex
**New**: Drain into local list, then batch-push lock-free
```c
// Drain remote stack into freelist (lock-free)
static inline void drain_remote_lockfree(int class_idx, int shard_idx) {
// Atomically swap remote head to NULL (unchanged)
uintptr_t head = atomic_exchange_explicit(&g_pool.remote_head[class_idx][shard_idx],
(uintptr_t)0, memory_order_acq_rel);
if (!head) return;
// Count blocks
int count = 0;
PoolBlock* tail = (PoolBlock*)head;
while (tail->next) {
tail = tail->next;
count++;
}
count++; // Include head
// Batch push to freelist (lock-free)
freelist_push_batch_lockfree(class_idx, shard_idx, (PoolBlock*)head, tail, count);
// Update remote count
atomic_fetch_sub_explicit(&g_pool.remote_count[class_idx][shard_idx], count,
memory_order_relaxed);
}
```
#### 5. Fallback Strategy (Optional)
For **rare contention** cases (e.g., CAS spin > 100 iterations):
- Option A: Keep spinning (acceptable for short lists)
- Option B: Fallback to mutex (hybrid approach)
- Option C: Backoff + retry (exponential backoff)
**Recommendation**: Start with Option A (pure lock-free), measure, add backoff if needed.
---
### File Changes Required
| File | Function | Change Type | Est. LOC |
|------|----------|-------------|----------|
| `hakmem_pool.c` | Globals | Replace mutexes with atomics | +10/-10 |
| `hakmem_pool.c` | `freelist_pop_lockfree()` | **New function** | +30 |
| `hakmem_pool.c` | `freelist_push_lockfree()` | **New function** | +20 |
| `hakmem_pool.c` | `freelist_push_batch_lockfree()` | **New function** | +25 |
| `hakmem_pool.c` | `drain_remote_lockfree()` | Rewrite (lock-free) | +25/-20 |
| `hakmem_pool.c` | `refill_freelist()` | Modify (use batch push) | +10/-15 |
| `hakmem_pool.c` | `hak_pool_try_alloc()` | Replace lock/unlock with pop | +5/-10 |
| `hakmem_pool.c` | `hak_pool_free()` | Lock-free path | +10/-10 |
| `hakmem_pool.c` | `hak_pool_init()` | Init atomics (not mutexes) | +5/-5 |
| **Total** | | | **~140 LOC (net ~100)** |
---
### Testing Strategy
#### Correctness Test
```bash
# Single-threaded (no contention, pure correctness)
THREADS=1 ./test_pool_lockfree
# Multi-threaded stress test (high contention)
THREADS=16 DURATION=60 ./test_pool_lockfree_stress
# Check for:
# - No memory leaks (valgrind)
# - No double-free (AddressSanitizer)
# - No lost blocks (counter invariants)
```
#### Performance Test
```bash
# Baseline (Phase 6.25, with batching)
HAKMEM_POOL_REFILL_BATCH=2 RUNTIME=10 THREADS=4 ./scripts/run_bench_suite.sh
# Lock-free (Phase 6.26)
HAKMEM_POOL_REFILL_BATCH=2 RUNTIME=10 THREADS=4 ./scripts/run_bench_suite.sh
# Expected: +15-20% on Mid 4T (13.8 → 16-18 M/s)
```
#### Contention Analysis
```bash
# Measure CAS retry rate
# Add instrumentation:
# atomic_uint_fast64_t cas_retries;
# atomic_uint_fast64_t cas_attempts;
# Print ratio at shutdown
# Target: <5% retry rate under 4T load
```
---
### Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|------------|--------|------------|
| ABA problem (block reuse) | Low | Critical | Use epoch-based reclamation or hazard pointers |
| CAS livelock (high contention) | Medium | High | Add exponential backoff after N retries |
| Memory ordering bugs (subtle races) | Medium | Critical | Extensive testing, TSan, formal verification |
| Performance regression (1T) | Low | Low | Single-thread has no contention, minimal overhead |
**ABA Problem**:
- **Scenario**: Block A popped, freed, reallocated, pushed back while another thread's CAS is in-flight
- **Solution**: Not critical for freelist (ABA still results in valid freelist state)
- **Alternative**: Add version counter (128-bit CAS) if issues arise
**Rollback Plan**: Keep mutexes in code (ifdef'd out), revert via compile flag if needed.
---
### Estimated Time
- **Implementation**: 5-6 hours
- Lock-free primitives: 2 hours
- Integration: 2 hours
- Testing: 2 hours
- **Debugging**: 2-3 hours (race conditions, TSan)
- **Benchmarking**: 2 hours
- **Total**: **9-11 hours**
---
## 🧠 Phase 6.27: Learner Integration
### Goal
**Dynamic optimization of CAP and W_MAX based on runtime behavior**
**Target**: +5-10% across all workloads via adaptive tuning
### Problem Statement
Current policy is **static** (set at init):
- `CAP = {64,64,64,32,16,32,32}` (conservative)
- `W_MAX_MID = 1.60`, `W_MAX_LARGE = 1.30`
- No adaptation to workload characteristics
**Opportunity**: Use **existing learner infrastructure** to:
1. Collect size distribution stats
2. Adjust `mid_cap[]` dynamically based on hit rate
3. Adjust `w_max_mid` based on fragmentation vs hit rate trade-off
**Learner Already Exists**: `hakmem_learner.c` (~585 LOC)
- Background thread (1 sec polling)
- Hit rate monitoring
- UCB1 for W_MAX exploration (Canary deployment)
- Budget enforcement + Water-filling
**Integration Work**: Minimal (learner already supports Mid Pool tuning)
---
### Implementation Approach
#### 1. Enable Learner for Mid Pool
**Already Implemented** (`hakmem_learner.c:239-272`):
```c
// Adjust Mid caps by hit rate vs target (delta over window) with dwell
int mid_classes = 5;
if (cur->mid_dyn1_bytes != 0 && cur->mid_dyn2_bytes != 0) mid_classes = 7;
// ...
for (int i = 0; i < mid_classes; i++) {
uint64_t dh = mid_hits[i] - prev_mid_hits[i];
uint64_t dm = mid_misses[i] - prev_mid_misses[i];
// ...
if (hit < (tgt_mid - eps)) {
cap += step_mid; // Increase CAP
} else if (hit > (tgt_mid + eps)) {
cap -= step_mid; // Decrease CAP
}
// ...
}
```
**Action**: Just enable via env var!
```bash
HAKMEM_LEARN=1 \
HAKMEM_TARGET_HIT_MID=0.65 \
HAKMEM_CAP_STEP_MID=8 \
HAKMEM_CAP_MIN_MID=16 \
HAKMEM_CAP_MAX_MID=512 \
./your_app
```
#### 2. W_MAX Learning (Optional, Risky)
**Already Implemented** (`hakmem_learner.c:388-499`):
- UCB1 multi-armed bandit
- Canary deployment (safe exploration)
- Rollback if performance regresses
**Candidates** (for Mid Pool):
```
W_MAX_MID candidates: [1.40, 1.50, 1.60, 1.70]
Default: 1.60 (current)
Exploration: Try 1.50 (tighter, less waste) or 1.70 (looser, higher hit)
```
**Enable**:
```bash
HAKMEM_LEARN=1 \
HAKMEM_WMAX_LEARN=1 \
HAKMEM_WMAX_CANDIDATES_MID=1.4,1.5,1.6,1.7 \
HAKMEM_WMAX_CANARY=1 \
./your_app
```
**Recommendation**: Start with CAP tuning only, add W_MAX later (more risk).
#### 3. Size Distribution Integration (Already Exists)
**Histogram** (`hakmem_size_hist.c`):
- 1KB granularity bins (0-64KB tracked)
- Per-allocation sampling
- Reset after learner snapshot
**DYN1 Auto-Assignment** (already implemented):
```bash
HAKMEM_LEARN=1 \
HAKMEM_DYN1_AUTO=1 \
HAKMEM_CAP_MID_DYN1=64 \
./your_app
```
**Effect**: Automatically finds peak size in 2-32KB range, assigns DYN1 class.
#### 4. New: ACE Stats Integration
**Current ACE** (`hakmem_ace.c`):
- Records size decisions (original_size → rounded_size → pool)
- Tracks L1 fallback rate (miss → malloc)
- Not integrated with learner
**Proposal**: Add ACE stats to learner score function
**Modify Learner Score** (`hakmem_learner.c:414`):
```c
// OLD: simple hit-based score
double score = (double)(ace.mid_hit + ace.large_hit)
- (double)(ace.mid_miss + ace.large_miss)
- 2.0 * (double)ace.l1_fallback;
// NEW: add fragmentation penalty
extern uint64_t hak_ace_get_total_waste(void); // sum of (rounded - original)
uint64_t waste = hak_ace_get_total_waste();
double frag_penalty = (double)waste / 1e6; // normalize to MB
double score = (double)(ace.mid_hit + ace.large_hit)
- (double)(ace.mid_miss + ace.large_miss)
- 2.0 * (double)ace.l1_fallback
- 0.5 * frag_penalty; // penalize waste
```
**Benefit**: Balance hit rate vs fragmentation (W_MAX tuning).
---
### File Changes Required
| File | Function | Change Type | Est. LOC |
|------|----------|-------------|----------|
| `hakmem_learner.c` | Learner (already exists) | Enable via env | 0 |
| `hakmem_ace.c` | `hak_ace_get_total_waste()` | **New function** | +15 |
| `hakmem_learner.c` | `learner_main()` | Add frag penalty to score | +10 |
| `hakmem_policy.c` | (none) | Learner publishes dynamically | 0 |
| **Total** | | | **~25 LOC** |
---
### Testing Strategy
#### Baseline Test (Learner Off)
```bash
# Static policy (current)
RUNTIME=60 THREADS=1,4 ./scripts/run_bench_suite.sh
# Record: Mid 1T, Mid 4T throughput
```
#### Learner Test (CAP Tuning)
```bash
# Enable learner with aggressive targets
HAKMEM_LEARN=1 \
HAKMEM_TARGET_HIT_MID=0.75 \
HAKMEM_CAP_STEP_MID=8 \
HAKMEM_CAP_MAX_MID=512 \
HAKMEM_LEARN_WINDOW_MS=2000 \
RUNTIME=60 THREADS=1,4 ./scripts/run_bench_suite.sh
# Expected: CAP increases to ~128-256 (hit 75% target)
# Expected: +5-10% throughput improvement
```
#### W_MAX Learning Test (Optional)
```bash
HAKMEM_LEARN=1 \
HAKMEM_WMAX_LEARN=1 \
HAKMEM_WMAX_CANDIDATES_MID=1.4,1.5,1.6,1.7 \
HAKMEM_WMAX_CANARY=1 \
HAKMEM_WMAX_TRIAL_SEC=5 \
RUNTIME=120 THREADS=1,4 ./scripts/run_bench_suite.sh
# Monitor stderr for learner logs:
# "[Learner] W_MAX mid canary start: 1.50"
# "[Learner] W_MAX mid canary adopt" (success)
# or
# "[Learner] W_MAX mid canary revert to 1.60" (failure)
```
#### Regression Test
```bash
# Check learner doesn't hurt stable workloads
# Run with learning OFF, then ON, compare variance
# Target: <5% variance, no regressions
```
---
### Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|------------|--------|------------|
| Over-tuning (oscillation) | Medium | Medium | Increase dwell time (3→5 sec) |
| Under-tuning (no effect) | Medium | Low | Lower target hit rate (0.75→0.65) |
| W_MAX instability (fragmentation spike) | Medium | High | Use Canary, revert on regression |
| Low-traffic workload (insufficient samples) | High | Low | Set min_samples=256, skip learning if below |
**Rollback Plan**: Set `HAKMEM_LEARN=0` (default, no learner overhead).
---
### Estimated Time
- **Implementation**: 1-2 hours
- ACE waste tracking: 1 hour
- Learner score update: 30 min
- Testing: 30 min
- **Validation**: 3-4 hours
- Run suite with/without learner
- Analyze CAP convergence
- W_MAX exploration (if enabled)
- **Total**: **4-6 hours**
---
## 📊 Expected Performance Improvements
### Cumulative Gains (Stacked)
| Phase | Change | Mid 1T | Mid 4T | Rationale |
|-------|--------|--------|--------|-----------|
| **Baseline (6.21)** | Current | 4.0 M/s (28%) | 13.8 M/s (47%) | Post-quick-wins |
| **6.25 (Batch)** | Refill 2-4 pages | +10-15% | +5-8% | Amortize syscall, 1T bottleneck |
| | | **4.5-5.0 M/s** | **14.5-15.2 M/s** | |
| **6.26 (Lock-Free)** | CAS freelist | +2-5% | +15-20% | Eliminate 4T contention |
| | | **4.6-5.2 M/s** | **17.0-18.2 M/s** | |
| **6.27 (Learner)** | Dynamic CAP/W_MAX | +5-10% | +5-10% | Adaptive tuning |
| | | **5.0-5.7 M/s** | **18.0-20.0 M/s** | |
| **Target (60-75%)** | vs mimalloc 14.6M / 29.5M | **8.8-11.0 M/s** | **17.7-22.1 M/s** | |
| **Achieved?** | | ❌ **35-39%** | ✅ **61-68%** | 1T still short, 4T on target! |
### Gap Analysis
**1T Performance**:
- Current: 4.0 M/s (28% of mimalloc)
- Post-6.27: 5.0-5.7 M/s (35-39% of mimalloc)
- **Gap to 60%**: Still need **+5.3-6.0 M/s** (~+110-120%)
**Remaining Bottlenecks (1T)**:
1. Single-threaded inherently lock-bound (no TLS benefit)
2. mimalloc's per-thread heaps eliminate ALL shared state
3. Bump allocation (mimalloc) vs freelist (hakmem)
4. Header overhead (32 bytes per alloc in hakmem)
**4T Performance**:
- Current: 13.8 M/s (47% of mimalloc)
- Post-6.27: 18.0-20.0 M/s (61-68% of mimalloc)
- **✅ Target achieved!** (60-75% range)
---
### Follow-Up Phases (Post-6.27)
**Phase 6.28: Header Elimination** (if 1T still target)
- Remove AllocHeader for Mid Pool (use page descriptors only)
- Saves 32 bytes per allocation (~8-15% memory)
- Saves header write on alloc hot path (~30-50 cycles)
- **Estimated Gain**: +15-20% (1T)
**Phase 6.29: Bump Allocation** (major refactor)
- Replace freelist with bump allocator (mimalloc-style)
- Per-thread arenas, no shared state at all
- **Estimated Gain**: +50-100% (1T), brings to mimalloc parity
- **Risk**: High complexity, long implementation (~2-3 weeks)
---
## 🗓️ Priority-Ordered Task List
### Phase 6.25: Refill Batching (Target: Week 1)
1.**Implement `alloc_tls_page_batch()` function** (2 hours)
- [ ] Write batch mmap loop
- [ ] Distribute pages to TLS slots
- [ ] Fill Ring/LIFO from overflow pages
- [ ] Add page descriptors registration
2.**Integrate batch refill into `hak_pool_try_alloc()`** (1 hour)
- [ ] Replace `alloc_tls_page()` call with batch version
- [ ] Prepare slot array logic
- [ ] Handle partial allocation (< batch_size)
3.**Add environment variable support** (30 min)
- [ ] Add `g_pool_refill_batch_size` global
- [ ] Parse `HAKMEM_POOL_REFILL_BATCH` in init
- [ ] Validate range (1-4)
4.**Unit testing** (1 hour)
- [ ] Test batch=1,2,4 correctness
- [ ] Verify TLS slots filled
- [ ] Check Ring population
- [ ] Valgrind (no leaks)
5.**Benchmark validation** (2 hours)
- [ ] Run suite with batch=1 (baseline)
- [ ] Run suite with batch=2,4
- [ ] Analyze throughput delta
- [ ] **Target**: +10-15% Mid 1T
**Total Estimate**: 6-7 hours
---
### Phase 6.26: Lock-Free Refill (Target: Week 2)
6.**Replace mutex with atomic freelist** (2 hours)
- [ ] Change `PoolBlock* freelist[]``atomic_uintptr_t freelist_head[]`
- [ ] Add `atomic_uint freelist_count[]`
- [ ] Remove `PaddedMutex freelist_locks[]`
7.**Implement lock-free primitives** (2 hours)
- [ ] Write `freelist_pop_lockfree()`
- [ ] Write `freelist_push_lockfree()`
- [ ] Write `freelist_push_batch_lockfree()`
8.**Rewrite drain functions** (1 hour)
- [ ] `drain_remote_lockfree()` (no mutex)
- [ ] Count blocks in remote stack
- [ ] Batch push to freelist
9.**Integrate into alloc/free paths** (1 hour)
- [ ] Replace lock/pop/unlock with `freelist_pop_lockfree()`
- [ ] Update refill to use batch push
- [ ] Update free to use lock-free push
10.**Testing (critical for lock-free)** (3 hours)
- [ ] Single-thread correctness test
- [ ] Multi-thread stress test (16T, 60 sec)
- [ ] TSan (ThreadSanitizer) run
- [ ] Check counter invariants (no lost blocks)
11.**Benchmark validation** (2 hours)
- [ ] Run suite with lock-free (4T focus)
- [ ] Compare to Phase 6.25 baseline
- [ ] Measure CAS retry rate
- [ ] **Target**: +15-20% Mid 4T
**Total Estimate**: 11-12 hours
---
### Phase 6.27: Learner Integration (Target: Week 2, parallel)
12.**Add ACE waste tracking** (1 hour)
- [ ] Implement `hak_ace_get_total_waste()` in `hakmem_ace.c`
- [ ] Track cumulative (rounded - original) per allocation
- [ ] Atomic counter for thread safety
13.**Update learner score function** (30 min)
- [ ] Add fragmentation penalty term
- [ ] Weight: -0.5 × (waste_MB)
- [ ] Test score computation
14.**Validation testing** (3 hours)
- [ ] Baseline run (learner OFF)
- [ ] CAP tuning run (learner ON, W_MAX fixed)
- [ ] W_MAX learning run (Canary enabled)
- [ ] Compare throughput, check convergence
15.**Documentation** (1 hour)
- [ ] Update ENV_VARS.md with learner params
- [ ] Document recommended settings
- [ ] Add troubleshooting guide (oscillation, no effect)
**Total Estimate**: 5-6 hours
---
### Post-Implementation (Week 3)
16.**Comprehensive benchmarking** (4 hours)
- [ ] Full suite (tiny, mid, large) with all phases enabled
- [ ] Head-to-head vs mimalloc (1T, 4T, 8T)
- [ ] Memory profiling (RSS, fragmentation)
- [ ] Generate performance report
17.**Code review & cleanup** (2 hours)
- [ ] Remove debug printfs
- [ ] Add comments to complex sections
- [ ] Update copyright/phase headers
- [ ] Check for code duplication
18.**Documentation updates** (2 hours)
- [ ] Update INDEX.md with new phases
- [ ] Write PHASE_6.25_6.27_RESULTS.md
- [ ] Update README.md benchmarks section
**Total Estimate**: 8 hours
---
## 📈 Success Metrics
### Primary Metrics
| Metric | Current | Target | Measurement |
|--------|---------|--------|-------------|
| **Mid 1T Throughput** | 4.0 M/s | 5.0-5.7 M/s | Larson benchmark, 10s |
| **Mid 4T Throughput** | 13.8 M/s | 18.0-20.0 M/s | Larson benchmark, 10s |
| **Mid 1T vs mimalloc** | 28% | 35-39% | Ratio of throughputs |
| **Mid 4T vs mimalloc** | 47% | 61-68% | Ratio of throughputs |
### Secondary Metrics
| Metric | Current | Target | Measurement |
|--------|---------|--------|-------------|
| Refill frequency (1T) | ~1000/sec | ~250-500/sec | Counter delta |
| Lock contention (4T) | ~40% wait | <10% wait | Trylock success rate |
| Hit rate (Mid Pool) | ~60% | 70-80% | hits / (hits + misses) |
| Memory footprint | 22 MB | <30 MB | RSS baseline |
### Regression Thresholds
| Scenario | Threshold | Action |
|----------|-----------|--------|
| Tiny Pool 4T | <2% regression | Acceptable |
| Large Pool | <5% regression | Acceptable |
| Memory bloat | >40 MB baseline | Reduce CAP or batch |
| Crash/hang in stress test | Any occurrence | Block release, debug |
---
## 🎬 Conclusion
This implementation plan provides a **systematic path** to improve hakmem's Mid Pool performance from **47% to 61-68% of mimalloc** for multi-threaded workloads (4T), bringing it into the target range of 60-75%.
**Key Insights**:
1. **Phase 6.25 (Batching)**: Low risk, medium reward, tackles 1T bottleneck
2. **Phase 6.26 (Lock-Free)**: Medium risk, high reward, critical for 4T scaling
3. **Phase 6.27 (Learner)**: Low risk, low-medium reward, adaptive optimization
**Recommendation**:
- Implement 6.25 and 6.27 in **parallel** (independent, ~12 hours total)
- Tackle 6.26 **after** 6.25 validated (builds on batch refill, ~12 hours)
- **Total time**: ~24-30 hours (3-4 days focused work)
**Next Steps**:
1. Review this plan with team
2. Set up benchmarking pipeline (automated, reproducible)
3. Implement Phase 6.25 (highest priority)
4. Measure, iterate, document
**Open Questions**:
- Should we extend TLS slots from 2 to 4? (Test in 6.25)
- Is W_MAX learning worth the risk? (Test in 6.27 with Canary)
- After 6.27, pursue header elimination (Phase 6.28) or accept 1T gap?
---
**Document Version**: 1.0
**Last Updated**: 2025-10-24
**Author**: Claude (Sonnet 4.5)
**Status**: Ready for Implementation