# Changelog
All notable changes to PulseMap will be documented in this file.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
---
## [v0.6.3] — 2026-08-17
### 📚 Documentation & Crates.io Links Fix
- **Updated Crates.io Documentation URL:** Pointed `documentation` field in `Cargo.toml` directly to the hosted mdBook documentation site (`https://ddsha441981.github.io/pulse_map/`).
- **Added Docs.rs Metadata:** Added `[package.metadata.docs.rs]` configuration in `Cargo.toml` with `all-features = true` and `rustdoc-args = ["--cfg", "docsrs"]` so docs.rs builds with complete feature flags.
---
## [v0.6.2] — 2026-08-11
### 🚀 Lock-Free Reads + Data Race Fixes + Latency Reductions
Major stability and performance release: fixed UB, eliminated lock contention on reads, and reduced GET latency by over 60%.
### Added
**Atomic MetaWord + Access Buffer (`src/engine/*`, `src/raw.rs`, `src/sync.rs`) — PR-8**
- `MetaWord(u64)` → `MetaWord(AtomicU64)` — all reads use `Relaxed` atomic loads
- `on_access()` uses CAS loop instead of exclusive mutation
- NEW: `AccessBuffer` — lock-free lossy ring buffer for deferred eviction tracking
- `get()` pushes access events to buffer instead of mutating MetaWord inline
- Removed unsafe raw pointer cast from `get()` in `raw.rs`
- `Bucket` no longer derives `Copy` (AtomicU64 is !Copy)
- Result: **66% improvement in GET p99 latency vs v0.6.1 baseline**
### Changed
**Upgrade TTL Epoch Types u32 → u64 (`src/raw.rs`, `src/sync.rs`, `src/sharded.rs`, `src/lib.rs`) — PR-4**
- `current_epoch`: `AtomicU32` → `AtomicU64`
- `default_ttl`: `AtomicU32` → `AtomicU64`
- `SlotTTL.epoch`: `u32` → `u64`
- All public TTL API signatures updated: `set_ttl(u64)`, `get_ttl() -> u64`, `current_epoch() -> u64`, `insert_ttl(..., ttl: u64)`
- Eliminates epoch wrap-around after 4.29B inserts
- **BREAKING CHANGE**: TTL parameter types changed from `u32` to `u64`
**Lazy Slab Lock in `get()` (`src/sync.rs`) — PR-7**
- Inline keys (mode=0, key ≤ 6 bytes) now skip the `slab_pool.lock()` mutex entirely during reads
- Slab-mode keys check 46-bit fingerprint BEFORE acquiring the lock
- Result: **60% improvement in GET p99 latency**
### Fixed
**Fix UB & Data Race in `raw.rs` (`src/raw.rs`) — PR-1**
- Removed `unsafe impl Sync for PulseMapRaw` — `PulseMapRaw` is now `Send` but NOT `Sync`
- Users must use `ConcurrentPulseMap` or `ShardedPulseMap` for multi-threaded access
**Fix Data Loss & TTL Wipe During `resize` (`src/sync.rs`) — PR-2**
- Fixed silent data loss when bucket overflows during rehash (added overflow retry loop that doubles capacity)
- Fixed TTL wipe: epochs/TTL metadata is now properly migrated during resize
**Fix Fingerprint Entropy Collapse in `ShardedPulseMap` (`src/sharded.rs`) — PR-3**
- Shard routing changed from `h1 >> 60` (bits 60-63) to `h1 as usize & mask` (low bits)
- This eliminated overlap with h2 fingerprint bits (57-63), restoring full 7-bit (128 values) fingerprint entropy within each shard
**SIMD Dispatch Fix (`src/engine/meta.rs`) — PR-5, PR-6**
- PR #5 removed SIMD dispatch based on agent analysis (WRONG — caused 20% throughput regression)
- PR #6 immediately restored SIMD dispatch — benchmarks proved SSE2 path IS faster in release builds
- Lesson learned: always benchmark before removing optimizations
### Benchmarks (v0.6.1 → v0.6.2)
| GET p99 (Mixed Workload) | 1.244 µs | 964 ns | 22.5% faster |
| Throughput (5M inserts) | 5.99M ops/s | 7.47M ops/s | 24.6% faster |
| Contention p99 (Hot Keys) | 1.277 µs | 1.134 µs | 11.2% faster |
| Memory per entry | 34.0 B | 34.0 B | Zero overhead |
### Testing
- 58 unit tests + 11 doc-tests passing
- All `cargo clippy`, `cargo fmt --check`, `cargo test` passed for every PR
---
## [v0.6.1] — 2026-08-03
### 🚀 Sharded Concurrency + Per-Entry TTL + Real Competitor Benchmarks
Major release: 16-shard concurrent map (2.4-3.1x faster), per-entry TTL, and honest benchmarks against moka + quick_cache.
### Added
**ShardedPulseMap (`src/sharded.rs`) — PR-3**
- `ShardedPulseMap<K,V>` — 16 independent `ConcurrentPulseMap` shards
- Shard selection: `h1 >> 60` (top 4 bits, independent from bucket selection)
- `insert()`, `get()`, `peek()`, `remove()`, `contains_key()` — routed to shard by hash
- `resize_all(n)` — per-shard rehash, no stop-the-world pause
- TTL propagation: `set_ttl()` applied to all shards, `current_epoch()` = max
- `len()`, `capacity()`, `load_factor()`, `eviction_count()` — aggregated stats
**Per-Entry TTL (`raw.rs`, `lib.rs`, `sync.rs`, `sharded.rs`) — PR-4**
- `insert_ttl(key, value, ttl)` on all map types (PulseMap, TypedPulseMap, ConcurrentPulseMap, ShardedPulseMap)
- `ttl = 0`: use global default (`set_ttl()`), `u32::MAX`: never expire, `N`: expire after N inserts
- `SlotTTL { epoch, ttl }` replaces `Vec<u32>` epochs (8 bytes/slot, was 4)
- Re-inserting refreshes both epoch and per-entry TTL
- Backward compatible: `set_ttl()`, `get_ttl()`, `insert()` behavior unchanged
**Zero-Copy Key Borrow (`lib.rs`, `sync.rs`) — PR-2**
- `PulseKey::key_bytes()` — borrow key bytes without allocation on read path
- Numeric types return stack-allocated `[u8; N]` via `with_key_bytes()`
- String lookup improved by -4.8%
**Real Competitor Benchmarks — PR-5**
- moka + quick_cache benchmarks (single-thread + 4-thread)
- Honest README benchmark table (losses documented alongside wins)
### Changed
- `raw.rs`: `epochs: Vec<u32>` → `slots_ttl: Vec<SlotTTL>`, `ttl_epochs` → `default_ttl`
- `raw.rs`: `insert()` refactored to `insert_internal(key, value, ttl)`
- `sync.rs`: epoch storage updated to `Vec<SlotTTL>`, `ttl_epochs` → `default_ttl`
- `is_expired()` now checks per-entry TTL with fallback to default
- `find_free_or_expired()` no longer requires global TTL to be set
### Benchmarks (v0.6.1)
**Single-Thread (100K ops)**
| INSERT | **6.1 ms** | 19.1 ms | 5.6 ms | 161 ms |
| LOOKUP | 5.4 ms | 5.4 ms | **2.8 ms** | 40 ms |
| EVICTION (50K) | **1.9 ms** 🥇 | 2.3 ms | 3.3 ms | 55.5 ms |
**Multi-Thread — 4 Threads, 100K ops**
| 4T INSERT | **8.8 ms** 🥇 | 20.2 ms | 104 ms |
| 4T LOOKUP | **9.0 ms** 🥇 | 35.0 ms | 21.1 ms |
| 4T MIXED | **15.9 ms** 🥇 | 46.6 ms | 197 ms |
### Testing
- **58 tests passing** (up from 57)
- 5 new ShardedPulseMap tests (basic, 4-thread, resize_all, TTL, len-sum)
- 6 new per-entry TTL tests (different expiries, never-expire, overrides-global, typed, concurrent, refresh)
### Rejected
- **PR-1 AHash**: A/B benchmark showed AHash 12.8% SLOWER than wyhash. wyhash retained.
## [v0.1.0] — 2026-05-22
### 🎉 Initial Release — Core Engine
The foundation of PulseMap: a 64-byte cache-line hash table with built-in eviction.
### Added
**Core Engine (`src/core/`)**
- `MetaWord` — 64-bit packed metadata storing state (2b), H2 fingerprint (7b), and priority (7b) for 4 slots
- `Slot` — 14-byte entry with two modes:
- Inline mode: keys ≤6 bytes + values ≤7 bytes stored directly in cache line
- Slab mode: 46-bit fingerprint + pointer to heap-allocated entry
- `Bucket` — 64-byte `#[repr(C, align(64))]` struct = exactly 1 CPU cache line (compile-time verified)
- `SlabPool` — Arena-based allocator for variable-length key+value entries
- `hash` — wyhash splitting into H1 (bucket index), H2 (7-bit fingerprint), ext_fp (46-bit slab fingerprint)
**PulseMap API (`src/lib.rs`)**
- `PulseMap::new(num_buckets)` — fixed-capacity construction
- `insert(&mut self, key, value)` — insert with automatic eviction on full buckets
- `get(&self, key)` — immutable lookup with interior priority update
- `peek(&self, key)` — lookup without priority update
- `remove(&mut self, key)` — key deletion
- `len()`, `capacity()`, `load_factor()`, `eviction_count()` — stats
**Eviction Policy**
- Hybrid LFU+LRU: 4-bit frequency + 3-bit recency = 7-bit priority per slot
- `on_access()`: boost frequency, set recency to max, decay other slots
- `on_insert()`: cold start (freq=0, recency=1)
- `find_evict_target()`: lowest priority slot evicted
- **Zero extra cache misses** — all priority data in MetaWord (already fetched)
**Optimizations**
- `match_mask()` — bitmask-based H2 scan (compiler-friendly unrolled)
- `get(&self)` not `get(&mut self)` — allows shared references
- `Send + Sync` implemented for thread-safe reads
**Testing**
- 16 tests passing (15 unit + 1 doc test)
- Bucket size compile-time assertion (must be 64 bytes)
**Benchmarks (vs std::HashMap / Swiss Table)**
- INSERT: 3.4x faster (22.7ms vs 78.0ms for 100K ops)
- MIXED: 2.5x faster (37.3ms vs 91.8ms)
- EVICTION: 2.5ms for 50K ops (std::HashMap: impossible)
- Cache misses: 47% fewer (perf stat verified)
### Known Limitations
- `&[u8]` keys only (no generic types yet)
- No iterator support
- No dynamic resizing
- Lookup 1.4x slower than std::HashMap (no SIMD yet)
- Single-threaded only (Send+Sync but no internal locking)
---
## [v0.2.0] — 2026-05-22
### 🚀 Generic Types + Iterator + Traits
Layered architecture: `core/` → `raw.rs` → `lib.rs`. Users get typed API, power users get raw bytes.
### Added
**Architecture Refactor**
- `raw.rs` — `PulseMapRaw` (v0.1.0 PulseMap renamed) — raw `&[u8]` engine
- `PulseMap` is now a type alias for `PulseMapRaw` (backward compatible)
- `TypedPulseMap<K, V>` — generic wrapper over PulseMapRaw
**Traits (`PulseKey` / `PulseValue`)**
- `PulseKey` trait with `to_bytes()` + `from_bytes()` for key serialization
- `PulseValue` trait with `to_bytes()` + `from_bytes()` for value serialization
- Built-in impls: `u8`, `u16`, `u32`, `u64`, `i32`, `i64`, `String`, `Vec<u8>`, `[u8; N]`, `bool`
**TypedPulseMap<K, V> API**
- `insert(K, V)`, `get(&K)→Option<V>`, `peek(&K)→Option<V>`
- `remove(&K)→bool`, `contains_key(&K)→bool`
- `iter()→TypedIter<K,V>` — typed iteration over all entries
**Iterator Support (`src/iter.rs`)**
- `RawIter` — iterates `(&[u8], &[u8])` raw pairs
- `TypedIter<K, V>` — iterates `(K, V)` with auto-deserialization
**Std Traits**
- `Debug` — shows len, capacity, load%, evictions
- `Display` — human-readable `PulseMap(n/cap entries, x% load, y evictions)`
- `Extend<(K, V)>` — bulk insertion from any iterator
- `From<HashMap<K, V>>` — convert std::HashMap to TypedPulseMap (auto-calculates bucket count)
**Zero-Alloc Serialization**
- `PulseKey`/`PulseValue` traits now use associated type `Bytes`
- Numeric types (`u32`, `u64`, etc.) return `[u8; N]` on stack — **zero heap allocation**
- `String`/`Vec<u8>` still use `Vec<u8>` (unavoidable)
### Design Decision: `Index<&K>` NOT Implemented
`map[&key]` syntax requires returning `&V` (a reference to the value). PulseMap stores values
as raw bytes and deserializes them on read — it returns `V` (an owned copy), not `&V`.
Implementing `Index` would require either:
1. Panicking (unsafe, bad UX) — rejected
2. Caching deserialized values (extra memory, defeats purpose) — rejected
3. Leaking memory (unsafe) — rejected
**Use `map.get(&key)` instead.** Returns `Option<V>`.
**Testing**
- 29 tests passing (25 unit + 4 doc tests)
### Benchmarks (v0.2.0) — Fair Comparison
**PulseMap vs `lru` crate (SAME CATEGORY — bounded cache with eviction)**
| **INSERT** | **36.3 ms** | 79.3 ms | ✅ **2.2x faster** |
| **MIXED** | **63.0 ms** | 87.6 ms | ✅ **1.4x faster** |
| **EVICTION (50K)** | **4.6 ms** | 4.8 ms | ✅ **~same** |
| LOOKUP | 34.2 ms | **15.1 ms** | ❌ lru 2.3x faster |
**PulseMap vs std::HashMap (DIFFERENT CATEGORY — reference only)**
| INSERT | 36.3 ms | 7.4 ms | std has no eviction |
| LOOKUP | 34.2 ms | 10.8 ms | std uses SIMD |
| MIXED | 63.0 ms | 19.5 ms | std uses native types |
---
## [v0.3.0] — 2026-05-26
### ⚡ Performance + SIMD + Entry API + no_std
**2x overall speedup.** Power-of-2 buckets, branchless H2 matching, SIMD support, and prefetch hints.
### Added
**Power-of-2 Bucket Count (`raw.rs`)**
- `num_buckets` auto-rounded to next power of 2
- `% num_buckets` → `& bucket_mask` — modulo replaced with bitwise AND
- Applied across all 4 hot paths: `insert()`, `get()`, `peek()`, `remove()`
**SIMD H2 Matching (`simd.rs`)**
- SSE2 `_mm_cmpeq_epi8` + `_mm_movemask_epi8` for parallel H2 comparison
- Behind `--features simd` flag (x86_64 only)
- Default scalar path uses branchless bit arithmetic (`meta.rs`)
**Prefetch Hints (`raw.rs`)**
- `_mm_prefetch` in `get()` — preloads bucket into L1 cache before access
**Entry API (`lib.rs`)**
- `map.entry(key).or_insert(value)` — insert if vacant
- `map.entry(key).or_insert_with(|| compute())` — lazy insert
- `map.entry(key).and_modify(|v| *v += 1).or_insert(0)` — modify-or-insert
- `OccupiedEntry`: `get()`, `key()`, `insert()`, `remove()`
- `VacantEntry`: `key()`, `insert()`
**`#![no_std]` Support**
- `default = ["std"]` — backward compatible
- `default-features = false` enables `no_std` with `alloc`
- `From<HashMap>` gated behind `#[cfg(feature = "std")]`
**Testing**
- 35 tests passing (30 unit + 5 doc tests)
### Benchmarks (v0.3.0)
**v0.2.0 → v0.3.0 Speedup**
| INSERT | 36 ms | **15 ms** | **2.4x faster** |
| LOOKUP | 34 ms | **18 ms** | **1.9x faster** |
| MIXED | 63 ms | **32 ms** | **2.0x faster** |
| EVICTION | 4.6 ms | **1.8 ms** | **2.6x faster** |
**PulseMap vs `lru` crate (same category)**
| **INSERT** | **15 ms** | 32 ms | ✅ **2.1x faster** |
| **MIXED** | **32 ms** | 44 ms | ✅ **1.4x faster** |
| **EVICTION** | **1.8 ms** | 3.2 ms | ✅ **1.8x faster** |
| LOOKUP | 18 ms | **8.3 ms** | ❌ lru 2.2x faster |
---
## [v0.4.0] — 2026-05-26
### 🔒 Thread Safety + Dynamic Resize
**ConcurrentPulseMap** — thread-safe with per-bucket spinlocks. Only 7% overhead vs single-threaded.
### Added
**ConcurrentPulseMap (`sync.rs`)**
- `ConcurrentPulseMap::<K, V>::new(n)` — fixed-size concurrent map
- `ConcurrentPulseMap::with_auto_resize(n)` — auto-grows at 75% load
- All methods take `&self` (not `&mut self`) — safe via `Arc`
- `insert()`, `get()`, `peek()`, `remove()`, `contains_key()`
- `len()`, `capacity()`, `load_factor()`, `eviction_count()`, `num_buckets()`
- `Debug` and `Display` trait implementations
**Per-Bucket Spinlock Architecture**
- `BucketLocks` — `Vec<AtomicU8>` (1 lock per bucket)
- `BucketGuard` — RAII guard (auto-unlock on drop)
- `compare_exchange_weak` + `spin_loop()` for low-latency locking
- Different buckets accessed fully in parallel
**Dynamic Resize**
- `map.resize(new_size)` — manual stop-the-world rehash
- `with_auto_resize(n)` — auto-doubles at 75% load factor
- `RwLock<MapInner>` — read lock for ops, write lock for resize
**Slot Helpers (`slot.rs`)**
- `get_key_bytes()` — extract key (inline or slab) for rehashing
- `get_value_bytes()` — extract value (inline or slab) for rehashing
**Testing**
- 46 tests passing (38 unit + 8 doc tests)
- Multi-threaded insert test (4 threads × 1000 entries)
- Concurrent read/write test
- Manual resize + auto-resize tests
### Benchmarks (v0.4.0)
**Concurrency Overhead**
| INSERT | 13.8 ms | 14.8 ms | **7%** |
**4-Thread Concurrent**
| 4T INSERT | **20.8 ms** |
| 4T LOOKUP | **15.2 ms** |
| 4T MIXED | **35.6 ms** |
**PulseMap vs `lru` crate (final score)**
| **INSERT** | **13.8 ms** | 19.1 ms | ✅ **1.4x faster** |
| **MIXED** | **17.9 ms** | 23.7 ms | ✅ **1.3x faster** |
| **EVICTION** | **1.5 ms** | 2.2 ms | ✅ **1.5x faster** |
| LOOKUP | 9.8 ms | **5.4 ms** | ❌ lru 1.8x faster |
---
## [v0.5.0] — 2026-05-26
### 🌐 FFI Bindings — Use PulseMap from Any Language
**Workspace architecture** — all bindings live in separate crates under one workspace.
### Added
**Workspace (`Cargo.toml`)**
- Rust workspace with 5 members: `pulse_map`, `pulse_map_ffi`, `pulse_map_py`, `pulse_map_java`, `pulse_map_node`
**Phase 1: C FFI (`pulse_map_ffi/`)**
- `libpulse_map_ffi.so` + `libpulse_map_ffi.a` (418K release)
- `include/pulse_map.h` — clean C header with opaque `PulseMapHandle*`
- 12 extern "C" functions: `new`, `new_auto_resize`, `free`, `insert`, `get`, `contains`, `remove`, `len`, `capacity`, `load_factor`, `eviction_count`, `resize`
- NULL-safe, buffer overflow protection (`-2` return code)
- 11 C tests passing
**Phase 2: Python (`pulse_map_py/`)**
- PyO3 bindings via `maturin`
- Dict-like API: `cache["key"] = "value"`, `cache["key"]`, `del cache["key"]`, `"key" in cache`
- Bytes API: `cache.insert(b"k", b"v")`, `cache.get(b"k")`
- Properties: `len()`, `capacity`, `load_factor`, `eviction_count`
- `repr()`: `PulseMap(len=1, capacity=256, load=0.4%)`
- 11 Python tests passing
**Phase 3: Java (`pulse_map_java/`)**
- Java 22+ Panama FFM API (no JNI!)
- Rust cdylib → `libpulse_map_java.so` → Java `Linker.downcallHandle()`
- `PulseMap` class: `put()`, `get()`, `remove()`, `size()`, `capacity()`
- `AutoCloseable` — `try (var cache = new PulseMap(1024)) { ... }`
- Unicode support (UTF-8 round-trip)
- 10 Java tests passing
**Phase 4: Node.js (`pulse_map_node/`)**
- napi-rs bindings → `pulse-map.node` (604K)
- String API: `cache.set()`, `cache.get()`, `cache.delete()`, `cache.has()`
- Bytes API: `cache.insertBytes()`, `cache.getBytes()`
- Getters: `size`, `capacity`, `loadFactor`, `evictionCount`
- 10 Node.js tests passing
### Testing
| C FFI | **11/11** |
| Python | **11/11** |
| Java | **10/10** |
| Node.js | **10/10** |
| **Total** | **42/42** |
---
## [v0.6.0] — 2026-06-15
### ⚡ Performance + Memory + TTL
Algorithmic fixes, memory correctness, and a new TTL feature.
### Added
**TTL via Epoch Counter (`raw.rs`)**
- `set_ttl(n: u32)` — entries expire after `n` insertions (0 = disabled)
- `get_ttl() → u32` — query current TTL setting
- `current_epoch() → u32` — total insertions so far
- Zero overhead when TTL is disabled (`ttl_epochs == 0` → single compare, skipped)
- Re-inserting a key refreshes its epoch (extends lifetime)
- Expired slots lazily reclaimed on next insert — no background thread needed
- Available on both `PulseMap` (raw) and `TypedPulseMap<K,V>`
```rust
let mut cache = PulseMap::new(1024);
cache.set_ttl(500); // entries expire after 500 insertions
cache.insert(b"session", b"abc123");
// ...500 inserts later...
assert_eq!(cache.get(b"session"), None); // expired ✓
```
**Slab Free List (`engine/slab.rs`)**
- `SlabPool` now uses `Vec<Option<Box<SlabEntry>>>` + `free_list: Vec<usize>`
- Evicted slab entries returned to free list via `free(idx)` — reused on next alloc
- `SlabEntry::rewrite()` — in-place key/value rewrite (realloc only if new data is larger)
- **Fixes memory leak**: previously, evicted slab entries were abandoned until map dropped
- High-churn workloads (e.g., DNS cache, session store) now have stable memory
**Slot Layout Change (pointer → index)**
- Slab slots now store `usize` index into `SlabPool` (bytes 6–13)
- Previously stored raw `*const SlabEntry` pointer
- Enables free list: `raw.rs` calls `slab_pool.free(slot.slab_idx())` on eviction
- `slab_idx()` method replaces old `slab_ptr()`
### Changed
**`peek()` + `remove()` now use `match_mask()` (`raw.rs`, `sync.rs`)**
- Previously used brute-force per-slot loop: 8 individual `get_state()` + `get_h2()` calls
- Now identical to `get()`: single branchless `match_mask(h2)` bit operation
- Consistent hot path across all 3 lookup functions
**`SlotState::Deleted` removed (`lib.rs`)**
- Variant was never written — only `Tombstone` is set by `remove()`
- `Deleted = 2, Tombstone = 3` → `Tombstone = 2` (simpler encoding)
- `find_free_slot()` simplified from 3-way OR to single `!= Full` check
- `from_bits()` updated accordingly
### Fixed
- **Memory leak on eviction**: slab entries now returned to free list instead of abandoned
- **Slab memory on `remove()`**: `slab_pool.free(idx)` called on explicit key removal
- **Slab memory on update**: old slab entry freed before allocating new one
### Testing
- **57 tests passing** (up from 50)
- 4 new slab free list tests (reuse, larger rewrite, bulk reuse)
- 5 new TTL tests (basic expiry, update refresh, typed map, zero disables, epoch counter)
### Benchmarks (v0.6.0) — Actual Measured Results
> Run: `cargo bench -- lookup` on same machine. Numbers vary per run.
| raw_lookup | ~7.2 ms | **8.38 ms** | No algorithmic change |
| typed_lookup | ~7.5 ms | **8.71 ms** | No algorithmic change |
| raw_mixed | 17.46 ms | not re-measured | Minor improvement from match_mask in remove() |
| lru_lookup | 3.40 ms | **3.17 ms** | Reference — not our code |
> **Correction from earlier estimate:** The "-8% mixed improvement" claim was based on
> one run and not reliably reproducible. v0.6.0 is a **correctness + memory release**,
> not a performance release. The lookup gap vs `lru` is unchanged.
### Known Remaining Gap
```
Measured (100K ops):
PulseMap typed lookup : 8.71 ms
lru lookup : 3.17 ms
Gap : 2.7x ← UNCHANGED from v0.5.0
Root cause (profiled):
from_bytes deserialization → only ~6% of lookup time (NOT the bottleneck)
Actual bottlenecks:
wyhash compute_hash() → ~35-40% of lookup
to_bytes() on every get → ~10% (key serialized even for read)
cache misses on bucket → ~35-40%
→ v0.7.0 will target wyhash replacement (AHash) and zero-copy key borrow.
TypedSlabPool approach was investigated and rejected — low ROI for numeric types.
```