LibJS/Bytecode: Cache object own property accesses

The instructions GetById and GetByIdWithThis now remember the last-seen
Shape, and if we see the same object again, we reuse the property offset
from last time without doing a new lookup.

This allows us to use Object::get_direct(), bypassing the entire lookup
machinery and saving lots of time.

~23% speed-up on Kraken/ai-astar.js :^)
This commit is contained in:
Andreas Kling 2023-07-08 17:43:26 +02:00
parent 52cd671163
commit de8e4b1853
Notes: sideshowbarker 2024-07-16 23:34:44 +09:00
8 changed files with 76 additions and 26 deletions

View file

@ -1120,7 +1120,7 @@ static Bytecode::CodeGenerationErrorOr<void> generate_object_binding_pattern_byt
}
generator.emit<Bytecode::Op::Load>(value_reg);
generator.emit<Bytecode::Op::GetById>(generator.intern_identifier(identifier));
generator.emit_get_by_id(generator.intern_identifier(identifier));
} else {
auto expression = name.get<NonnullRefPtr<Expression const>>();
TRY(expression->generate_bytecode(generator));
@ -1474,7 +1474,7 @@ static Bytecode::CodeGenerationErrorOr<void> get_base_and_value_from_member_expr
} else {
// 3. Let propertyKey be StringValue of IdentifierName.
auto identifier_table_ref = generator.intern_identifier(verify_cast<Identifier>(member_expression.property()).string());
generator.emit<Bytecode::Op::GetByIdWithThis>(identifier_table_ref, this_reg);
generator.emit_get_by_id_with_this(identifier_table_ref, this_reg);
}
} else {
TRY(member_expression.object().generate_bytecode(generator));
@ -1485,7 +1485,7 @@ static Bytecode::CodeGenerationErrorOr<void> get_base_and_value_from_member_expr
} else if (is<PrivateIdentifier>(member_expression.property())) {
generator.emit<Bytecode::Op::GetPrivateById>(generator.intern_identifier(verify_cast<PrivateIdentifier>(member_expression.property()).string()));
} else {
generator.emit<Bytecode::Op::GetById>(generator.intern_identifier(verify_cast<Identifier>(member_expression.property()).string()));
generator.emit_get_by_id(generator.intern_identifier(verify_cast<Identifier>(member_expression.property()).string()));
}
}
@ -1587,11 +1587,11 @@ Bytecode::CodeGenerationErrorOr<void> YieldExpression::generate_bytecode(Bytecod
// The accumulator is set to an object, for example: { "type": 1 (normal), value: 1337 }
generator.emit<Bytecode::Op::Store>(received_completion_register);
generator.emit<Bytecode::Op::GetById>(type_identifier);
generator.emit_get_by_id(type_identifier);
generator.emit<Bytecode::Op::Store>(received_completion_type_register);
generator.emit<Bytecode::Op::Load>(received_completion_register);
generator.emit<Bytecode::Op::GetById>(value_identifier);
generator.emit_get_by_id(value_identifier);
generator.emit<Bytecode::Op::Store>(received_completion_value_register);
};
@ -1613,14 +1613,14 @@ Bytecode::CodeGenerationErrorOr<void> YieldExpression::generate_bytecode(Bytecod
// 5. Let iterator be iteratorRecord.[[Iterator]].
auto iterator_register = generator.allocate_register();
auto iterator_identifier = generator.intern_identifier("iterator");
generator.emit<Bytecode::Op::GetById>(iterator_identifier);
generator.emit_get_by_id(iterator_identifier);
generator.emit<Bytecode::Op::Store>(iterator_register);
// Cache iteratorRecord.[[NextMethod]] for use in step 7.a.i.
auto next_method_register = generator.allocate_register();
auto next_method_identifier = generator.intern_identifier("next");
generator.emit<Bytecode::Op::Load>(iterator_record_register);
generator.emit<Bytecode::Op::GetById>(next_method_identifier);
generator.emit_get_by_id(next_method_identifier);
generator.emit<Bytecode::Op::Store>(next_method_register);
// 6. Let received be NormalCompletion(undefined).
@ -2424,11 +2424,11 @@ static void generate_await(Bytecode::Generator& generator)
// The accumulator is set to an object, for example: { "type": 1 (normal), value: 1337 }
generator.emit<Bytecode::Op::Store>(received_completion_register);
generator.emit<Bytecode::Op::GetById>(type_identifier);
generator.emit_get_by_id(type_identifier);
generator.emit<Bytecode::Op::Store>(received_completion_type_register);
generator.emit<Bytecode::Op::Load>(received_completion_register);
generator.emit<Bytecode::Op::GetById>(value_identifier);
generator.emit_get_by_id(value_identifier);
generator.emit<Bytecode::Op::Store>(received_completion_value_register);
auto& normal_completion_continuation_block = generator.make_block();
@ -2923,7 +2923,7 @@ static Bytecode::CodeGenerationErrorOr<void> generate_optional_chain(Bytecode::G
},
[&](OptionalChain::MemberReference const& ref) -> Bytecode::CodeGenerationErrorOr<void> {
generator.emit<Bytecode::Op::Store>(current_base_register);
generator.emit<Bytecode::Op::GetById>(generator.intern_identifier(ref.identifier->string()));
generator.emit_get_by_id(generator.intern_identifier(ref.identifier->string()));
generator.emit<Bytecode::Op::Store>(current_value_register);
return {};
},

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021-2023, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -8,14 +8,21 @@
#include <AK/DeprecatedFlyString.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/WeakPtr.h>
#include <LibJS/Bytecode/BasicBlock.h>
#include <LibJS/Bytecode/IdentifierTable.h>
#include <LibJS/Bytecode/StringTable.h>
namespace JS::Bytecode {
struct PropertyLookupCache {
WeakPtr<Shape> shape;
Optional<u32> property_offset;
};
struct Executable {
DeprecatedFlyString name;
Vector<PropertyLookupCache> property_lookup_caches;
Vector<NonnullOwnPtr<BasicBlock>> basic_blocks;
NonnullOwnPtr<StringTable> string_table;
NonnullOwnPtr<IdentifierTable> identifier_table;

View file

@ -54,8 +54,12 @@ CodeGenerationErrorOr<NonnullOwnPtr<Executable>> Generator::generate(ASTNode con
else if (is<FunctionExpression>(node))
is_strict_mode = static_cast<FunctionExpression const&>(node).is_strict_mode();
Vector<PropertyLookupCache> property_lookup_caches;
property_lookup_caches.resize(generator.m_next_property_lookup_cache);
return adopt_own(*new Executable {
.name = {},
.property_lookup_caches = move(property_lookup_caches),
.basic_blocks = move(generator.m_root_basic_blocks),
.string_table = move(generator.m_string_table),
.identifier_table = move(generator.m_identifier_table),
@ -199,7 +203,7 @@ CodeGenerationErrorOr<void> Generator::emit_load_from_reference(JS::ASTNode cons
} else {
// 3. Let propertyKey be StringValue of IdentifierName.
auto identifier_table_ref = intern_identifier(verify_cast<Identifier>(expression.property()).string());
emit<Bytecode::Op::GetByIdWithThis>(identifier_table_ref, super_reference.this_value);
emit_get_by_id_with_this(identifier_table_ref, super_reference.this_value);
}
} else {
TRY(expression.object().generate_bytecode(*this));
@ -212,7 +216,7 @@ CodeGenerationErrorOr<void> Generator::emit_load_from_reference(JS::ASTNode cons
emit<Bytecode::Op::GetByValue>(object_reg);
} else if (expression.property().is_identifier()) {
auto identifier_table_ref = intern_identifier(verify_cast<Identifier>(expression.property()).string());
emit<Bytecode::Op::GetById>(identifier_table_ref);
emit_get_by_id(identifier_table_ref);
} else if (expression.property().is_private_identifier()) {
auto identifier_table_ref = intern_identifier(verify_cast<PrivateIdentifier>(expression.property()).string());
emit<Bytecode::Op::GetPrivateById>(identifier_table_ref);
@ -543,4 +547,14 @@ CodeGenerationErrorOr<void> Generator::emit_named_evaluation_if_anonymous_functi
return {};
}
void Generator::emit_get_by_id(IdentifierTableIndex id)
{
emit<Op::GetById>(id, m_next_property_lookup_cache++);
}
void Generator::emit_get_by_id_with_this(IdentifierTableIndex id, Register this_reg)
{
emit<Op::GetByIdWithThis>(id, this_reg, m_next_property_lookup_cache++);
}
}

View file

@ -199,6 +199,9 @@ public:
m_boundaries.take_last();
}
void emit_get_by_id(IdentifierTableIndex);
void emit_get_by_id_with_this(IdentifierTableIndex, Register);
private:
Generator();
~Generator() = default;
@ -218,6 +221,7 @@ private:
u32 m_next_register { 2 };
u32 m_next_block { 1 };
u32 m_next_property_lookup_cache { 0 };
FunctionKind m_enclosing_function_kind { FunctionKind::Normal };
Vector<LabelableScope> m_continuable_scopes;
Vector<LabelableScope> m_breakable_scopes;

View file

@ -193,7 +193,7 @@ ThrowCompletionOr<Value> Interpreter::run(SourceTextModule& module)
return js_undefined();
}
Interpreter::ValueAndFrame Interpreter::run_and_return_frame(Realm& realm, Executable const& executable, BasicBlock const* entry_point, RegisterWindow* in_frame)
Interpreter::ValueAndFrame Interpreter::run_and_return_frame(Realm& realm, Executable& executable, BasicBlock const* entry_point, RegisterWindow* in_frame)
{
dbgln_if(JS_BYTECODE_DEBUG, "Bytecode::Interpreter will run unit {:p}", &executable);

View file

@ -50,7 +50,7 @@ public:
ThrowCompletionOr<Value> run(Script&, JS::GCPtr<Environment> lexical_environment_override = nullptr);
ThrowCompletionOr<Value> run(SourceTextModule&);
ThrowCompletionOr<Value> run(Realm& realm, Bytecode::Executable const& executable, Bytecode::BasicBlock const* entry_point = nullptr)
ThrowCompletionOr<Value> run(Realm& realm, Bytecode::Executable& executable, Bytecode::BasicBlock const* entry_point = nullptr)
{
auto value_and_frame = run_and_return_frame(realm, executable, entry_point);
return move(value_and_frame.value);
@ -60,7 +60,7 @@ public:
ThrowCompletionOr<Value> value;
OwnPtr<RegisterWindow> frame;
};
ValueAndFrame run_and_return_frame(Realm&, Bytecode::Executable const&, Bytecode::BasicBlock const* entry_point, RegisterWindow* = nullptr);
ValueAndFrame run_and_return_frame(Realm&, Bytecode::Executable&, Bytecode::BasicBlock const* entry_point, RegisterWindow* = nullptr);
ALWAYS_INLINE Value& accumulator() { return reg(Register::accumulator()); }
Value& reg(Register const& r) { return registers()[r.index()]; }
@ -88,7 +88,8 @@ public:
void leave_unwind_context();
ThrowCompletionOr<void> continue_pending_unwind(Label const& resume_label);
Executable const& current_executable() { return *m_current_executable; }
Executable& current_executable() { return *m_current_executable; }
Executable const& current_executable() const { return *m_current_executable; }
BasicBlock const& current_block() const { return *m_current_block; }
size_t pc() const;
DeprecatedString debug_position() const;
@ -124,7 +125,7 @@ private:
Optional<Value> m_return_value;
Optional<Value> m_saved_return_value;
Optional<Value> m_saved_exception;
Executable const* m_current_executable { nullptr };
Executable* m_current_executable { nullptr };
OwnPtr<JS::Interpreter> m_ast_interpreter;
BasicBlock const* m_current_block { nullptr };
InstructionStreamIterator* m_pc { nullptr };

View file

@ -515,15 +515,19 @@ ThrowCompletionOr<void> SetLocal::execute_impl(Bytecode::Interpreter& interprete
return {};
}
static ThrowCompletionOr<void> get_by_id(Bytecode::Interpreter& interpreter, IdentifierTableIndex property, Value base_value, Value this_value)
static ThrowCompletionOr<void> get_by_id(Bytecode::Interpreter& interpreter, IdentifierTableIndex property, Value base_value, Value this_value, u32 cache_index)
{
auto& vm = interpreter.vm();
auto const& name = interpreter.current_executable().get_identifier(property);
auto& cache = interpreter.current_executable().property_lookup_caches[cache_index];
// OPTIMIZATION: For various primitives we can avoid actually creating a new object for them.
GCPtr<Object> base_obj;
if (base_value.is_string()) {
if (base_value.is_object()) {
// This would be covered by the `else` branch below,
// but let's avoid all the extra checks if it's already an object.
base_obj = base_value.as_object();
} else if (base_value.is_string()) {
auto string_value = TRY(base_value.as_string().get(vm, name));
if (string_value.has_value()) {
interpreter.accumulator() = *string_value;
@ -538,21 +542,37 @@ static ThrowCompletionOr<void> get_by_id(Bytecode::Interpreter& interpreter, Ide
base_obj = TRY(base_value.to_object(vm));
}
interpreter.accumulator() = TRY(base_obj->internal_get(name, this_value));
// OPTIMIZATION: If the shape of the object hasn't changed, we can use the cached property offset.
if (&base_obj->shape() == cache.shape) {
interpreter.accumulator() = base_obj->get_direct(cache.property_offset.value());
return {};
}
CacheablePropertyMetadata cacheable_metadata;
interpreter.accumulator() = TRY(base_obj->internal_get(name, this_value, &cacheable_metadata));
if (cacheable_metadata.type == CacheablePropertyMetadata::Type::OwnProperty) {
cache.shape = &base_obj->shape();
cache.property_offset = cacheable_metadata.property_offset.value();
} else {
cache.shape = nullptr;
cache.property_offset = {};
}
return {};
}
ThrowCompletionOr<void> GetById::execute_impl(Bytecode::Interpreter& interpreter) const
{
auto base_value = interpreter.accumulator();
return get_by_id(interpreter, m_property, base_value, base_value);
return get_by_id(interpreter, m_property, base_value, base_value, m_cache_index);
}
ThrowCompletionOr<void> GetByIdWithThis::execute_impl(Bytecode::Interpreter& interpreter) const
{
auto base_value = interpreter.accumulator();
auto this_value = interpreter.reg(m_this_value);
return get_by_id(interpreter, m_property, base_value, this_value);
return get_by_id(interpreter, m_property, base_value, this_value, m_cache_index);
}
ThrowCompletionOr<void> GetPrivateById::execute_impl(Bytecode::Interpreter& interpreter) const

View file

@ -564,9 +564,10 @@ private:
class GetById final : public Instruction {
public:
explicit GetById(IdentifierTableIndex property)
GetById(IdentifierTableIndex property, u32 cache_index)
: Instruction(Type::GetById)
, m_property(property)
, m_cache_index(cache_index)
{
}
@ -577,14 +578,16 @@ public:
private:
IdentifierTableIndex m_property;
u32 m_cache_index { 0 };
};
class GetByIdWithThis final : public Instruction {
public:
GetByIdWithThis(IdentifierTableIndex property, Register this_value)
GetByIdWithThis(IdentifierTableIndex property, Register this_value, u32 cache_index)
: Instruction(Type::GetByIdWithThis)
, m_property(property)
, m_this_value(this_value)
, m_cache_index(cache_index)
{
}
@ -600,6 +603,7 @@ public:
private:
IdentifierTableIndex m_property;
Register m_this_value;
u32 m_cache_index { 0 };
};
class GetPrivateById final : public Instruction {