mirror of
https://github.com/RGBCube/serenity
synced 2025-07-26 13:37:44 +00:00
LibJS+CI: Remove bytecode optimization passes for now
These passes have not been shown to actually optimize any JS, and tests have become very flaky with optimizations enabled. Until some measurable benefit is shown, remove the optimization passes to reduce overhead of maintaining bytecode operations and to reduce CI churn. The framework for optimizations will live on in git history, and can be restored once proven useful.
This commit is contained in:
parent
164c132928
commit
77d7f715e3
17 changed files with 1 additions and 1311 deletions
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,220 +0,0 @@
|
|||
/*
|
||||
* 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(¤t_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 != ¤t_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 JumpConditional:
|
||||
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_target();
|
||||
auto finalizer_target = static_cast<Op::EnterUnwindContext const&>(instruction).finalizer_target();
|
||||
|
||||
// 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);
|
||||
VERIFY(unwind_frames.last() != &frame);
|
||||
|
||||
// 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 {
|
||||
VERIFY(unwind_frames.last() == &frame);
|
||||
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;
|
||||
}
|
||||
default:
|
||||
dbgln("Unhandled terminator instruction: `{}`", instruction.to_deprecated_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();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,208 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Leon Albrecht <leon.a@serenityos.com>.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Bitmap.h>
|
||||
#include <AK/TypeCasts.h>
|
||||
#include <LibJS/Bytecode/Op.h>
|
||||
#include <LibJS/Bytecode/PassManager.h>
|
||||
|
||||
namespace JS::Bytecode::Passes {
|
||||
|
||||
static NonnullOwnPtr<BasicBlock> eliminate_loads(BasicBlock const& block, size_t number_of_registers)
|
||||
{
|
||||
auto array_ranges = Bitmap::create(number_of_registers, false).release_value_but_fixme_should_propagate_errors();
|
||||
|
||||
for (auto it = InstructionStreamIterator(block.instruction_stream()); !it.at_end(); ++it) {
|
||||
if ((*it).type() == Instruction::Type::NewArray) {
|
||||
Op::NewArray const& array_instruction = static_cast<Op::NewArray const&>(*it);
|
||||
if (size_t element_count = array_instruction.element_count())
|
||||
array_ranges.set_range<true, false>(array_instruction.start().index(), element_count);
|
||||
} else if ((*it).type() == Instruction::Type::Call) {
|
||||
auto const& call_instruction = static_cast<Op::Call const&>(*it);
|
||||
if (size_t element_count = call_instruction.argument_count())
|
||||
array_ranges.set_range<true, false>(call_instruction.first_argument().index(), element_count);
|
||||
}
|
||||
}
|
||||
|
||||
auto new_block = BasicBlock::create(block.name(), block.size());
|
||||
HashMap<size_t, Register> identifier_table {};
|
||||
HashMap<u32, Register> register_rerouting_table {};
|
||||
|
||||
for (auto it = InstructionStreamIterator(block.instruction_stream()); !it.at_end();) {
|
||||
using enum Instruction::Type;
|
||||
|
||||
// Note: When creating a variable, we technically purge the cache of any
|
||||
// variables of the same name;
|
||||
// In practice, we always generate a coinciding SetVariable, which
|
||||
// does the same
|
||||
switch ((*it).type()) {
|
||||
case GetVariable: {
|
||||
auto const& get_variable = static_cast<Op::GetVariable const&>(*it);
|
||||
++it;
|
||||
auto const& next_instruction = *it;
|
||||
|
||||
if (auto reg = identifier_table.find(get_variable.identifier().value()); reg != identifier_table.end()) {
|
||||
// If we have already seen a variable, we can replace its GetVariable with a simple Load
|
||||
// knowing that it was already stored in a register
|
||||
new (new_block->next_slot()) Op::Load(reg->value);
|
||||
new_block->grow(sizeof(Op::Load));
|
||||
|
||||
if (next_instruction.type() == Instruction::Type::Store) {
|
||||
// If the next instruction is a Store, that is not meant to
|
||||
// construct an array, we can simply elide that store and reroute
|
||||
// all further references to the stores destination to the cached
|
||||
// instance of variable.
|
||||
// FIXME: We might be able to elide the previous load in the non-array case,
|
||||
// because we do not yet reuse the accumulator
|
||||
auto const& store = static_cast<Op::Store const&>(next_instruction);
|
||||
|
||||
if (array_ranges.get(store.dst().index())) {
|
||||
// re-emit the store
|
||||
new (new_block->next_slot()) Op::Store(store);
|
||||
new_block->grow(sizeof(Op::Store));
|
||||
} else {
|
||||
register_rerouting_table.set(store.dst().index(), reg->value);
|
||||
}
|
||||
|
||||
++it;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
// Otherwise we need to emit the GetVariable
|
||||
new (new_block->next_slot()) Op::GetVariable(get_variable);
|
||||
new_block->grow(sizeof(Op::GetVariable));
|
||||
|
||||
// And if the next instruction is a Store, we can cache it's destination
|
||||
if (next_instruction.type() == Instruction::Type::Store) {
|
||||
auto const& store = static_cast<Op::Store const&>(next_instruction);
|
||||
identifier_table.set(get_variable.identifier().value(), store.dst());
|
||||
|
||||
new (new_block->next_slot()) Op::Store(store);
|
||||
new_block->grow(sizeof(Op::Store));
|
||||
++it;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
case SetVariable: {
|
||||
// When a variable is set we need to remove it from the cache, because
|
||||
// we don't have an accurate view on it anymore
|
||||
// FIXME: If the previous instruction was a `Load $reg`, we could
|
||||
// update the cache instead
|
||||
auto const& set_variable = static_cast<Op::SetVariable const&>(*it);
|
||||
|
||||
identifier_table.remove(set_variable.identifier().value());
|
||||
|
||||
break;
|
||||
}
|
||||
case DeleteVariable: {
|
||||
// When a variable is deleted we need to remove it from the cache, it does not
|
||||
// exist anymore, although a variable of the same name may exist in upper scopes
|
||||
auto const& set_variable = static_cast<Op::DeleteVariable const&>(*it);
|
||||
|
||||
identifier_table.remove(set_variable.identifier().value());
|
||||
|
||||
break;
|
||||
}
|
||||
case Store: {
|
||||
// If we store to a position that we have are rerouting from,
|
||||
// we need to remove it from the routeing table
|
||||
// FIXME: This may be redundant due to us assigning to registers only once
|
||||
auto const& store = static_cast<Op::Store const&>(*it);
|
||||
register_rerouting_table.remove(store.dst().index());
|
||||
|
||||
break;
|
||||
}
|
||||
case DeleteById:
|
||||
case DeleteByIdWithThis:
|
||||
case DeleteByValue:
|
||||
case DeleteByValueWithThis:
|
||||
// These can trigger proxies, which call into user code
|
||||
// So these are treated like calls
|
||||
case GetByValue:
|
||||
case GetByValueWithThis:
|
||||
case GetById:
|
||||
case GetByIdWithThis:
|
||||
case PutByValue:
|
||||
case PutByValueWithThis:
|
||||
case PutById:
|
||||
case PutByIdWithThis:
|
||||
// Attribute accesses (`a.o` or `a[o]`) may result in calls to getters or setters
|
||||
// or may trigger proxies
|
||||
// So these are treated like calls
|
||||
case Call:
|
||||
case CallWithArgumentArray:
|
||||
// Calls, especially to local functions and eval, may poison visible and
|
||||
// cached variables, hence we need to clear the lookup cache after emitting them
|
||||
// FIXME: In strict mode and with better identifier metrics, we might be able
|
||||
// to safe some caching with a more fine-grained identifier table
|
||||
// FIXME: We might be able to save some lookups to objects like `this`
|
||||
// which should not change their pointer
|
||||
memcpy(new_block->next_slot(), &*it, (*it).length());
|
||||
for (auto route : register_rerouting_table)
|
||||
reinterpret_cast<Instruction*>(new_block->next_slot())->replace_references(Register { route.key }, route.value);
|
||||
new_block->grow((*it).length());
|
||||
|
||||
identifier_table.clear_with_capacity();
|
||||
|
||||
++it;
|
||||
continue;
|
||||
case NewBigInt:
|
||||
// FIXME: This is the only non trivially copyable Instruction,
|
||||
// so we need to do some extra work here
|
||||
new (new_block->next_slot()) Op::NewBigInt(static_cast<Op::NewBigInt const&>(*it));
|
||||
new_block->grow(sizeof(Op::NewBigInt));
|
||||
++it;
|
||||
continue;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
memcpy(new_block->next_slot(), &*it, (*it).length());
|
||||
for (auto route : register_rerouting_table) {
|
||||
// rerouting from key to value
|
||||
reinterpret_cast<Instruction*>(new_block->next_slot())->replace_references(Register { route.key }, route.value);
|
||||
}
|
||||
// because we are replacing the current block, we need to replace references
|
||||
// to ourselves here
|
||||
reinterpret_cast<Instruction*>(new_block->next_slot())->replace_references(block, *new_block);
|
||||
|
||||
new_block->grow((*it).length());
|
||||
|
||||
++it;
|
||||
}
|
||||
return new_block;
|
||||
}
|
||||
|
||||
void EliminateLoads::perform(PassPipelineExecutable& executable)
|
||||
{
|
||||
started();
|
||||
|
||||
// FIXME: If we walk the CFG instead of the block list, we might be able to
|
||||
// save some work between blocks
|
||||
for (auto it = executable.executable.basic_blocks.begin(); it != executable.executable.basic_blocks.end(); ++it) {
|
||||
auto const& old_block = *it;
|
||||
auto new_block = eliminate_loads(*old_block, executable.executable.number_of_registers);
|
||||
|
||||
// We will replace the old block, with a new one, so we need to replace all references,
|
||||
// to the old one with the new one
|
||||
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(*old_block, *new_block);
|
||||
}
|
||||
}
|
||||
|
||||
executable.executable.basic_blocks[it.index()] = move(new_block);
|
||||
}
|
||||
|
||||
finished();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,185 +0,0 @@
|
|||
/*
|
||||
* 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);
|
||||
|
||||
size_t size = 0;
|
||||
StringBuilder builder;
|
||||
builder.append("merge"sv);
|
||||
for (auto& entry : successors) {
|
||||
size += entry->size();
|
||||
builder.append('.');
|
||||
builder.append(entry->name());
|
||||
}
|
||||
|
||||
auto new_block = BasicBlock::create(builder.to_deprecated_string(), size);
|
||||
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: Op::NewBigInt is not trivially copyable, so we cant use
|
||||
// a simple memcpy to transfer them.
|
||||
// When this is resolved we can use a single memcpy to copy
|
||||
// the whole block at once
|
||||
if (instruction.type() == Instruction::Type::NewBigInt) {
|
||||
new (block.next_slot()) Op::NewBigInt(static_cast<Op::NewBigInt const&>(instruction));
|
||||
block.grow(sizeof(Op::NewBigInt));
|
||||
} else {
|
||||
auto instruction_size = instruction.length();
|
||||
memcpy(block.next_slot(), &instruction, instruction_size);
|
||||
block.grow(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();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
auto candidate_bytes = candidate_block->instruction_stream();
|
||||
// FIXME: NewBigInt's value is not correctly reflected by its encoding in memory,
|
||||
// this will yield false negatives for blocks containing that
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue