From 65418072af1cf8be1743cbe657f8adf4ce082eea Mon Sep 17 00:00:00 2001 From: Andres Gutierrez Date: Mon, 22 Jun 2026 20:58:10 -0700 Subject: [PATCH] perf(object): add data-property fast path to OrdinaryGet Avoid constructing a PropertyDescriptor for the common case of reading a plain data property. `ordinary_get` now resolves named own-property lookups on ordinary objects in a single shape probe via `PropertyMap::get_own_data_named`, reading the value directly from storage and going straight to the prototype walk on a miss (no redundant second lookup). Gated on the object using the ordinary `__get_own_property__`, so accessors, indexed keys and exotic objects fall back to the spec path. Benchmarks vs baseline: regexp -5.9%, earley-boyer -3.7%, raytrace -3.2%; properties/access -10%, prototypes/chain -10%, strings -12..-23%. --- .../engine/src/object/internal_methods/mod.rs | 27 +++++++++++++ core/engine/src/object/property_map.rs | 38 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/core/engine/src/object/internal_methods/mod.rs b/core/engine/src/object/internal_methods/mod.rs index 8e474b5dde5..1bc75ed6ac9 100644 --- a/core/engine/src/object/internal_methods/mod.rs +++ b/core/engine/src/object/internal_methods/mod.rs @@ -9,6 +9,7 @@ use std::ops::{Deref, DerefMut}; use super::{ JsPrototype, PROTOTYPE, + property_map::OwnDataLookup, shape::slot::{Slot, SlotAttributes}, }; use crate::{ @@ -709,6 +710,32 @@ pub(crate) fn ordinary_get( receiver: JsValue, context: &mut InternalMethodPropertyContext<'_>, ) -> JsResult { + // Fast path: read a plain data own-property's value directly, skipping + // `PropertyDescriptor` construction. Accessors, indexed keys and exotic objects fall + // through to the spec path below. + if !matches!(key, PropertyKey::Index(_)) + && obj.vtable().__get_own_property__ as usize + == ordinary_get_own_property as *const () as usize + { + let lookup = obj + .borrow() + .properties + .get_own_data_named(key, context.slot()); + match lookup { + OwnDataLookup::Data(value) => return Ok(value), + OwnDataLookup::Absent => { + if let Some(parent) = obj.__get_prototype_of__(context)? { + context.slot().set_not_cacheable_if_already_prototype(); + context.slot().attributes |= SlotAttributes::PROTOTYPE; + return parent.__get__(key, receiver, context); + } + return Ok(JsValue::undefined()); + } + // Accessor: fall through to invoke the getter. + OwnDataLookup::Accessor => {} + } + } + // 1. Assert: IsPropertyKey(P) is true. // 2. Let desc be ? O.[[GetOwnProperty]](P). match obj.__get_own_property__(key, context)? { diff --git a/core/engine/src/object/property_map.rs b/core/engine/src/object/property_map.rs index 80cf4408272..73b37a5fd7f 100644 --- a/core/engine/src/object/property_map.rs +++ b/core/engine/src/object/property_map.rs @@ -14,6 +14,18 @@ use rustc_hash::FxHashMap; use std::{collections::hash_map, iter::FusedIterator}; use thin_vec::ThinVec; +/// Result of [`PropertyMap::get_own_data_named`], the fast-path own-property lookup +/// used by `OrdinaryGet`. +#[derive(Debug)] +pub(crate) enum OwnDataLookup { + /// A plain data own property; carries its value read directly from storage. + Data(JsValue), + /// The own property exists but is an accessor; the caller must use the descriptor path. + Accessor, + /// No such own property; the caller can proceed directly to the prototype walk. + Absent, +} + /// This represents all the indexed properties. /// /// The index properties can be stored in two storage methods: @@ -565,6 +577,32 @@ impl PropertyMap { None } + /// Fast path for `OrdinaryGet`: resolves a named own-property lookup in a single shape + /// probe, returning a plain data property's value straight from storage without building + /// a [`PropertyDescriptor`]. `out_slot` is updated only on a data hit. + #[must_use] + pub(crate) fn get_own_data_named( + &self, + key: &PropertyKey, + out_slot: &mut Slot, + ) -> OwnDataLookup { + debug_assert!(!matches!(key, PropertyKey::Index(_))); + + let Some(slot) = self.shape.lookup(key) else { + return OwnDataLookup::Absent; + }; + if slot.attributes.is_accessor_descriptor() { + return OwnDataLookup::Accessor; + } + + out_slot.index = slot.index; + // Remove all descriptor attributes, but keep inline caching bits. + out_slot.attributes = (out_slot.attributes & SlotAttributes::INLINE_CACHE_BITS) + | slot.attributes + | SlotAttributes::FOUND; + OwnDataLookup::Data(self.storage[slot.index as usize].clone()) + } + /// Get the property with the given key from the [`PropertyMap`]. #[must_use] pub(crate) fn get_storage(&self, Slot { index, attributes }: Slot) -> PropertyDescriptor {