perf(object): O(1) dynamic property set + wide-object read index (#5054)#5059
Merged
Conversation
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).
805ce2a to
58e5600
Compare
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.
Fixes #5054.
Problem
Building a wide dynamic object —
obj[key] = vthousands of times — was quadratic. 10k props took ~12s; 100k never finished. Both the write and every subsequent read did a linearkeys_arrayscan per key.Fix — two halves
Write path (
proxy.rs::ordinary_set_with_receiver)Every
obj[key] = vprobedown_set_descriptoron 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: plainGC_TYPE_OBJECTwritten as its own receiver, no descriptor on this object (newOBJ_FLAG_HAS_DESCRIPTORSGcHeader bit, travels with the object on evacuation),class_id == 0, prototype exactlyObject.prototype(nosetPrototypeOf, no descriptor onObject.prototype), extensible, string key.GLOBAL_DESCRIPTORS_IN_USEwas 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 dedicatedOBJECT_PROTO_DESCRIPTORSglobal 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 onkeys_arrayid, 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 undefinedcliff.Results
node --experimental-strip-types: delete+rebuild,definePropertynon-writable/setter installed after a wide build,freeze,preventExtensions, symbol keys,Object.prototypesetter intercepting missing-key writes,setPrototypeOfto a proto with a setter, spread/entries/JSON round-trip, shape sharing,hasOwnProperty/in.wide_object_index_reads_and_descriptor_writes.Code-only PR — version bump + changelog left for merge time.