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.
This commit is contained in:
Andreas Kling 2024-05-09 15:13:31 +02:00 committed by Alexander Kalenik
parent 9bcb0feb4d
commit 7654da3851
Notes: sideshowbarker 2024-07-18 00:41:35 +09:00
8 changed files with 166 additions and 28 deletions

View file

@ -186,13 +186,13 @@ Bytecode::CodeGenerationErrorOr<Optional<ScopedOperand>> LogicalExpression::gene
switch (m_op) {
case LogicalOp::And:
generator.emit<Bytecode::Op::JumpIf>(
generator.emit_jump_if(
lhs,
Bytecode::Label { rhs_block },
Bytecode::Label { end_block });
break;
case LogicalOp::Or:
generator.emit<Bytecode::Op::JumpIf>(
generator.emit_jump_if(
lhs,
Bytecode::Label { end_block },
Bytecode::Label { rhs_block });
@ -549,7 +549,7 @@ Bytecode::CodeGenerationErrorOr<Optional<ScopedOperand>> AssignmentExpression::g
lhs_block_ptr = &generator.make_block();
end_block_ptr = &generator.make_block();
generator.emit<Bytecode::Op::JumpIf>(
generator.emit_jump_if(
lhs,
Bytecode::Label { *rhs_block_ptr },
Bytecode::Label { *lhs_block_ptr });
@ -558,7 +558,7 @@ Bytecode::CodeGenerationErrorOr<Optional<ScopedOperand>> AssignmentExpression::g
lhs_block_ptr = &generator.make_block();
end_block_ptr = &generator.make_block();
generator.emit<Bytecode::Op::JumpIf>(
generator.emit_jump_if(
lhs,
Bytecode::Label { *lhs_block_ptr },
Bytecode::Label { *rhs_block_ptr });
@ -748,7 +748,7 @@ Bytecode::CodeGenerationErrorOr<Optional<ScopedOperand>> WhileStatement::generat
generator.switch_to_basic_block(test_block);
auto test = TRY(m_test->generate_bytecode(generator)).value();
generator.emit<Bytecode::Op::JumpIf>(
generator.emit_jump_if(
test,
Bytecode::Label { body_block },
Bytecode::Label { end_block });
@ -798,7 +798,7 @@ Bytecode::CodeGenerationErrorOr<Optional<ScopedOperand>> DoWhileStatement::gener
generator.switch_to_basic_block(test_block);
auto test = TRY(m_test->generate_bytecode(generator)).value();
generator.emit<Bytecode::Op::JumpIf>(
generator.emit_jump_if(
test,
Bytecode::Label { body_block },
Bytecode::Label { load_result_and_jump_to_end_block });
@ -901,10 +901,7 @@ Bytecode::CodeGenerationErrorOr<Optional<ScopedOperand>> ForStatement::generate_
generator.switch_to_basic_block(*test_block_ptr);
auto test = TRY(m_test->generate_bytecode(generator)).value();
generator.emit<Bytecode::Op::JumpIf>(
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<void> generate_array_binding_pattern_byte
auto& if_not_exhausted_block = generator.make_block();
auto& continuation_block = generator.make_block();
generator.emit<Bytecode::Op::JumpIf>(
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<void> generate_array_binding_pattern_byte
if (!first) {
auto& iterator_is_not_exhausted_block = generator.make_block();
generator.emit<Bytecode::Op::JumpIf>(
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<void> 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<Bytecode::Op::JumpIf>(
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<void> generate_array_binding_pattern_byte
auto& done_block = generator.make_block();
auto& not_done_block = generator.make_block();
generator.emit<Bytecode::Op::JumpIf>(
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<Bytecode::Op::JumpIf>(
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<Bytecode::Op::JumpIf>(
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<Optional<ScopedOperand>> YieldExpression::genera
received_completion_type_register_is_normal,
received_completion_type,
generator.add_constant(Value(to_underlying(Completion::Type::Normal))));
generator.emit<Bytecode::Op::JumpIf>(
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<Optional<ScopedOperand>> 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<Bytecode::Op::JumpIf>(
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<Optional<ScopedOperand>> YieldExpression::genera
received_completion_type_register_is_throw,
received_completion_type,
generator.add_constant(Value(to_underlying(Completion::Type::Throw))));
generator.emit<Bytecode::Op::JumpIf>(
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<Optional<ScopedOperand>> 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<Bytecode::Op::JumpIf>(
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<Optional<ScopedOperand>> 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<Bytecode::Op::JumpIf>(
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<Optional<ScopedOperand>> YieldExpression::genera
received_completion_type_is_normal,
received_completion_type,
generator.add_constant(Value(to_underlying(Completion::Type::Normal))));
generator.emit<Bytecode::Op::JumpIf>(
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<Optional<ScopedOperand>> 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<Bytecode::Op::JumpIf>(
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<Optional<ScopedOperand>> IfStatement::generate_b
generator.emit<Bytecode::Op::Mov>(dst, generator.add_constant(js_undefined()));
auto predicate = TRY(m_predicate->generate_bytecode(generator)).value();
generator.emit<Bytecode::Op::JumpIf>(
generator.emit_jump_if(
predicate,
Bytecode::Label { true_block },
Bytecode::Label { false_block });
@ -2241,7 +2238,7 @@ Bytecode::CodeGenerationErrorOr<Optional<ScopedOperand>> ConditionalExpression::
auto& end_block = generator.make_block();
auto test = TRY(m_test->generate_bytecode(generator)).value();
generator.emit<Bytecode::Op::JumpIf>(
generator.emit_jump_if(
test,
Bytecode::Label { true_block },
Bytecode::Label { false_block });
@ -2601,7 +2598,7 @@ Bytecode::CodeGenerationErrorOr<Optional<ScopedOperand>> SwitchStatement::genera
auto result = generator.allocate_register();
generator.emit<Bytecode::Op::StrictlyEquals>(result, test_value, discriminant);
next_test_block = test_blocks.dequeue();
generator.emit<Bytecode::Op::JumpIf>(
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<Bytecode::Op::JumpIf>(
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<Optional<ScopedOperand>> for_in_of_body_e
// e. If done is true, return V.
auto& loop_continue = generator.make_block();
generator.emit<Bytecode::Op::JumpIf>(
generator.emit_jump_if(
done,
Bytecode::Label { loop_end },
Bytecode::Label { loop_continue });

View file

@ -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<Generator>) { 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<size_t, SourceRecord> m_source_map;
Optional<ScopedOperand> m_this;
size_t m_last_instruction_start_offset { 0 };
};
}

View file

@ -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<Instruction const*>(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<Op::op_TitleCase const&>(last_instruction); \
VERIFY(comparison.dst() == condition); \
auto lhs = comparison.lhs(); \
auto rhs = comparison.rhs(); \
m_current_basic_block->rewind(); \
emit<Op::Jump##op_TitleCase>(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<Op::JumpIf>(condition, true_target, false_target);
}
}

View file

@ -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>(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>(args)...);
@ -115,6 +117,8 @@ public:
emit_with_extra_slots<OpType, Value>(extra_operand_slots, forward<Args>(args)...);
}
void emit_jump_if(ScopedOperand const& condition, Label true_target, Label false_target);
struct ReferenceOperands {
Optional<ScopedOperand> base {}; // [[Base]]
Optional<ScopedOperand> 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<DeprecatedFlyString> language_label_set;

View file

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

View file

@ -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<Op::Jump##op_TitleCase const*>(&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<Op::JumpUndefined const*>(&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:{}",

View file

@ -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<void> execute_impl(Bytecode::Interpreter&) const; \
ByteString to_byte_string_impl(Bytecode::Executable const&) const; \
void visit_labels_impl(Function<void(Label&)> 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;

View file

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