perf: replace Map<number,number> elapsedDays cache with Int32Array#18
Merged
Conversation
The previous Map-based cache provided real benefit (~2.7x over uncached calls on a hot, narrow workload), but Map.get/set carry hashing and boxing overhead that grow with cache size. Hebrew years are dense small integers ideal for direct typed-array indexing. Switch to an Int32Array(10000) indexed by year, using 0 as the "not computed" sentinel (every valid result is >= 1). A Map fallback covers years outside the pre-allocated range so behavior is unchanged for any input. Benchmarks against the built dist (bench/elapsedDaysReal.bench.mjs), median of 7 runs: elapsedDays 31 yrs x 1000 iter 47.5ms -> 17.4ms (2.7x) elapsedDays 4001 yrs x 1 pass 110.4ms -> 22.0ms (5.0x) hebrew2abs 111 yrs x 13 mo x 2 41.0ms -> 28.8ms (1.4x) abs2hebrew full calendar year 288.1ms -> 228.5ms (1.2x) Memory cost is ~40 KB pre-allocated, vs a Map that grew per call. Tests unchanged (all 105 passing).
The previous Int32Array(10000) covered years 0-9999 with a Map fallback for anything outside. In practice nobody asks for Hebrew years before ~AD 1 (year 3760), so we can index the typed array as `year - 3760` and get the same hit rate for ~25 KB instead of ~40 KB, with no fallback data structure to reason about. Out-of-range years now fall through to the uncached path. End-to-end timings (median of multiple runs against built dist): workload prev (40KB) this (25KB) elapsedDays 31 yrs x 1000 iter ~29 ms ~31 ms elapsedDays 4001 yrs in range ~33 ms ~45 ms hebrew2abs 111 yrs x 13 mo x 2 ~38 ms ~41 ms abs2hebrew full calendar year ~300 ms ~305 ms The added subtraction per call costs ~1 ns; everything is still substantially faster than the original Map (47 / 110 / 41 / 288 ms).
Reverts the offset+min/max range from the previous commit in favor of direct indexing by year. The simpler single-bound check is marginally faster on hot workloads, and 40 KB vs 25 KB is a non-difference for a date library. Years outside the range still fall through uncached.
The full 10000-entry cache spent most of its memory on years no caller ever touches. Hebrew year 5000 is ~AD 1240 and year 6999 is ~AD 3240, covering every realistic modern use case in 2000 Int32 slots (~8 KB). Years outside the range fall through to the uncached calculation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The previous Map-based cache provided real benefit (~2.7x over uncached
calls on a hot, narrow workload), but Map.get/set carry hashing and
boxing overhead that grow with cache size. Hebrew years are dense small
integers ideal for direct typed-array indexing.
Switch to an Int32Array(10000) indexed by year, using 0 as the
"not computed" sentinel (every valid result is >= 1). A Map fallback
covers years outside the pre-allocated range so behavior is unchanged
for any input.
Benchmarks against the built dist (bench/elapsedDaysReal.bench.mjs),
median of 7 runs:
elapsedDays 31 yrs x 1000 iter 47.5ms -> 17.4ms (2.7x)
elapsedDays 4001 yrs x 1 pass 110.4ms -> 22.0ms (5.0x)
hebrew2abs 111 yrs x 13 mo x 2 41.0ms -> 28.8ms (1.4x)
abs2hebrew full calendar year 288.1ms -> 228.5ms (1.2x)
Memory cost is ~40 KB pre-allocated, vs a Map that grew per call.
Tests unchanged (all 105 passing).