1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-26 18:27:39 +00:00

LibJS/Bytecode: Bring back the bytecode optimization pipeline

...minus the EliminateLoads pass, since it was not compatible with the
new bytecode format.
This commit is contained in:
Andreas Kling 2024-02-25 17:59:20 +01:00
parent 836d93f7e3
commit 5b29974bfa
15 changed files with 962 additions and 65 deletions

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2021, Ali Mohammad Pur <mpfard@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Bytecode/PassManager.h>
namespace JS::Bytecode::Passes {
void DumpCFG::perform(PassPipelineExecutable& executable)
{
started();
VERIFY(executable.cfg.has_value());
outln(m_file, "CFG Dump for {} basic blocks:", executable.executable.basic_blocks.size());
for (auto& entry : executable.cfg.value()) {
for (auto& value : entry.value)
outln(m_file, "{} -> {}", entry.key->name(), value->name());
}
outln(m_file);
finished();
}
}

View file

@ -0,0 +1,220 @@
/*
* Copyright (c) 2021, Ali Mohammad Pur <mpfard@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/TemporaryChange.h>
#include <LibJS/Bytecode/PassManager.h>
namespace JS::Bytecode::Passes {
struct UnwindFrame {
BasicBlock const* handler;
BasicBlock const* finalizer;
Vector<BasicBlock const*> finalizer_targets;
};
static HashTable<BasicBlock const*> seen_blocks;
static Vector<UnwindFrame> unwind_frames;
static BasicBlock const* next_handler_or_finalizer()
{
return unwind_frames.last().handler ?: unwind_frames.last().finalizer;
}
static void generate_cfg_for_block(BasicBlock const& current_block, PassPipelineExecutable& executable)
{
seen_blocks.set(&current_block);
auto enter_label = [&](Label const& label, BasicBlock const& entering_block) {
executable.cfg->ensure(&entering_block).set(&label.block());
executable.inverted_cfg->ensure(&label.block()).set(&entering_block);
// The finalizers and handlers of an unwind context are handled separately
if (!seen_blocks.contains(&label.block())
&& &label.block() != unwind_frames.last().handler
&& &label.block() != unwind_frames.last().finalizer)
generate_cfg_for_block(label.block(), executable);
};
if (auto const* block = next_handler_or_finalizer())
enter_label(Label { *block }, current_block);
for (InstructionStreamIterator it { current_block.instruction_stream() }; !it.at_end(); ++it) {
auto const& instruction = *it;
if (instruction.type() == Instruction::Type::LeaveUnwindContext) {
if (unwind_frames.last().finalizer && unwind_frames.last().finalizer != &current_block)
dbgln("FIXME: Popping finalizer from the unwind context from outside the finalizer");
unwind_frames.take_last();
if (auto const* block = next_handler_or_finalizer())
enter_label(Label { *block }, current_block);
}
if (!instruction.is_terminator())
continue;
using enum Instruction::Type;
switch (instruction.type()) {
case Jump: {
auto true_target = *static_cast<Op::Jump const&>(instruction).true_target();
enter_label(true_target, current_block);
return;
}
case JumpIf:
case JumpNullish:
case JumpUndefined: {
// FIXME: It would be nice if we could avoid this copy, if we know that the unwind context stays the same in both paths
// Or with a COW capable Vector alternative
// Note: We might partially unwind here, so we need to make a copy of
// the current context to assure that the falsy code path has the same one
{
TemporaryChange saved_context { unwind_frames, unwind_frames };
auto true_target = *static_cast<Op::Jump const&>(instruction).true_target();
enter_label(true_target, current_block);
}
auto false_target = *static_cast<Op::Jump const&>(instruction).false_target();
enter_label(false_target, current_block);
return;
}
case Yield: {
auto continuation = static_cast<Op::Yield const&>(instruction).continuation();
if (continuation.has_value()) {
executable.exported_blocks->set(&continuation->block());
enter_label(*continuation, current_block);
} else if (auto const* finalizer = unwind_frames.last().finalizer) {
enter_label(Label { *finalizer }, current_block);
unwind_frames.last().finalizer_targets.append(nullptr);
}
return;
}
case Await: {
auto const& continuation = static_cast<Op::Await const&>(instruction).continuation();
executable.exported_blocks->set(&continuation.block());
enter_label(continuation, current_block);
return;
}
case EnterUnwindContext: {
auto entry_point = static_cast<Op::EnterUnwindContext const&>(instruction).entry_point();
auto handler_target = static_cast<Op::EnterUnwindContext const&>(instruction).handler();
auto finalizer_target = static_cast<Op::EnterUnwindContext const&>(instruction).finalizer();
// We keep the frame alive here on the stack, to save some allocation size
UnwindFrame frame {
.handler = handler_target.has_value() ? &handler_target->block() : nullptr,
.finalizer = finalizer_target.has_value() ? &finalizer_target->block() : nullptr,
.finalizer_targets = {}
};
unwind_frames.append(frame);
{
// This will enter the handler and finalizer when needed.
TemporaryChange saved_context { unwind_frames, unwind_frames };
enter_label(entry_point, current_block);
}
frame.handler = nullptr;
if (handler_target.has_value()) {
// We manually generate the CFG, because we previously skiped it
TemporaryChange saved_context { unwind_frames, unwind_frames };
generate_cfg_for_block(handler_target->block(), executable);
}
if (finalizer_target.has_value()) {
// We manually generate the CFG, because we previously halted before entering it
generate_cfg_for_block(finalizer_target->block(), executable);
// We previously halted execution when we would enter the finalizer,
// So we now have to visit all possible targets
// This mainly affects the ScheduleJump instruction
for (auto const* block : frame.finalizer_targets) {
if (block == nullptr) {
// This signals a `return`, which we do not handle specially, so we skip
continue;
}
if (!seen_blocks.contains(block))
generate_cfg_for_block(*block, executable);
}
} else {
unwind_frames.take_last();
VERIFY(frame.finalizer_targets.is_empty());
}
return;
}
case ContinuePendingUnwind: {
auto resume_target = static_cast<Op::ContinuePendingUnwind const&>(instruction).resume_target();
enter_label(resume_target, current_block);
// Note: We already mark these possible control flow changes further up, but when we get
// get better error awareness, being explicit here will be required
if (auto const* handler = unwind_frames.last().handler)
enter_label(Label { *handler }, current_block);
else if (auto const* finalizer = unwind_frames.last().finalizer)
enter_label(Label { *finalizer }, current_block);
return;
}
case Throw:
// Note: We technically register that we enter the handler in the prelude,
// but lets be correct and mark it again,
// this will be useful once we have more info on which instruction can
// actually fail
if (auto const* handler = unwind_frames.last().handler) {
enter_label(Label { *handler }, current_block);
} else if (auto const* finalizer = unwind_frames.last().finalizer) {
enter_label(Label { *finalizer }, current_block);
// Note: This error might bubble through the finalizer to the next handler/finalizer,
// This is currently marked in the general path
}
return;
case Return:
if (auto const* finalizer = unwind_frames.last().finalizer) {
enter_label(Label { *finalizer }, current_block);
unwind_frames.last().finalizer_targets.append(nullptr);
}
return;
case ScheduleJump: {
enter_label(Label { *unwind_frames.last().finalizer }, current_block);
unwind_frames.last().finalizer_targets.append(
&static_cast<Op::ScheduleJump const&>(instruction).target().block());
return;
}
case End:
return;
default:
dbgln("Unhandled terminator instruction: `{}`", instruction.to_byte_string(executable.executable));
VERIFY_NOT_REACHED();
};
}
// We have left the block, but not through a designated terminator,
// so before we return, we need to check if we still need to go through a finalizer
if (auto const* finalizer = unwind_frames.last().finalizer)
enter_label(Label { *finalizer }, current_block);
}
void GenerateCFG::perform(PassPipelineExecutable& executable)
{
started();
executable.cfg = HashMap<BasicBlock const*, HashTable<BasicBlock const*>> {};
executable.inverted_cfg = HashMap<BasicBlock const*, HashTable<BasicBlock const*>> {};
executable.exported_blocks = HashTable<BasicBlock const*> {};
seen_blocks.clear();
unwind_frames.clear();
UnwindFrame top_level_frame = {};
unwind_frames.append(top_level_frame);
generate_cfg_for_block(*executable.executable.basic_blocks.first(), executable);
finished();
}
}

View file

@ -0,0 +1,177 @@
/*
* Copyright (c) 2021, Ali Mohammad Pur <mpfard@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Bytecode/PassManager.h>
namespace JS::Bytecode::Passes {
void MergeBlocks::perform(PassPipelineExecutable& executable)
{
started();
VERIFY(executable.cfg.has_value());
VERIFY(executable.inverted_cfg.has_value());
auto cfg = executable.cfg.release_value();
auto inverted_cfg = executable.inverted_cfg.release_value();
// Figure out which blocks can be merged
HashTable<BasicBlock const*> blocks_to_merge;
HashMap<BasicBlock const*, BasicBlock const*> blocks_to_replace;
Vector<BasicBlock const*> blocks_to_remove;
Vector<size_t> boundaries;
for (auto& entry : cfg) {
if (entry.value.size() != 1)
continue;
if (executable.exported_blocks->contains(*entry.value.begin()))
continue;
if (!entry.key->is_terminated())
continue;
if (entry.key->terminator()->type() != Instruction::Type::Jump)
continue;
{
InstructionStreamIterator it { entry.key->instruction_stream() };
auto& first_instruction = *it;
if (first_instruction.type() == Instruction::Type::Jump) {
auto const* replacing_block = &static_cast<Op::Jump const&>(first_instruction).true_target()->block();
if (replacing_block != entry.key) {
blocks_to_replace.set(entry.key, replacing_block);
}
continue;
}
}
if (auto cfg_iter = inverted_cfg.find(*entry.value.begin()); cfg_iter != inverted_cfg.end()) {
auto& predecessor_entry = cfg_iter->value;
if (predecessor_entry.size() != 1)
continue;
}
// The two blocks are safe to merge.
blocks_to_merge.set(entry.key);
}
for (auto& entry : blocks_to_replace) {
auto const* replacement = entry.value;
for (;;) {
auto lookup = blocks_to_replace.get(replacement);
if (!lookup.has_value())
break;
if (replacement == *lookup)
break;
replacement = *lookup;
}
entry.value = replacement;
}
auto replace_blocks = [&](auto& blocks, auto& replacement) {
Optional<size_t> first_successor_position;
for (auto& entry : blocks) {
blocks_to_remove.append(entry);
auto it = executable.executable.basic_blocks.find_if([entry](auto& block) { return entry == block; });
VERIFY(!it.is_end());
if (!first_successor_position.has_value())
first_successor_position = it.index();
}
for (auto& block : executable.executable.basic_blocks) {
InstructionStreamIterator it { block->instruction_stream() };
while (!it.at_end()) {
auto& instruction = *it;
++it;
for (auto& entry : blocks)
const_cast<Instruction&>(instruction).replace_references(*entry, replacement);
}
}
return first_successor_position;
};
for (auto& entry : blocks_to_replace) {
AK::Array candidates { entry.key };
(void)replace_blocks(candidates, *entry.value);
}
while (!blocks_to_merge.is_empty()) {
auto it = blocks_to_merge.begin();
auto const* current_block = *it;
blocks_to_merge.remove(it);
Vector<BasicBlock const*> successors { current_block };
for (;;) {
auto const* last = successors.last();
auto entry = cfg.find(last);
if (entry == cfg.end())
break;
auto const* successor = *entry->value.begin();
successors.append(successor);
if (!blocks_to_merge.remove(successor))
break;
}
auto blocks_to_merge_copy = blocks_to_merge;
// We need to do the following multiple times, due to it not being
// guaranteed, that the blocks are in sequential order
bool did_prepend = true;
while (did_prepend) {
did_prepend = false;
for (auto const* last : blocks_to_merge) {
auto entry = cfg.find(last);
if (entry == cfg.end())
continue;
auto const* successor = *entry->value.begin();
if (successor == successors.first()) {
successors.prepend(last);
blocks_to_merge_copy.remove(last);
did_prepend = true;
}
}
}
blocks_to_merge = move(blocks_to_merge_copy);
StringBuilder builder;
builder.append("merge"sv);
for (auto& entry : successors) {
builder.append('.');
builder.append(entry->name());
}
auto new_block = BasicBlock::create(MUST(builder.to_string()));
auto& block = *new_block;
auto first_successor_position = replace_blocks(successors, *new_block);
VERIFY(first_successor_position.has_value());
size_t last_successor_index = successors.size() - 1;
for (size_t i = 0; i < successors.size(); ++i) {
auto& entry = successors[i];
InstructionStreamIterator it { entry->instruction_stream() };
while (!it.at_end()) {
auto& instruction = *it;
++it;
if (instruction.is_terminator() && last_successor_index != i)
break;
// FIXME: Use a single memcpy to copy the whole block at once.
auto instruction_size = instruction.length();
size_t slot_offset = block.size();
block.grow(instruction_size);
auto* next_slot = block.data() + slot_offset;
memcpy(next_slot, &instruction, instruction_size);
}
}
executable.executable.basic_blocks.insert(*first_successor_position, move(new_block));
}
executable.executable.basic_blocks.remove_all_matching([&blocks_to_remove](auto& candidate) { return blocks_to_remove.contains_slow(candidate.ptr()); });
finished();
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2021, Ali Mohammad Pur <mpfard@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Bytecode/PassManager.h>
namespace JS::Bytecode::Passes {
void PlaceBlocks::perform(PassPipelineExecutable& executable)
{
started();
VERIFY(executable.cfg.has_value());
auto cfg = executable.cfg.release_value();
Vector<BasicBlock&> replaced_blocks;
HashTable<BasicBlock const*> reachable_blocks;
// Visit the blocks in CFG order
Function<void(BasicBlock const*)> visit = [&](auto* block) {
if (reachable_blocks.contains(block))
return;
reachable_blocks.set(block);
replaced_blocks.append(*const_cast<BasicBlock*>(block));
auto children = cfg.find(block);
if (children == cfg.end())
return;
for (auto& entry : children->value)
visit(entry);
};
// Make sure to visit the entry block first
visit(executable.executable.basic_blocks.first());
for (auto& entry : cfg)
visit(entry.key);
// Put the unreferenced blocks back in at the end
for (auto& entry : static_cast<Vector<NonnullOwnPtr<BasicBlock>>&>(executable.executable.basic_blocks)) {
if (reachable_blocks.contains(entry.ptr()))
(void)entry.leak_ptr();
else
replaced_blocks.append(*entry.leak_ptr()); // Don't try to do DCE here.
}
executable.executable.basic_blocks.clear();
for (auto& block : replaced_blocks)
executable.executable.basic_blocks.append(adopt_own(block));
finished();
}
}

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2021, Ali Mohammad Pur <mpfard@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Bytecode/PassManager.h>
#include <string.h>
namespace JS::Bytecode::Passes {
void UnifySameBlocks::perform(PassPipelineExecutable& executable)
{
started();
VERIFY(executable.cfg.has_value());
VERIFY(executable.inverted_cfg.has_value());
auto cfg = executable.cfg.release_value();
auto inverted_cfg = executable.inverted_cfg.release_value();
HashMap<BasicBlock const*, BasicBlock const*> equal_blocks;
for (size_t i = 0; i < executable.executable.basic_blocks.size(); ++i) {
auto& block = executable.executable.basic_blocks[i];
auto block_bytes = block->instruction_stream();
for (auto& candidate_block : executable.executable.basic_blocks.span().slice(i + 1)) {
if (equal_blocks.contains(&*candidate_block))
continue;
// FIXME: This can probably be relaxed a bit...
if (candidate_block->size() != block->size())
continue;
if (candidate_block->finalizer() != block->finalizer())
continue;
if (candidate_block->handler() != block->handler())
continue;
auto candidate_bytes = candidate_block->instruction_stream();
if (memcmp(candidate_bytes.data(), block_bytes.data(), candidate_block->size()) == 0)
equal_blocks.set(candidate_block.ptr(), block);
}
}
auto replace_blocks = [&](auto& match, auto& replacement) {
Optional<size_t> first_successor_position;
auto it = executable.executable.basic_blocks.find_if([match](auto& block) { return match == block; });
VERIFY(!it.is_end());
executable.executable.basic_blocks.remove(it.index());
if (!first_successor_position.has_value())
first_successor_position = it.index();
for (auto& block : executable.executable.basic_blocks) {
InstructionStreamIterator it { block->instruction_stream() };
while (!it.at_end()) {
auto& instruction = *it;
++it;
const_cast<Instruction&>(instruction).replace_references(*match, replacement);
}
}
return first_successor_position;
};
for (auto& entry : equal_blocks)
(void)replace_blocks(entry.key, *entry.value);
finished();
}
}