From a677d96b8fa90e4a714200fdd222c5fe287d92c6 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 6 May 2025 13:16:44 +0200 Subject: [PATCH] LibJS: Make GetById cache polymorphic Instead of monomorphic (1 shape), GetById inline caches are now polymorphic (4 shapes). This improves inline cache hit rates greatly on most web JavaScript. For example, Speedometer 2.1 sees 88% -> 97% cache hit rate improvement. 1.71x speedup on MicroBench/pic-get-own.js 1.82x speedup on MicroBench/pic-get-pchain.js --- Libraries/LibJS/Bytecode/Executable.h | 13 ++-- Libraries/LibJS/Bytecode/Interpreter.cpp | 89 +++++++++++++----------- 2 files changed, 59 insertions(+), 43 deletions(-) diff --git a/Libraries/LibJS/Bytecode/Executable.h b/Libraries/LibJS/Bytecode/Executable.h index 08f9b2c3c95..ecffacf1df4 100644 --- a/Libraries/LibJS/Bytecode/Executable.h +++ b/Libraries/LibJS/Bytecode/Executable.h @@ -23,11 +23,16 @@ namespace JS::Bytecode { +// Represents one polymorphic inline cache used for property lookups. struct PropertyLookupCache { - WeakPtr shape; - Optional property_offset; - WeakPtr prototype; - WeakPtr prototype_chain_validity; + static constexpr size_t max_number_of_shapes_to_remember = 4; + struct Entry { + WeakPtr shape; + Optional property_offset; + WeakPtr prototype; + WeakPtr prototype_chain_validity; + }; + AK::Array entries; }; struct GlobalVariableCache : public PropertyLookupCache { diff --git a/Libraries/LibJS/Bytecode/Interpreter.cpp b/Libraries/LibJS/Bytecode/Interpreter.cpp index 4070fc59ea3..049d8bd3641 100644 --- a/Libraries/LibJS/Bytecode/Interpreter.cpp +++ b/Libraries/LibJS/Bytecode/Interpreter.cpp @@ -998,44 +998,53 @@ inline ThrowCompletionOr get_by_id(VM& vm, Optional auto& shape = base_obj->shape(); - if (cache.prototype) { - // OPTIMIZATION: If the prototype chain hasn't been mutated in a way that would invalidate the cache, we can use it. - bool can_use_cache = [&]() -> bool { - if (&shape != cache.shape) - return false; - if (!cache.prototype_chain_validity) - return false; - if (!cache.prototype_chain_validity->is_valid()) - return false; - return true; - }(); - if (can_use_cache) { - auto value = cache.prototype->get_direct(cache.property_offset.value()); + for (auto& cache_entry : cache.entries) { + if (cache_entry.prototype) { + // OPTIMIZATION: If the prototype chain hasn't been mutated in a way that would invalidate the cache, we can use it. + bool can_use_cache = [&]() -> bool { + if (&shape != cache_entry.shape) + return false; + if (!cache_entry.prototype_chain_validity) + return false; + if (!cache_entry.prototype_chain_validity->is_valid()) + return false; + return true; + }(); + if (can_use_cache) { + auto value = cache_entry.prototype->get_direct(cache_entry.property_offset.value()); + if (value.is_accessor()) + return TRY(call(vm, value.as_accessor().getter(), this_value)); + return value; + } + } else if (&shape == cache_entry.shape) { + // OPTIMIZATION: If the shape of the object hasn't changed, we can use the cached property offset. + auto value = base_obj->get_direct(cache_entry.property_offset.value()); if (value.is_accessor()) return TRY(call(vm, value.as_accessor().getter(), this_value)); return value; } - } else if (&shape == cache.shape) { - // OPTIMIZATION: If the shape of the object hasn't changed, we can use the cached property offset. - auto value = base_obj->get_direct(cache.property_offset.value()); - if (value.is_accessor()) - return TRY(call(vm, value.as_accessor().getter(), this_value)); - return value; } CacheablePropertyMetadata cacheable_metadata; auto value = TRY(base_obj->internal_get(executable.get_identifier(property), this_value, &cacheable_metadata)); + auto get_cache_slot = [&] -> PropertyLookupCache::Entry& { + for (size_t i = cache.entries.size() - 1; i >= 1; --i) { + cache.entries[i] = cache.entries[i - 1]; + } + cache.entries[0] = {}; + return cache.entries[0]; + }; if (cacheable_metadata.type == CacheablePropertyMetadata::Type::OwnProperty) { - cache = {}; - cache.shape = shape; - cache.property_offset = cacheable_metadata.property_offset.value(); + auto& entry = get_cache_slot(); + entry.shape = shape; + entry.property_offset = cacheable_metadata.property_offset.value(); } else if (cacheable_metadata.type == CacheablePropertyMetadata::Type::InPrototypeChain) { - cache = {}; - cache.shape = &base_obj->shape(); - cache.property_offset = cacheable_metadata.property_offset.value(); - cache.prototype = *cacheable_metadata.prototype; - cache.prototype_chain_validity = *cacheable_metadata.prototype->shape().prototype_chain_validity(); + auto& entry = get_cache_slot(); + entry.shape = &base_obj->shape(); + entry.property_offset = cacheable_metadata.property_offset.value(); + entry.prototype = *cacheable_metadata.prototype; + entry.prototype_chain_validity = *cacheable_metadata.prototype->shape().prototype_chain_validity(); } return value; @@ -1133,8 +1142,8 @@ inline ThrowCompletionOr get_global(Interpreter& interpreter, IdentifierT // OPTIMIZATION: For global var bindings, if the shape of the global object hasn't changed, // we can use the cached property offset. - if (&shape == cache.shape) { - auto value = binding_object.get_direct(cache.property_offset.value()); + if (&shape == cache.entries[0].shape) { + auto value = binding_object.get_direct(cache.entries[0].property_offset.value()); if (value.is_accessor()) return TRY(call(vm, value.as_accessor().getter(), js_undefined())); return value; @@ -1183,8 +1192,8 @@ inline ThrowCompletionOr get_global(Interpreter& interpreter, IdentifierT CacheablePropertyMetadata cacheable_metadata; auto value = TRY(binding_object.internal_get(identifier, js_undefined(), &cacheable_metadata)); if (cacheable_metadata.type == CacheablePropertyMetadata::Type::OwnProperty) { - cache.shape = shape; - cache.property_offset = cacheable_metadata.property_offset.value(); + cache.entries[0].shape = shape; + cache.entries[0].property_offset = cacheable_metadata.property_offset.value(); } return value; } @@ -1192,7 +1201,7 @@ inline ThrowCompletionOr get_global(Interpreter& interpreter, IdentifierT return vm.throw_completion(ErrorType::UnknownIdentifier, identifier); } -inline ThrowCompletionOr put_by_property_key(VM& vm, Value base, Value this_value, Value value, Optional const& base_identifier, PropertyKey name, Op::PropertyKind kind, PropertyLookupCache* cache = nullptr) +inline ThrowCompletionOr put_by_property_key(VM& vm, Value base, Value this_value, Value value, Optional const& base_identifier, PropertyKey name, Op::PropertyKind kind, PropertyLookupCache* caches = nullptr) { // Better error message than to_object would give if (vm.in_strict_mode() && base.is_nullish()) @@ -1224,7 +1233,8 @@ inline ThrowCompletionOr put_by_property_key(VM& vm, Value base, Value thi break; } case Op::PropertyKind::KeyValue: { - if (cache) { + if (caches) { + auto* cache = &caches->entries[0]; if (cache->prototype) { // OPTIMIZATION: If the prototype chain hasn't been mutated in a way that would invalidate the cache, we can use it. bool can_use_cache = [&]() -> bool { @@ -1257,7 +1267,8 @@ inline ThrowCompletionOr put_by_property_key(VM& vm, Value base, Value thi CacheablePropertyMetadata cacheable_metadata; bool succeeded = TRY(object->internal_set(name, value, this_value, &cacheable_metadata)); - if (succeeded && cache) { + if (succeeded && caches) { + auto* cache = &caches->entries[0]; if (cacheable_metadata.type == CacheablePropertyMetadata::Type::OwnProperty) { *cache = {}; cache->shape = object->shape(); @@ -2349,12 +2360,12 @@ ThrowCompletionOr SetGlobal::execute_impl(Bytecode::Interpreter& interpret if (cache.environment_serial_number == declarative_record.environment_serial_number()) { // OPTIMIZATION: For global var bindings, if the shape of the global object hasn't changed, // we can use the cached property offset. - if (&shape == cache.shape) { - auto value = binding_object.get_direct(cache.property_offset.value()); + if (&shape == cache.entries[0].shape) { + auto value = binding_object.get_direct(cache.entries[0].property_offset.value()); if (value.is_accessor()) TRY(call(vm, value.as_accessor().setter(), &binding_object, src)); else - binding_object.put_direct(cache.property_offset.value(), src); + binding_object.put_direct(cache.entries[0].property_offset.value(), src); return {}; } @@ -2416,8 +2427,8 @@ ThrowCompletionOr SetGlobal::execute_impl(Bytecode::Interpreter& interpret return vm.throw_completion(ErrorType::ObjectSetReturnedFalse); } if (cacheable_metadata.type == CacheablePropertyMetadata::Type::OwnProperty) { - cache.shape = shape; - cache.property_offset = cacheable_metadata.property_offset.value(); + cache.entries[0].shape = shape; + cache.entries[0].property_offset = cacheable_metadata.property_offset.value(); } return {}; }