From 7654da3851faf847a46b05e00e0b969d063eec88 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Thu, 9 May 2024 15:13:31 +0200 Subject: [PATCH] LibJS/Bytecode: Do basic compare-and-jump peephole optimization We now fuse sequences like [LessThan, JumpIf] to JumpLessThan. This is only allowed for temporaries (i.e VM registers) with no other references to them. --- .../Libraries/LibJS/Bytecode/ASTCodegen.cpp | 53 +++++++++---------- .../Libraries/LibJS/Bytecode/BasicBlock.h | 11 ++++ .../Libraries/LibJS/Bytecode/Generator.cpp | 34 ++++++++++++ Userland/Libraries/LibJS/Bytecode/Generator.h | 7 +++ .../Libraries/LibJS/Bytecode/Instruction.h | 8 +++ .../Libraries/LibJS/Bytecode/Interpreter.cpp | 33 ++++++++++++ Userland/Libraries/LibJS/Bytecode/Op.h | 46 ++++++++++++++++ .../Libraries/LibJS/Bytecode/ScopedOperand.h | 2 + 8 files changed, 166 insertions(+), 28 deletions(-) diff --git a/Userland/Libraries/LibJS/Bytecode/ASTCodegen.cpp b/Userland/Libraries/LibJS/Bytecode/ASTCodegen.cpp index a6f70b0c533..2f80debbf18 100644 --- a/Userland/Libraries/LibJS/Bytecode/ASTCodegen.cpp +++ b/Userland/Libraries/LibJS/Bytecode/ASTCodegen.cpp @@ -186,13 +186,13 @@ Bytecode::CodeGenerationErrorOr> LogicalExpression::gene switch (m_op) { case LogicalOp::And: - generator.emit( + generator.emit_jump_if( lhs, Bytecode::Label { rhs_block }, Bytecode::Label { end_block }); break; case LogicalOp::Or: - generator.emit( + generator.emit_jump_if( lhs, Bytecode::Label { end_block }, Bytecode::Label { rhs_block }); @@ -549,7 +549,7 @@ Bytecode::CodeGenerationErrorOr> AssignmentExpression::g lhs_block_ptr = &generator.make_block(); end_block_ptr = &generator.make_block(); - generator.emit( + generator.emit_jump_if( lhs, Bytecode::Label { *rhs_block_ptr }, Bytecode::Label { *lhs_block_ptr }); @@ -558,7 +558,7 @@ Bytecode::CodeGenerationErrorOr> AssignmentExpression::g lhs_block_ptr = &generator.make_block(); end_block_ptr = &generator.make_block(); - generator.emit( + generator.emit_jump_if( lhs, Bytecode::Label { *lhs_block_ptr }, Bytecode::Label { *rhs_block_ptr }); @@ -748,7 +748,7 @@ Bytecode::CodeGenerationErrorOr> WhileStatement::generat generator.switch_to_basic_block(test_block); auto test = TRY(m_test->generate_bytecode(generator)).value(); - generator.emit( + generator.emit_jump_if( test, Bytecode::Label { body_block }, Bytecode::Label { end_block }); @@ -798,7 +798,7 @@ Bytecode::CodeGenerationErrorOr> DoWhileStatement::gener generator.switch_to_basic_block(test_block); auto test = TRY(m_test->generate_bytecode(generator)).value(); - generator.emit( + generator.emit_jump_if( test, Bytecode::Label { body_block }, Bytecode::Label { load_result_and_jump_to_end_block }); @@ -901,10 +901,7 @@ Bytecode::CodeGenerationErrorOr> ForStatement::generate_ generator.switch_to_basic_block(*test_block_ptr); auto test = TRY(m_test->generate_bytecode(generator)).value(); - generator.emit( - test, - Bytecode::Label { *body_block_ptr }, - Bytecode::Label { end_block }); + generator.emit_jump_if(test, Bytecode::Label { *body_block_ptr }, Bytecode::Label { end_block }); } if (m_update) { @@ -1298,7 +1295,7 @@ static Bytecode::CodeGenerationErrorOr generate_array_binding_pattern_byte auto& if_not_exhausted_block = generator.make_block(); auto& continuation_block = generator.make_block(); - generator.emit( + generator.emit_jump_if( is_iterator_exhausted, Bytecode::Label { if_exhausted_block }, Bytecode::Label { if_not_exhausted_block }); @@ -1324,7 +1321,7 @@ static Bytecode::CodeGenerationErrorOr generate_array_binding_pattern_byte if (!first) { auto& iterator_is_not_exhausted_block = generator.make_block(); - generator.emit( + generator.emit_jump_if( is_iterator_exhausted, Bytecode::Label { iterator_is_exhausted_block }, Bytecode::Label { iterator_is_not_exhausted_block }); @@ -1338,7 +1335,7 @@ static Bytecode::CodeGenerationErrorOr generate_array_binding_pattern_byte // We still have to check for exhaustion here. If the iterator is exhausted, // we need to bail before trying to get the value auto& no_bail_block = generator.make_block(); - generator.emit( + generator.emit_jump_if( is_iterator_exhausted, Bytecode::Label { iterator_is_exhausted_block }, Bytecode::Label { no_bail_block }); @@ -1392,7 +1389,7 @@ static Bytecode::CodeGenerationErrorOr generate_array_binding_pattern_byte auto& done_block = generator.make_block(); auto& not_done_block = generator.make_block(); - generator.emit( + generator.emit_jump_if( is_iterator_exhausted, Bytecode::Label { done_block }, Bytecode::Label { not_done_block }); @@ -1740,7 +1737,7 @@ static void generate_yield(Bytecode::Generator& generator, resumption_value_type_is_not_return_result, received_completion_type, generator.add_constant(Value(to_underlying(Completion::Type::Return)))); - generator.emit( + generator.emit_jump_if( resumption_value_type_is_not_return_result, Bytecode::Label { continuation_label }, Bytecode::Label { resumption_value_type_is_return_block }); @@ -1757,7 +1754,7 @@ static void generate_yield(Bytecode::Generator& generator, awaited_type_is_throw_result, received_completion_type, generator.add_constant(Value(to_underlying(Completion::Type::Throw)))); - generator.emit( + generator.emit_jump_if( awaited_type_is_throw_result, Bytecode::Label { continuation_label }, Bytecode::Label { awaited_type_is_normal_block }); @@ -1839,7 +1836,7 @@ Bytecode::CodeGenerationErrorOr> YieldExpression::genera received_completion_type_register_is_normal, received_completion_type, generator.add_constant(Value(to_underlying(Completion::Type::Normal)))); - generator.emit( + generator.emit_jump_if( received_completion_type_register_is_normal, Bytecode::Label { type_is_normal_block }, Bytecode::Label { is_type_throw_block }); @@ -1868,7 +1865,7 @@ Bytecode::CodeGenerationErrorOr> YieldExpression::genera // v. If done is true, then auto& type_is_normal_done_block = generator.make_block(); auto& type_is_normal_not_done_block = generator.make_block(); - generator.emit( + generator.emit_jump_if( done, Bytecode::Label { type_is_normal_done_block }, Bytecode::Label { type_is_normal_not_done_block }); @@ -1917,7 +1914,7 @@ Bytecode::CodeGenerationErrorOr> YieldExpression::genera received_completion_type_register_is_throw, received_completion_type, generator.add_constant(Value(to_underlying(Completion::Type::Throw)))); - generator.emit( + generator.emit_jump_if( received_completion_type_register_is_throw, Bytecode::Label { type_is_throw_block }, Bytecode::Label { type_is_return_block }); @@ -1959,7 +1956,7 @@ Bytecode::CodeGenerationErrorOr> YieldExpression::genera // 6. If done is true, then auto& type_is_throw_done_block = generator.make_block(); auto& type_is_throw_not_done_block = generator.make_block(); - generator.emit( + generator.emit_jump_if( done, Bytecode::Label { type_is_throw_done_block }, Bytecode::Label { type_is_throw_not_done_block }); @@ -2056,7 +2053,7 @@ Bytecode::CodeGenerationErrorOr> YieldExpression::genera // viii. If done is true, then auto& type_is_return_done_block = generator.make_block(); auto& type_is_return_not_done_block = generator.make_block(); - generator.emit( + generator.emit_jump_if( done, Bytecode::Label { type_is_return_done_block }, Bytecode::Label { type_is_return_not_done_block }); @@ -2126,7 +2123,7 @@ Bytecode::CodeGenerationErrorOr> YieldExpression::genera received_completion_type_is_normal, received_completion_type, generator.add_constant(Value(to_underlying(Completion::Type::Normal)))); - generator.emit( + generator.emit_jump_if( received_completion_type_is_normal, Bytecode::Label { normal_completion_continuation_block }, Bytecode::Label { throw_completion_continuation_block }); @@ -2142,7 +2139,7 @@ Bytecode::CodeGenerationErrorOr> YieldExpression::genera generator.add_constant(Value(to_underlying(Completion::Type::Throw)))); // If type is not equal to "throw" or "normal", assume it's "return". - generator.emit( + generator.emit_jump_if( received_completion_type_is_throw, Bytecode::Label { throw_value_block }, Bytecode::Label { return_value_block }); @@ -2178,7 +2175,7 @@ Bytecode::CodeGenerationErrorOr> IfStatement::generate_b generator.emit(dst, generator.add_constant(js_undefined())); auto predicate = TRY(m_predicate->generate_bytecode(generator)).value(); - generator.emit( + generator.emit_jump_if( predicate, Bytecode::Label { true_block }, Bytecode::Label { false_block }); @@ -2241,7 +2238,7 @@ Bytecode::CodeGenerationErrorOr> ConditionalExpression:: auto& end_block = generator.make_block(); auto test = TRY(m_test->generate_bytecode(generator)).value(); - generator.emit( + generator.emit_jump_if( test, Bytecode::Label { true_block }, Bytecode::Label { false_block }); @@ -2601,7 +2598,7 @@ Bytecode::CodeGenerationErrorOr> SwitchStatement::genera auto result = generator.allocate_register(); generator.emit(result, test_value, discriminant); next_test_block = test_blocks.dequeue(); - generator.emit( + generator.emit_jump_if( result, Bytecode::Label { case_block }, Bytecode::Label { *next_test_block }); @@ -2738,7 +2735,7 @@ static ScopedOperand generate_await( received_completion_type_is_normal, received_completion_type, generator.add_constant(Value(to_underlying(Completion::Type::Normal)))); - generator.emit( + generator.emit_jump_if( received_completion_type_is_normal, Bytecode::Label { normal_completion_continuation_block }, Bytecode::Label { throw_value_block }); @@ -2975,7 +2972,7 @@ static Bytecode::CodeGenerationErrorOr> for_in_of_body_e // e. If done is true, return V. auto& loop_continue = generator.make_block(); - generator.emit( + generator.emit_jump_if( done, Bytecode::Label { loop_end }, Bytecode::Label { loop_continue }); diff --git a/Userland/Libraries/LibJS/Bytecode/BasicBlock.h b/Userland/Libraries/LibJS/Bytecode/BasicBlock.h index 0596da01df2..166dbf69a25 100644 --- a/Userland/Libraries/LibJS/Bytecode/BasicBlock.h +++ b/Userland/Libraries/LibJS/Bytecode/BasicBlock.h @@ -36,6 +36,12 @@ public: u8 const* data() const { return m_buffer.data(); } size_t size() const { return m_buffer.size(); } + void rewind() + { + m_buffer.resize_and_keep_capacity(m_last_instruction_start_offset); + m_terminated = false; + } + void grow(size_t additional_size); void terminate(Badge) { m_terminated = true; } @@ -55,6 +61,9 @@ public: auto const& this_() const { return m_this; } void set_this(ScopedOperand operand) { m_this = operand; } + [[nodiscard]] size_t last_instruction_start_offset() const { return m_last_instruction_start_offset; } + void set_last_instruction_start_offset(size_t offset) { m_last_instruction_start_offset = offset; } + private: explicit BasicBlock(u32 index, String name); @@ -68,6 +77,8 @@ private: HashMap m_source_map; Optional m_this; + + size_t m_last_instruction_start_offset { 0 }; }; } diff --git a/Userland/Libraries/LibJS/Bytecode/Generator.cpp b/Userland/Libraries/LibJS/Bytecode/Generator.cpp index 696d54ba553..a6a8c2976d3 100644 --- a/Userland/Libraries/LibJS/Bytecode/Generator.cpp +++ b/Userland/Libraries/LibJS/Bytecode/Generator.cpp @@ -833,4 +833,38 @@ ScopedOperand Generator::accumulator() return m_accumulator; } +bool Generator::fuse_compare_and_jump(ScopedOperand const& condition, Label true_target, Label false_target) +{ + auto& last_instruction = *reinterpret_cast(m_current_basic_block->data() + m_current_basic_block->last_instruction_start_offset()); + +#define HANDLE_COMPARISON_OP(op_TitleCase, op_snake_case) \ + if (last_instruction.type() == Instruction::Type::op_TitleCase) { \ + auto& comparison = static_cast(last_instruction); \ + VERIFY(comparison.dst() == condition); \ + auto lhs = comparison.lhs(); \ + auto rhs = comparison.rhs(); \ + m_current_basic_block->rewind(); \ + emit(lhs, rhs, true_target, false_target); \ + return true; \ + } + + JS_ENUMERATE_COMPARISON_OPS(HANDLE_COMPARISON_OP); +#undef HANDLE_COMPARISON_OP + + return false; +} + +void Generator::emit_jump_if(ScopedOperand const& condition, Label true_target, Label false_target) +{ + // NOTE: It's only safe to fuse compare-and-jump if the condition is a temporary with no other dependents. + if (condition.operand().is_register() + && condition.ref_count() == 1 + && m_current_basic_block->size() > 0) { + if (fuse_compare_and_jump(condition, true_target, false_target)) + return; + } + + emit(condition, true_target, false_target); +} + } diff --git a/Userland/Libraries/LibJS/Bytecode/Generator.h b/Userland/Libraries/LibJS/Bytecode/Generator.h index 5002ab3ef5f..eb632ff4005 100644 --- a/Userland/Libraries/LibJS/Bytecode/Generator.h +++ b/Userland/Libraries/LibJS/Bytecode/Generator.h @@ -77,6 +77,7 @@ public: { VERIFY(!is_current_block_terminated()); size_t slot_offset = m_current_basic_block->size(); + m_current_basic_block->set_last_instruction_start_offset(slot_offset); grow(sizeof(OpType)); void* slot = m_current_basic_block->data() + slot_offset; new (slot) OpType(forward(args)...); @@ -93,6 +94,7 @@ public: size_t size_to_allocate = round_up_to_power_of_two(sizeof(OpType) + extra_slot_count * sizeof(ExtraSlotType), alignof(void*)); size_t slot_offset = m_current_basic_block->size(); + m_current_basic_block->set_last_instruction_start_offset(slot_offset); grow(size_to_allocate); void* slot = m_current_basic_block->data() + slot_offset; new (slot) OpType(forward(args)...); @@ -115,6 +117,8 @@ public: emit_with_extra_slots(extra_operand_slots, forward(args)...); } + void emit_jump_if(ScopedOperand const& condition, Label true_target, Label false_target); + struct ReferenceOperands { Optional base {}; // [[Base]] Optional referenced_name {}; // [[ReferencedName]] as an operand @@ -309,6 +313,9 @@ private: void grow(size_t); + // Returns true if a fused instruction was emitted. + [[nodiscard]] bool fuse_compare_and_jump(ScopedOperand const& condition, Label true_target, Label false_target); + struct LabelableScope { Label bytecode_target; Vector language_label_set; diff --git a/Userland/Libraries/LibJS/Bytecode/Instruction.h b/Userland/Libraries/LibJS/Bytecode/Instruction.h index 87384fd031c..4e33356e54b 100644 --- a/Userland/Libraries/LibJS/Bytecode/Instruction.h +++ b/Userland/Libraries/LibJS/Bytecode/Instruction.h @@ -70,8 +70,16 @@ O(IteratorToArray) \ O(Jump) \ O(JumpFalse) \ + O(JumpGreaterThan) \ + O(JumpGreaterThanEquals) \ O(JumpIf) \ + O(JumpLessThan) \ + O(JumpLessThanEquals) \ + O(JumpLooselyEquals) \ + O(JumpLooselyInequals) \ O(JumpNullish) \ + O(JumpStrictlyEquals) \ + O(JumpStrictlyInequals) \ O(JumpTrue) \ O(JumpUndefined) \ O(LeaveFinally) \ diff --git a/Userland/Libraries/LibJS/Bytecode/Interpreter.cpp b/Userland/Libraries/LibJS/Bytecode/Interpreter.cpp index 8046973a253..364efbf08a3 100644 --- a/Userland/Libraries/LibJS/Bytecode/Interpreter.cpp +++ b/Userland/Libraries/LibJS/Bytecode/Interpreter.cpp @@ -362,6 +362,7 @@ FLATTEN_ON_CLANG void Interpreter::run_bytecode(size_t entry_point) #define SET_UP_LABEL(name) &&handle_##name, ENUMERATE_BYTECODE_OPS(SET_UP_LABEL) }; +#undef SET_UP_LABEL #define DISPATCH_NEXT(name) \ do { \ @@ -441,6 +442,26 @@ FLATTEN_ON_CLANG void Interpreter::run_bytecode(size_t entry_point) goto start; } +#define HANDLE_COMPARISON_OP(op_TitleCase, op_snake_case) \ + handle_Jump##op_TitleCase: \ + { \ + auto& instruction = *reinterpret_cast(&bytecode[program_counter]); \ + auto result = op_snake_case(vm(), get(instruction.lhs()), get(instruction.rhs())); \ + if (result.is_error()) { \ + if (handle_exception(program_counter, *result.throw_completion().value()) == HandleExceptionResponse::ExitFromExecutable) \ + return; \ + goto start; \ + } \ + if (result.value().to_boolean()) \ + program_counter = instruction.true_target().address(); \ + else \ + program_counter = instruction.false_target().address(); \ + goto start; \ + } + + JS_ENUMERATE_COMPARISON_OPS(HANDLE_COMPARISON_OP) +#undef HANDLE_COMPARISON_OP + handle_JumpUndefined: { auto& instruction = *reinterpret_cast(&bytecode[program_counter]); if (get(instruction.condition()).is_undefined()) @@ -2142,6 +2163,18 @@ ByteString JumpNullish::to_byte_string_impl(Bytecode::Executable const& executab m_false_target); } +#define HANDLE_COMPARISON_OP(op_TitleCase, op_snake_case) \ + ByteString Jump##op_TitleCase::to_byte_string_impl(Bytecode::Executable const& executable) const \ + { \ + return ByteString::formatted("Jump" #op_TitleCase " {}, {}, true:{}, false:{}", \ + format_operand("lhs"sv, m_lhs, executable), \ + format_operand("rhs"sv, m_rhs, executable), \ + m_true_target, \ + m_false_target); \ + } + +JS_ENUMERATE_COMPARISON_OPS(HANDLE_COMPARISON_OP) + ByteString JumpUndefined::to_byte_string_impl(Bytecode::Executable const& executable) const { return ByteString::formatted("JumpUndefined {}, undefined:{} defined:{}", diff --git a/Userland/Libraries/LibJS/Bytecode/Op.h b/Userland/Libraries/LibJS/Bytecode/Op.h index df2d7f6539d..0cd92b19c04 100644 --- a/Userland/Libraries/LibJS/Bytecode/Op.h +++ b/Userland/Libraries/LibJS/Bytecode/Op.h @@ -1157,6 +1157,52 @@ private: Label m_target; }; +#define JS_ENUMERATE_COMPARISON_OPS(X) \ + X(LessThan, less_than) \ + X(LessThanEquals, less_than_equals) \ + X(GreaterThan, greater_than) \ + X(GreaterThanEquals, greater_than_equals) \ + X(LooselyEquals, loosely_equals) \ + X(LooselyInequals, loosely_inequals) \ + X(StrictlyEquals, strict_equals) \ + X(StrictlyInequals, strict_inequals) + +#define DECLARE_COMPARISON_OP(op_TitleCase, op_snake_case) \ + class Jump##op_TitleCase final : public Instruction { \ + public: \ + constexpr static bool IsTerminator = true; \ + \ + explicit Jump##op_TitleCase(Operand lhs, Operand rhs, Label true_target, Label false_target) \ + : Instruction(Type::Jump##op_TitleCase) \ + , m_lhs(lhs) \ + , m_rhs(rhs) \ + , m_true_target(true_target) \ + , m_false_target(false_target) \ + { \ + } \ + \ + ThrowCompletionOr execute_impl(Bytecode::Interpreter&) const; \ + ByteString to_byte_string_impl(Bytecode::Executable const&) const; \ + void visit_labels_impl(Function visitor) \ + { \ + visitor(m_true_target); \ + visitor(m_false_target); \ + } \ + \ + Operand lhs() const { return m_lhs; } \ + Operand rhs() const { return m_rhs; } \ + auto& true_target() const { return m_true_target; } \ + auto& false_target() const { return m_false_target; } \ + \ + private: \ + Operand m_lhs; \ + Operand m_rhs; \ + Label m_true_target; \ + Label m_false_target; \ + }; + +JS_ENUMERATE_COMPARISON_OPS(DECLARE_COMPARISON_OP) + class JumpNullish final : public Instruction { public: constexpr static bool IsTerminator = true; diff --git a/Userland/Libraries/LibJS/Bytecode/ScopedOperand.h b/Userland/Libraries/LibJS/Bytecode/ScopedOperand.h index a2f42a42a43..1dccec19467 100644 --- a/Userland/Libraries/LibJS/Bytecode/ScopedOperand.h +++ b/Userland/Libraries/LibJS/Bytecode/ScopedOperand.h @@ -40,6 +40,8 @@ public: [[nodiscard]] bool operator==(ScopedOperand const& other) const { return operand() == other.operand(); } + [[nodiscard]] size_t ref_count() const { return m_impl->ref_count(); } + private: NonnullRefPtr m_impl; };