KAME

Large-allocation recycle cache — §25 global log-slot

Covers the recycle layer in front of the two large tiers:

The recycle cache holds freed-but-warm blocks so a reuse skips the cold cost (chunk: claim + madvise; mmap: mmap + first-touch zero) and, above all, the re-fault / re-zero of the pages.

History

phase cache issue it hit
§21/§22 per-thread LIFO, 8 entries / 64 MiB working-set cliff: a rotating set > 8 blocks misses every cycle
§23 same LIFO, moved to IE-TLS fixed __tls_get_addr (35 % CPU on a 64 KiB hot loop) but kept the 8-entry cliff
§25 global log-slot no TLS, no cliff, lock-free, byte-capped — but every op is an atomic CAS on a SHARED slot ⇒ MT contention on a narrow band (x86 measured 64 KiB mt:4 ≈ 10 M/s)
§26 per-thread L1 log + global L2 (this doc) L1 absorbs same-thread ping-pong with NO atomics; spills to / refills from the §25 global L2. Restores MT scaling (64 KiB mt:4 ≈ 458 M/s) AND single-thread (64 KiB hot 52→121 M/s) without losing §25’s no-cliff property

§26 — per-thread L1 in front of the global log-slot

The §25 global cache is scalable and cliff-free, but every op is an atomic CAS on a shared slot. When many threads ping-pong a narrow size band (e.g. all freeing/allocating 64 KiB → all hit the same low slots) the CAS serialises. §26 adds a per-thread L1 in front:

TLS model (the §23 lesson): the L1 array is plain __thread (572 pointers, too big for the initial-exec surplus), but its per-thread base address is taken ONCE and cached in an IE-TLS pointer (tls_l1), so the hot path is one fs:offset read + index, never __tls_get_addr. Single-owner ⇒ the L1 slots are plain loads/stores, no CAS.

Measured (x86 Xeon 2.1 GHz, M ops/s):

pattern §25 §26
hot 64 KiB 52 121
hot 1 MiB 47 91
fifo:64 64 KiB 48 122
mt:4 64 KiB 10 458
mt:4 1 MiB 11 238
hot ≥ 8 MiB ~46 ~46 (mmap tier — L1 cut, L2 only, unchanged)

Structure

One atomic pointer per log2-spaced size index over [ALLOC_MIN_CHUNK_SIZE (256 KiB), ALLOC_MIN_MMAP_SIZE (32 MiB)]:

g_lrc_slot[LRC_N + 1]      atomic<char*>   one cached block (base) per index, or null
g_lrc_bytes                atomic<int64>   total cached bytes (sloppy)
g_lrc_cap                  atomic<int64>   byte cap, default ~1 GiB (tunable, dynamic)
idx(S) = N·log2(S/LO)/log2(HI/LO)          integer (CLZ + 8-bit mantissa interp; no libm)

Properties

Why this shape (measured)

A touch-included benchmark (see proto + git history) showed the large-reuse regime is fault/re-zero-dominated, not syscall-dominated: mmap+munmap is ~380 ns flat, but a cold first-touch zero is ~50 ns/KiB (~20 GB/s). Warm-resident reuse avoids the re-zero (~100× faster page-touch). Hence:

Hot micro-bench (no touch, after the integer-log2 fix): 64 KiB 185, 1 MiB 164, 8 MiB 135, 16 MiB 136 M ops/s — matches/beats the §22/§23 LIFO and is flat across the range (no cliff).

Prototype

tests/large_recycle_proto.cpp is a standalone validator (raw-mmap backing, not built by CMake). It carries the unit tests (test_basic / test_cap / test_mt / test_livelock) and a touch bench. Build & run:

clang++ -O3 -DNDEBUG -std=gnu++17 -pthread tests/large_recycle_proto.cpp -o /tmp/lrproto
/tmp/lrproto test          # correctness + livelock
/tmp/lrproto bench 1048576 200000 fifo:16 page

Tunables / drift guards