From 1a5a235449dbe8f380e6039d14d1c6b2ad595368 Mon Sep 17 00:00:00 2001 From: Aakash Patel Date: Thu, 30 Jan 2025 16:53:06 -0800 Subject: [PATCH] Cache the object's HC for use in GetNextPName Summary: The first step of speeding up `for-in` iteration over objects is to avoid doing unnecessary membership checks every iteration. This is done by also caching the object's own HiddenClass (we're already storing the rest of the prototype chain) along with the number of properties it has. Then on each iteration we can do some simple math and check that the HiddenClass hasn't changed instead of running `findProperty`. Reviewed By: tmikov Differential Revision: D67292849 fbshipit-source-id: a1807d0d1bb5ef1a6bf318bb32e8088ab2bc578b --- lib/VM/Interpreter-slowpaths.cpp | 21 +++++++++++++ lib/VM/JSObject.cpp | 52 ++++++++++++++++++++++++++++---- lib/VM/StaticH.cpp | 20 ++++++++++++ 3 files changed, 87 insertions(+), 6 deletions(-) diff --git a/lib/VM/Interpreter-slowpaths.cpp b/lib/VM/Interpreter-slowpaths.cpp index f288d2475ce..8b3a39acafe 100644 --- a/lib/VM/Interpreter-slowpaths.cpp +++ b/lib/VM/Interpreter-slowpaths.cpp @@ -361,6 +361,7 @@ ExecutionStatus Interpreter::caseGetNextPName( PinnedValue<> tmp; PinnedValue propObj; PinnedValue tmpPropNameStorage; + PinnedValue cachedClass; } lv; LocalsRAII lraii{runtime, &lv}; @@ -371,11 +372,31 @@ ExecutionStatus Interpreter::caseGetNextPName( auto arr = Handle::vmcast(&O2REG(GetNextPName)); uint32_t idx = O4REG(GetNextPName).getNumber(); uint32_t size = O5REG(GetNextPName).getNumber(); + + // If there's a class at index 2, it means we have a cached class. + uint32_t startIdx = 0; + uint32_t numObjProps = 0; + if (LLVM_LIKELY(size > 2)) { + lv.cachedClass = dyn_vmcast(arr->at(runtime, 2)); + if (lv.cachedClass.get()) { + startIdx = arr->at(runtime, 0).getNumberAs(); + numObjProps = arr->at(runtime, 1).getNumberAs(); + } + } + MutableHandle propObj{lv.propObj}; MutableHandle tmpPropNameStorage{lv.tmpPropNameStorage}; // Loop until we find a property which is present. while (LLVM_LIKELY(idx < size)) { lv.tmp = arr->at(runtime, idx); + // If there's no caching, lv.cachedClass is nullptr and the comparison will + // fail. + if (LLVM_LIKELY(size > 0) && idx - startIdx < numObjProps && + LLVM_LIKELY(lv.cachedClass.get() == obj->getClass(runtime))) { + // Cached. + propObj = obj; + break; + } if (lv.tmp->isSymbol()) { // NOTE: This call is safe because we immediately discard desc, // so it can't outlive the SymbolID. diff --git a/lib/VM/JSObject.cpp b/lib/VM/JSObject.cpp index 2ebd92fa0d1..14ced7d46d6 100644 --- a/lib/VM/JSObject.cpp +++ b/lib/VM/JSObject.cpp @@ -3080,6 +3080,8 @@ namespace { /// All string keys are added as Symbols, to aid lookup in GetNextPName. /// All number keys are added as numbers. /// Only enumerable properties are included. +/// Write the number of object properties for the obj to index 1 of \p arr +/// (does not include proto properties). /// Returns the index after the last property added. CallResult appendAllPropertyKeys( Handle obj, @@ -3130,7 +3132,7 @@ CallResult appendAllPropertyKeys( } } }; // end of lambda expression - while (lv.head.get()) { + for (bool first = true; lv.head.get(); first = false) { GCScope gcScope(runtime); // enumerableProps will contain all enumerable own properties from obj. @@ -3145,6 +3147,13 @@ CallResult appendAllPropertyKeys( } auto enumerableProps = *cr; auto marker = gcScope.createMarker(); + if (first && beginIndex > 0) { + arr->set( + runtime, + 1, + HermesValue::encodeTrustedNumberValue( + enumerableProps->getEndIndex())); + } for (unsigned i = 0, e = enumerableProps->getEndIndex(); i < e; ++i) { gcScope.flushToMarker(marker); lv.prop = enumerableProps->at(runtime, i).unboxToHV(runtime); @@ -3221,8 +3230,11 @@ CallResult appendAllPropertyKeys( } /// Adds the hidden classes of the prototype chain of obj to arr, -/// starting with the prototype of obj at index 0, etc., and +/// starting with the class of obj at index 2, etc., and /// terminates with null. +/// Index 0 will contain the end index after this is complete. +/// Index 1 will contain the number of properties pushed in obj. +/// Index 2 will contain the class of obj. /// /// \param obj The object whose prototype chain should be output /// \param[out] arr The array where the classes will be appended. This @@ -3232,15 +3244,31 @@ ExecutionStatus setProtoClasses( Handle obj, MutableHandle &arr) { // Layout of a JSArray stored in the for-in cache: - // [class(proto(obj)), class(proto(proto(obj))), ..., null, prop0, prop1, ...] + // [numProtos, numObjProps, class(obj), + // class(proto(obj)), class(proto(proto(obj))), ..., null, + // prop0, prop1, ...] + // prop0 is at index numProtos. + // There are numObjProps properties on obj before the proto properties start. if (!obj->shouldCacheForIn(runtime)) { arr->clear(runtime); return ExecutionStatus::RETURNED; } - MutableHandle head(runtime, obj->getParent(runtime)); + MutableHandle head(runtime, *obj); MutableHandle<> clazz(runtime); GCScopeMarkerRAII marker{runtime}; + // Push entries 0 and 1 (we'll fill them in later). + clazz = HermesValue::encodeTrustedNumberValue(0); + if (LLVM_UNLIKELY( + BigStorage::push_back(arr, runtime, clazz) == + ExecutionStatus::EXCEPTION)) { + return ExecutionStatus::EXCEPTION; + } + if (LLVM_UNLIKELY( + BigStorage::push_back(arr, runtime, clazz) == + ExecutionStatus::EXCEPTION)) { + return ExecutionStatus::EXCEPTION; + } while (head.get()) { if (!head->shouldCacheForIn(runtime)) { arr->clear(runtime); @@ -3262,11 +3290,22 @@ ExecutionStatus setProtoClasses( marker.flush(); } clazz = HermesValue::encodeNullValue(); - return BigStorage::push_back(arr, runtime, clazz); + if (LLVM_UNLIKELY( + BigStorage::push_back(arr, runtime, clazz) == + ExecutionStatus::EXCEPTION)) { + return ExecutionStatus::EXCEPTION; + } + + arr->set( + runtime, 0, HermesValue::encodeTrustedNumberValue(arr->size(runtime))); + return ExecutionStatus::RETURNED; } /// Verifies that the classes of obj's prototype chain still matches those /// previously prefixed to arr by setProtoClasses. +/// Assumes that the first elements are the count of HiddenClasses and the count +/// of the object's own properties, and element 2 is is the object's own +/// HiddenClass, so it doesn't check those. /// /// \param obj The object whose prototype chain should be verified /// \param arr Array previously populated by setProtoClasses @@ -3277,7 +3316,8 @@ uint32_t matchesProtoClasses( Handle obj, Handle arr) { MutableHandle head(runtime, obj->getParent(runtime)); - uint32_t i = 0; + // Skip the counts and object's own class. + uint32_t i = 3; while (head.get()) { HermesValue protoCls = arr->at(runtime, i++); if (protoCls.isNull() || protoCls.getObject() != head->getClass(runtime) || diff --git a/lib/VM/StaticH.cpp b/lib/VM/StaticH.cpp index d1def9c5575..c5726739e2d 100644 --- a/lib/VM/StaticH.cpp +++ b/lib/VM/StaticH.cpp @@ -1865,16 +1865,36 @@ extern "C" SHLegacyValue _sh_ljs_get_next_pname_rjs( PinnedValue<> tmp; PinnedValue propObj; PinnedValue tmpPropNameStorage; + PinnedValue cachedClass; } lv; LocalsRAII lraii{runtime, &lv}; GCScopeMarkerRAII marker{runtime}; uint32_t idx = toPHV(indexVal)->getNumber(); uint32_t size = toPHV(sizeVal)->getNumber(); + // If there's a class at index 2, it means we have a cached class. + uint32_t startIdx = 0; + uint32_t numObjProps = 0; + if (LLVM_LIKELY(size > 2)) { + lv.cachedClass = dyn_vmcast(arr->at(runtime, 2)); + if (lv.cachedClass.get()) { + startIdx = arr->at(runtime, 0).getNumberAs(); + numObjProps = arr->at(runtime, 1).getNumberAs(); + } + } + MutableHandle propObj{lv.propObj}; MutableHandle tmpPropNameStorage{lv.tmpPropNameStorage}; // Loop until we find a property which is present. while (idx < size) { lv.tmp = arr->at(runtime, idx); + // If there's no caching, lv.cachedClass is nullptr and the comparison + // will fail. + if (LLVM_LIKELY(size > 0) && idx - startIdx < numObjProps && + LLVM_LIKELY(lv.cachedClass.get() == obj->getClass(runtime))) { + // Cached. + propObj = obj; + break; + } if (lv.tmp->isSymbol()) { // NOTE: This call is safe because we immediately discard desc, // so it can't outlive the SymbolID.