Skip to content

perf(object): O(1) dynamic property set + wide-object read index (#5054)#5059

Merged
proggeramlug merged 1 commit into
mainfrom
perf/dynamic-set-own-key-scan
Jun 13, 2026
Merged

perf(object): O(1) dynamic property set + wide-object read index (#5054)#5059
proggeramlug merged 1 commit into
mainfrom
perf/dynamic-set-own-key-scan

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Fixes #5054.

Problem

Building a wide dynamic object — obj[key] = v thousands of times — was quadratic. 10k props took ~12s; 100k never finished. Both the write and every subsequent read did a linear keys_array scan per key.

Fix — two halves

Write path (proxy.rs::ordinary_set_with_receiver)

Every obj[key] = v probed own_set_descriptor on the target, which ends in a linear keys scan. Added a fast path that reduces the write to the ordinary data-property store when nothing the spec walk models can apply: plain GC_TYPE_OBJECT written as its own receiver, no descriptor on this object (new OBJ_FLAG_HAS_DESCRIPTORS GcHeader bit, travels with the object on evacuation), class_id == 0, prototype exactly Object.prototype (no setPrototypeOf, no descriptor on Object.prototype), extensible, string key.

GLOBAL_DESCRIPTORS_IN_USE was unusable as the gate — it's poisoned at startup when the runtime installs attrs on builtin prototypes (RegExp etc.), so the very first dynamic write already saw it true. The per-object header flag plus a dedicated OBJECT_PROTO_DESCRIPTORS global are precise.

Read path (field_get_set.rs::js_object_get_field_by_name)

The 1024-entry inline field cache thrashes once an object has thousands of distinct keys. For keys arrays past 257 entries, build a validated key→index HashMap (keyed on keys_array id, LRU of 4), re-validated against the live slot on every hit — exactly the trust model the inline cache already uses. Misses fall through to the linear scan (the index is an accelerator, never authoritative); scan hits back-fill it. Side effect: indexed hits lift the old >65536-key → return undefined cliff.

Results

  • 10k props: 12s → 0.05s. 100k build + read-all: never → 0.14s.
  • 12-case edge battery byte-identical to node --experimental-strip-types: delete+rebuild, defineProperty non-writable/setter installed after a wide build, freeze, preventExtensions, symbol keys, Object.prototype setter intercepting missing-key writes, setPrototypeOf to a proto with a setter, spread/entries/JSON round-trip, shape sharing, hasOwnProperty/in.
  • New unit test wide_object_index_reads_and_descriptor_writes.
  • Gap suite: 207 pass / 27 fail, set-identical to a pristine-main baseline binary (0 regressions); the 27 are pre-existing/environmental.

Code-only PR — version bump + changelog left for merge time.

Building a wide dynamic object (`obj[key] = v` in a loop) was quadratic:
both the write and every read did a linear keys_array scan. 10k props
took ~12s; 100k never finished. Two halves:

WRITE — ordinary_set_with_receiver (proxy.rs) probed own_set_descriptor
on the target, ending in a linear scan, on every `obj[key] = v`. Add a
fast path: a plain GC_TYPE_OBJECT written as its own receiver with no
descriptor on it (new OBJ_FLAG_HAS_DESCRIPTORS header bit), no class
machinery, exactly Object.prototype (no setPrototypeOf, no descriptor on
Object.prototype), extensible, string key -> the ordinary data store.
GLOBAL_DESCRIPTORS_IN_USE was unusable as the gate (poisoned at startup
by builtin attrs on RegExp.prototype etc.); the per-object flag and a
dedicated OBJECT_PROTO_DESCRIPTORS global are precise.

READ — js_object_get_field_by_name's 1024-entry inline cache thrashes
once an object has thousands of distinct keys. For keys arrays past 257
entries, build a validated key->index HashMap (per keys_array id, LRU of
4), re-validated against the live slot on every hit exactly like the
inline cache. Misses fall through to the linear scan (the index is an
accelerator, not authoritative); scan hits back-fill it. As a side
effect this lifts the old >65536-key 'return undefined' cliff for
indexed hits.

10k props: 12s -> 0.05s. 100k build+read-all: never -> 0.14s.

12-case edge battery (delete+rebuild, defineProperty non-writable/setter
after build, freeze, preventExtensions, symbol keys, Object.prototype
setter, setPrototypeOf, spread/entries/JSON, shape sharing, hasOwn/in)
byte-identical to Node. New unit test wide_object_index_reads_and_
descriptor_writes. Gap suite: 207/27 identical to baseline (0 regress).
@proggeramlug proggeramlug force-pushed the perf/dynamic-set-own-key-scan branch from 805ce2a to 58e5600 Compare June 13, 2026 07:28
@proggeramlug proggeramlug merged commit ab5b7e8 into main Jun 13, 2026
13 checks passed
@proggeramlug proggeramlug deleted the perf/dynamic-set-own-key-scan branch June 13, 2026 08:49
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.

Quadratic dynamic property insertion: obj[key] = v does a linear own-key scan per set (10k props ≈ 12s)

1 participant