LibJS: Cache PutById to setters in the prototype chain
Some checks are pending
CI / Lagom (x86_64, Fuzzers_CI, false, ubuntu-24.04, Linux, Clang) (push) Waiting to run
CI / Lagom (arm64, Sanitizer_CI, false, macos-15, macOS, Clang) (push) Waiting to run
CI / Lagom (x86_64, Sanitizer_CI, false, ubuntu-24.04, Linux, GNU) (push) Waiting to run
CI / Lagom (x86_64, Sanitizer_CI, true, ubuntu-24.04, Linux, Clang) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (arm64, macos-15, macOS, macOS-universal2) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (x86_64, ubuntu-24.04, Linux, Linux-x86_64) (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Label PRs with merge conflicts / auto-labeler (push) Waiting to run
Push notes / build (push) Waiting to run

This is *extremely* common on the web, but barely shows up at all in
JavaScript benchmarks.

A typical example is setting Element.innerHTML on a HTMLDivElement.
HTMLDivElement doesn't have innerHTML, so it has to travel up the
prototype chain until it finds it.

Before this change, we didn't cache this at all, so we had to travel
the prototype chain every time a setter like this was used.

We now use the same mechanism we already had for GetBydId and cache
PutById setter accesses in the prototype chain as well.

1.74x speedup on MicroBench/setter-in-prototype-chain.js
This commit is contained in:
Andreas Kling 2025-05-04 16:00:13 +02:00 committed by Andreas Kling
parent 71665fa504
commit 183c847c80
Notes: github-actions[bot] 2025-05-05 13:22:41 +00:00
18 changed files with 93 additions and 39 deletions

View file

@ -1224,20 +1224,54 @@ inline ThrowCompletionOr<void> put_by_property_key(VM& vm, Value base, Value thi
break;
}
case Op::PropertyKind::KeyValue: {
if (cache && cache->shape == &object->shape()) {
object->put_direct(*cache->property_offset, value);
return {};
if (cache) {
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 (&object->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_in_prototype = cache->prototype->get_direct(cache->property_offset.value());
if (value_in_prototype.is_accessor()) {
TRY(call(vm, value_in_prototype.as_accessor().setter(), this_value, value));
return {};
}
}
} else if (cache->shape == &object->shape()) {
auto value_in_object = object->get_direct(cache->property_offset.value());
if (value_in_object.is_accessor()) {
TRY(call(vm, value_in_object.as_accessor().setter(), this_value, value));
} else {
object->put_direct(*cache->property_offset, value);
}
return {};
}
}
CacheablePropertyMetadata cacheable_metadata;
bool succeeded = TRY(object->internal_set(name, value, this_value, &cacheable_metadata));
if (succeeded && cache && cacheable_metadata.type == CacheablePropertyMetadata::Type::OwnProperty) {
cache->shape = object->shape();
cache->property_offset = cacheable_metadata.property_offset.value();
if (succeeded && cache) {
if (cacheable_metadata.type == CacheablePropertyMetadata::Type::OwnProperty) {
*cache = {};
cache->shape = object->shape();
cache->property_offset = cacheable_metadata.property_offset.value();
} else if (cacheable_metadata.type == CacheablePropertyMetadata::Type::InPrototypeChain) {
*cache = {};
cache->shape = object->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();
}
}
if (!succeeded && vm.in_strict_mode()) {
if (!succeeded && vm.in_strict_mode()) [[unlikely]] {
if (base.is_object())
return vm.throw_completion<TypeError>(ErrorType::ReferenceNullishSetProperty, name, base.to_string_without_side_effects());
return vm.throw_completion<TypeError>(ErrorType::ReferencePrimitiveSetProperty, name, base.typeof_(vm)->utf8_string(), base.to_string_without_side_effects());
@ -2318,7 +2352,7 @@ ThrowCompletionOr<void> SetGlobal::execute_impl(Bytecode::Interpreter& interpret
if (&shape == cache.shape) {
auto value = binding_object.get_direct(cache.property_offset.value());
if (value.is_accessor())
TRY(call(vm, value.as_accessor().setter(), js_undefined(), value));
TRY(call(vm, value.as_accessor().setter(), &binding_object, src));
else
binding_object.put_direct(cache.property_offset.value(), src);
return {};