Skip to content

perf: replace Map<number,number> elapsedDays cache with Int32Array#18

Merged
mjradwin merged 4 commits into
mainfrom
claude/elapsed-days-cache-benchmark-9wPp6
May 25, 2026
Merged

perf: replace Map<number,number> elapsedDays cache with Int32Array#18
mjradwin merged 4 commits into
mainfrom
claude/elapsed-days-cache-benchmark-9wPp6

Conversation

@mjradwin

Copy link
Copy Markdown
Member

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).

claude added 4 commits May 24, 2026 22:03
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.
@mjradwin mjradwin merged commit e68efb0 into main May 25, 2026
4 checks passed
@mjradwin mjradwin deleted the claude/elapsed-days-cache-benchmark-9wPp6 branch May 25, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants