Skip to content

perf(core): PHPMNT-394 Index sibling maps by value for constant-time lookup - #69

Draft
TomA-R wants to merge 1 commit into
masterfrom
phpmnt-394-index-sibling-lookups
Draft

perf(core): PHPMNT-394 Index sibling maps by value for constant-time lookup#69
TomA-R wants to merge 1 commit into
masterfrom
phpmnt-394-index-sibling-lookups

Conversation

@TomA-R

@TomA-R TomA-R commented Aug 13, 2026

Copy link
Copy Markdown
Member

Jira: PHPMNT-394

What/Why?

Looking up a cached value scans every sibling argument one by one. That's fine for a small cache, but it gets slow once a cache holds many entries, cycling 200k calls through 1000 cached keys took over a second.

This adds a Map index next to the existing list, so most lookups become instant instead of scanning every entry.

A couple of notes on how it stays correct:

  • Two different objects that just look the same still need the old scan (a Map can't find those by shape), so that case is unchanged.
  • If a custom isEqual is passed in, the index is turned off entirely. A custom comparison might treat values as equal that a Map wouldn't.
  • Small caches (8 or fewer entries at a given level) skip the index and just scan, since scanning a handful of items is already faster than using a Map. I tried removing this cutoff and it made the most common case (repeated calls with the same arguments) 20-30% slower, so I kept it.

Numbers (best of several runs):

scenario before after
Repeated calls with the same arguments (typical case) ~50 ms ~50 ms (no change)
Cycling through 1000 cached keys (large cache) ~1.1 s ~13 ms
Fresh-but-shallow-equal objects (unchanged by design) ~3.7 s ~3.7 s

Rollout/Rollback

Merge / revert

Testing

Existing tests pass, plus new tests for the new code paths (large caches, NaN, functions, multi-argument lookups, custom isEqual). No behavior changes: checked with a manual regression script and by reviewing real usage in checkout-sdk-js and checkout-js

🤖 code changes made with help from Claude 🤖

@TomA-R
TomA-R force-pushed the phpmnt-394-index-sibling-lookups branch from 998140f to 99cb6dc Compare August 13, 2026 10:35
…lookup

Every cache lookup scanned the sibling values one by one with isEqual,
which is fine for a handful of distinct arguments but degrades badly
once a level accumulates many of them (cycling calls through a large
cache could take tens of seconds).

Add a Map index per level of the key tree so most lookups become a
single Map.get. Map key equality (identity, plus NaN equals NaN)
matches the default comparison for everything except shallowly-equal-
but-distinct objects, so:

- objects that are shallowly equal but not the same instance fall
  back to the existing linear scan
- a custom isEqual could treat values as equal that a Map would keep
  apart, so the index is disabled entirely in that case
- levels with 8 or fewer siblings (MAX_SIBLINGS_FOR_SCAN) just keep
  scanning, since a Map lookup costs more than the one or two ===
  checks a narrow scan needs; benchmarking confirms dropping this
  threshold regresses the most common memoize case (repeated calls
  with the same arguments) by 20-30% despite helping wider cases

The lookup itself lives in a single _findMap helper (try the index,
else scan) instead of a nested loop with a mutable isMatched flag, and
the scan uses a manual for loop rather than findIndex(closure) to
avoid allocating a closure on every call in this hot path.

No behavioral differences: verified against the existing suite, a
manual regression script covering NaN/objects/functions/LRU eviction/
multi-level lookups/custom isEqual, and by tracing real usage in
checkout-sdk-js and checkout-js (both mostly use memoizeOne, which
rarely engages this path, and the one custom-isEqual call site
disables the index outright).
@TomA-R
TomA-R force-pushed the phpmnt-394-index-sibling-lookups branch from 99cb6dc to a05eb8e Compare August 23, 2026 12:09
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.

1 participant