Repository navigation
perf: lazily initialize ScanHashMap resize targets - #363
thweetkomputer wants to merge 1 commit into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
Walkthrough
ChangesLazy ScanHashMap bucket initialization
Priority: ⬇️ Low Estimated code review effort: 4 (Complex) | ~60 minutes Change: Refactor Sequence Diagram(s)sequenceDiagram
participant ScanHashMap
participant Table
participant OverflowBuckets
ScanHashMap->>Table: Allocate resize target with deferred initialization
Table->>OverflowBuckets: Record uninitialized blocks
ScanHashMap->>Table: ReadBucket for an uninitialized block
Table-->>ScanHashMap: Return null
ScanHashMap->>Table: WriteBucket for a target block
Table->>OverflowBuckets: Mark the constructed block initialized
ScanHashMap->>Table: Finish bucket initialization after rehash
Table->>OverflowBuckets: Verify all blocks and clear lazy state
Merge Risk: ⚪ Minimal · up to
|
Large
ScanHashMapresizes currently initialize the entire destination bucket array synchronously before starting incremental rehash. The triggering mutation therefore pays an initialization cost proportional to the new table size.Initialize resize targets larger than 64 KiB in 64 KiB blocks on first write. A bitmap makes untouched blocks read as empty across lookup, address validation, admission estimates, scans, deletion, and cleanup. Early inserts and migration share the initialization state, so migration cannot erase an early write. Migration prepares destinations even for empty source buckets; all buckets are constructed and the bitmap is discarded before the target becomes primary and concurrent reader admission reopens.
Small tables and recovery preallocation remain eager. Primary-table
Findand candidate lookup compile out initialization checks, and lookup wrappers explicitly retain inlining. Resize metadata shares the optional overflow allocation, and the lazy flag fits in existing table padding (sizeof(ScanHashMap<uint64_t>)remains 128 bytes in the measured build). Full bucket-array allocation/accounting and final release remain synchronous. This bounds explicit initialization per block, not allocator or OS latency; total initialization work moves into writes and migration steps.Validation:
LAVIK_ENABLE_WERROR=ON.lavik_unit_testscases passed using CTest process isolation.ScanHashMapTestcases passed in a standalone UBSan build (-fsanitize=undefined -fno-sanitize-recover=all).git diff --checkpassed.ReceiveModes/TcpStreamCloseTest.PendingReceiveDoesNotKeepPeerConnected/0receivesEINTRfromrecv. Its two parameterizations passed 10 focused repetitions, and both passed under CTest. No end-to-end Valkey compatibility or production throughput benchmark was run.Local microbenchmark against
2d8a293dcon Linux ARM64, GCC 13.3,-O3 -DNDEBUG -march=native, pinned to CPU 7. Each map starts with nine entries per source bucket. Time the next insertion that starts growth, each subsequentMaintain()call, then two million successful const lookups after migration. Each cell below is the median of seven rounds; the step column is the median of each round's p99.9. These are synthetic local measurements, not an end-to-end latency claim.For the 32 MiB target, the slowest growth-triggering insertion across those seven rounds changed from 6,018.265 to 20.875 μs. The slowest individual migration step changed from 37.708 to 63.625 μs, reflecting the deferred first-touch work and system noise. The largest-table settled lookup was about 7% slower in this sample; the other sizes were similar or faster, and primary lookup performs no bitmap access.
Standalone benchmark source and run settings
Compile this source with
src/memory.cppandsrc/storage/format.cpp, the repository's mimalloc and Abseil static libraries, and the include directories for Lavik, mimalloc, and Abseil. Use C++23 and the compiler flags above. The baseline uses the header at2d8a293dc; the changed executable uses this PR's header, with other source and library inputs identical. Run each executable withtaskset -c 7 ./bench <exponent> 7for exponents10,11,15, and18(source bucket counts are powers of two). Both command arguments are required.Summary by CodeRabbit