From bf695d61e79862d2e72dae9f62a82749bb92bacb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 28 Aug 2026 13:32:52 +0200 Subject: [PATCH] perf(object): kill the READ path's per-element key scan (-27% on overwrite loop) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The twin of #8936: an isolated overwrite-loop profile still showed js_array_get_f64 at 23.5% self time, and the caller graph attributed it to accessors::own_data_field_by_name — the [[Get]] fallback's own copy of the per-element js_array_get + js_string_key_matches walk, run on every dynamic string-keyed read. Replaced with the shared keys_find_slot_by_key_ptr helper (shape index first, raw dense-slot fallback). SSO-aware byte resolution preserves #1781's short-key acceptance. Interleaved A/B at stable load: 660->496, 681->497, 680->497 ms (-27%). Node same host: 55 ms. Suite 2772 passed. --- changelog.d/8947-read-scan-kill.md | 18 ++++++++++++++++++ .../src/object/field_get_set/accessors.rs | 15 +++++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 changelog.d/8947-read-scan-kill.md diff --git a/changelog.d/8947-read-scan-kill.md b/changelog.d/8947-read-scan-kill.md new file mode 100644 index 0000000000..efe4b295e9 --- /dev/null +++ b/changelog.d/8947-read-scan-kill.md @@ -0,0 +1,18 @@ +Killed the READ path's per-element key scan — the twin of #8936's write/delete +fix: **−27% on the dynamic-property overwrite loop** (interleaved A/B pairs at +stable load: 660 → 496, 681 → 497, 680 → 497 ms; node on the same host: 55 ms). + +#8936 replaced the `for i in 0..key_count { js_array_get(keys, i) + +js_string_key_matches }` walks on the `[[Set]]` and `delete` paths, but an +isolated profile of a pure overwrite loop still showed `js_array_get_f64` at +**23.5% self time** — and the caller graph attributed it to +`accessors::own_data_field_by_name`: the `[[Get]]` fallback's own copy of the +same scan, run on every dynamic string-keyed read. + +It now goes through the same shared helper (`keys_find_slot_by_key_ptr`): the +shape hash index answers in O(1) when present, with the raw dense-slot linear +scan as fallback and correctness backstop. The helper's byte resolver is +SSO-aware, preserving #1781's short-key acceptance that the old loop's comment +guarded. + +Suite: 2772 passed, 0 failed (full macOS run). diff --git a/crates/perry-runtime/src/object/field_get_set/accessors.rs b/crates/perry-runtime/src/object/field_get_set/accessors.rs index 9bff3f8bb1..46b190cc6d 100644 --- a/crates/perry-runtime/src/object/field_get_set/accessors.rs +++ b/crates/perry-runtime/src/object/field_get_set/accessors.rs @@ -115,12 +115,15 @@ pub(crate) unsafe fn own_data_field_by_name( crate::object::object_live_slot_count(obj), crate::object::INLINE_SLOT_FLOOR as u32, ) as usize; - for i in 0..key_count { - let key_val = crate::array::js_array_get(keys, i as u32); - // #1781: accept inline SSO short keys — `is_string()` is - // STRING_TAG-only, so the pre-fix shape silently skipped any - // ≤5-byte key stored as a `SHORT_STRING_TAG` value. - if crate::string::js_string_key_matches(key_val, key) { + // #6759: shape-index + raw dense-slot scan, replacing the per-element + // `js_array_get` + `js_string_key_matches` walk. This is the READ path's + // copy of the scan that #8936 killed on the [[Set]]/delete side — an + // isolated overwrite-loop profile still showed `js_array_get_f64` at 23.5% + // self time, and the caller graph attributed it here. The shared helper + // preserves #1781's SSO-key acceptance (its byte resolver is SSO-aware). + if let Some(islot) = crate::object::keys_find_slot_by_key_ptr(keys, key_count as u32, key) { + let i = islot as usize; + { if i < alloc_limit { return Some(js_object_get_field(obj, i as u32)); }