فهرست منبع

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 :^)
Andreas Kling 2 سال پیش
والد
کامیت
de8e4b1853

+ 10 - 10
Userland/Libraries/LibJS/Bytecode/ASTCodegen.cpp

@@ -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 {};
             },

+ 8 - 1
Userland/Libraries/LibJS/Bytecode/Executable.h

@@ -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;

+ 16 - 2
Userland/Libraries/LibJS/Bytecode/Generator.cpp

@@ -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++);
+}
+
 }

+ 4 - 0
Userland/Libraries/LibJS/Bytecode/Generator.h

@@ -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;

+ 1 - 1
Userland/Libraries/LibJS/Bytecode/Interpreter.cpp

@@ -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);
 

+ 5 - 4
Userland/Libraries/LibJS/Bytecode/Interpreter.h

@@ -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 };

+ 26 - 6
Userland/Libraries/LibJS/Bytecode/Op.cpp

@@ -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

+ 6 - 2
Userland/Libraries/LibJS/Bytecode/Op.h

@@ -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 {