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
This commit is contained in:
Andreas Kling 2025-05-06 13:16:44 +02:00 committed by Andreas Kling
parent 678c15a06a
commit a677d96b8f
Notes: github-actions[bot] 2025-05-06 22:28:14 +00:00
2 changed files with 59 additions and 43 deletions

View file

@ -23,11 +23,16 @@
namespace JS::Bytecode { namespace JS::Bytecode {
// Represents one polymorphic inline cache used for property lookups.
struct PropertyLookupCache { struct PropertyLookupCache {
WeakPtr<Shape> shape; static constexpr size_t max_number_of_shapes_to_remember = 4;
Optional<u32> property_offset; struct Entry {
WeakPtr<Object> prototype; WeakPtr<Shape> shape;
WeakPtr<PrototypeChainValidity> prototype_chain_validity; Optional<u32> property_offset;
WeakPtr<Object> prototype;
WeakPtr<PrototypeChainValidity> prototype_chain_validity;
};
AK::Array<Entry, max_number_of_shapes_to_remember> entries;
}; };
struct GlobalVariableCache : public PropertyLookupCache { struct GlobalVariableCache : public PropertyLookupCache {

View file

@ -998,44 +998,53 @@ inline ThrowCompletionOr<Value> get_by_id(VM& vm, Optional<IdentifierTableIndex>
auto& shape = base_obj->shape(); auto& shape = base_obj->shape();
if (cache.prototype) { for (auto& cache_entry : cache.entries) {
// OPTIMIZATION: If the prototype chain hasn't been mutated in a way that would invalidate the cache, we can use it. if (cache_entry.prototype) {
bool can_use_cache = [&]() -> bool { // OPTIMIZATION: If the prototype chain hasn't been mutated in a way that would invalidate the cache, we can use it.
if (&shape != cache.shape) bool can_use_cache = [&]() -> bool {
return false; if (&shape != cache_entry.shape)
if (!cache.prototype_chain_validity) return false;
return false; if (!cache_entry.prototype_chain_validity)
if (!cache.prototype_chain_validity->is_valid()) return false;
return false; if (!cache_entry.prototype_chain_validity->is_valid())
return true; return false;
}(); return true;
if (can_use_cache) { }();
auto value = cache.prototype->get_direct(cache.property_offset.value()); 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()) if (value.is_accessor())
return TRY(call(vm, value.as_accessor().getter(), this_value)); return TRY(call(vm, value.as_accessor().getter(), this_value));
return 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; CacheablePropertyMetadata cacheable_metadata;
auto value = TRY(base_obj->internal_get(executable.get_identifier(property), this_value, &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) { if (cacheable_metadata.type == CacheablePropertyMetadata::Type::OwnProperty) {
cache = {}; auto& entry = get_cache_slot();
cache.shape = shape; entry.shape = shape;
cache.property_offset = cacheable_metadata.property_offset.value(); entry.property_offset = cacheable_metadata.property_offset.value();
} else if (cacheable_metadata.type == CacheablePropertyMetadata::Type::InPrototypeChain) { } else if (cacheable_metadata.type == CacheablePropertyMetadata::Type::InPrototypeChain) {
cache = {}; auto& entry = get_cache_slot();
cache.shape = &base_obj->shape(); entry.shape = &base_obj->shape();
cache.property_offset = cacheable_metadata.property_offset.value(); entry.property_offset = cacheable_metadata.property_offset.value();
cache.prototype = *cacheable_metadata.prototype; entry.prototype = *cacheable_metadata.prototype;
cache.prototype_chain_validity = *cacheable_metadata.prototype->shape().prototype_chain_validity(); entry.prototype_chain_validity = *cacheable_metadata.prototype->shape().prototype_chain_validity();
} }
return value; return value;
@ -1133,8 +1142,8 @@ inline ThrowCompletionOr<Value> get_global(Interpreter& interpreter, IdentifierT
// OPTIMIZATION: For global var bindings, if the shape of the global object hasn't changed, // OPTIMIZATION: For global var bindings, if the shape of the global object hasn't changed,
// we can use the cached property offset. // we can use the cached property offset.
if (&shape == cache.shape) { if (&shape == cache.entries[0].shape) {
auto value = binding_object.get_direct(cache.property_offset.value()); auto value = binding_object.get_direct(cache.entries[0].property_offset.value());
if (value.is_accessor()) if (value.is_accessor())
return TRY(call(vm, value.as_accessor().getter(), js_undefined())); return TRY(call(vm, value.as_accessor().getter(), js_undefined()));
return value; return value;
@ -1183,8 +1192,8 @@ inline ThrowCompletionOr<Value> get_global(Interpreter& interpreter, IdentifierT
CacheablePropertyMetadata cacheable_metadata; CacheablePropertyMetadata cacheable_metadata;
auto value = TRY(binding_object.internal_get(identifier, js_undefined(), &cacheable_metadata)); auto value = TRY(binding_object.internal_get(identifier, js_undefined(), &cacheable_metadata));
if (cacheable_metadata.type == CacheablePropertyMetadata::Type::OwnProperty) { if (cacheable_metadata.type == CacheablePropertyMetadata::Type::OwnProperty) {
cache.shape = shape; cache.entries[0].shape = shape;
cache.property_offset = cacheable_metadata.property_offset.value(); cache.entries[0].property_offset = cacheable_metadata.property_offset.value();
} }
return value; return value;
} }
@ -1192,7 +1201,7 @@ inline ThrowCompletionOr<Value> get_global(Interpreter& interpreter, IdentifierT
return vm.throw_completion<ReferenceError>(ErrorType::UnknownIdentifier, identifier); return vm.throw_completion<ReferenceError>(ErrorType::UnknownIdentifier, identifier);
} }
inline ThrowCompletionOr<void> put_by_property_key(VM& vm, Value base, Value this_value, Value value, Optional<FlyString const&> const& base_identifier, PropertyKey name, Op::PropertyKind kind, PropertyLookupCache* cache = nullptr) inline ThrowCompletionOr<void> put_by_property_key(VM& vm, Value base, Value this_value, Value value, Optional<FlyString const&> const& base_identifier, PropertyKey name, Op::PropertyKind kind, PropertyLookupCache* caches = nullptr)
{ {
// Better error message than to_object would give // Better error message than to_object would give
if (vm.in_strict_mode() && base.is_nullish()) if (vm.in_strict_mode() && base.is_nullish())
@ -1224,7 +1233,8 @@ inline ThrowCompletionOr<void> put_by_property_key(VM& vm, Value base, Value thi
break; break;
} }
case Op::PropertyKind::KeyValue: { case Op::PropertyKind::KeyValue: {
if (cache) { if (caches) {
auto* cache = &caches->entries[0];
if (cache->prototype) { if (cache->prototype) {
// OPTIMIZATION: If the prototype chain hasn't been mutated in a way that would invalidate the cache, we can use it. // 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 { bool can_use_cache = [&]() -> bool {
@ -1257,7 +1267,8 @@ inline ThrowCompletionOr<void> put_by_property_key(VM& vm, Value base, Value thi
CacheablePropertyMetadata cacheable_metadata; CacheablePropertyMetadata cacheable_metadata;
bool succeeded = TRY(object->internal_set(name, value, this_value, &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) { if (cacheable_metadata.type == CacheablePropertyMetadata::Type::OwnProperty) {
*cache = {}; *cache = {};
cache->shape = object->shape(); cache->shape = object->shape();
@ -2349,12 +2360,12 @@ ThrowCompletionOr<void> SetGlobal::execute_impl(Bytecode::Interpreter& interpret
if (cache.environment_serial_number == declarative_record.environment_serial_number()) { 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, // OPTIMIZATION: For global var bindings, if the shape of the global object hasn't changed,
// we can use the cached property offset. // we can use the cached property offset.
if (&shape == cache.shape) { if (&shape == cache.entries[0].shape) {
auto value = binding_object.get_direct(cache.property_offset.value()); auto value = binding_object.get_direct(cache.entries[0].property_offset.value());
if (value.is_accessor()) if (value.is_accessor())
TRY(call(vm, value.as_accessor().setter(), &binding_object, src)); TRY(call(vm, value.as_accessor().setter(), &binding_object, src));
else else
binding_object.put_direct(cache.property_offset.value(), src); binding_object.put_direct(cache.entries[0].property_offset.value(), src);
return {}; return {};
} }
@ -2416,8 +2427,8 @@ ThrowCompletionOr<void> SetGlobal::execute_impl(Bytecode::Interpreter& interpret
return vm.throw_completion<TypeError>(ErrorType::ObjectSetReturnedFalse); return vm.throw_completion<TypeError>(ErrorType::ObjectSetReturnedFalse);
} }
if (cacheable_metadata.type == CacheablePropertyMetadata::Type::OwnProperty) { if (cacheable_metadata.type == CacheablePropertyMetadata::Type::OwnProperty) {
cache.shape = shape; cache.entries[0].shape = shape;
cache.property_offset = cacheable_metadata.property_offset.value(); cache.entries[0].property_offset = cacheable_metadata.property_offset.value();
} }
return {}; return {};
} }