mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-14 01:04:38 +03:00
LibJS: Add an EliminateLoads pass to Bytecode
This pass tries to eliminate repeated lookups of variables by name, by remembering where these where last loaded to. For now the lookup cache needs to be fully cleared with each call or property access, because we do not have a way to check if these have any side effects on the currently visible scopes. Note that property accesses can cause getters/setters to be called, so these are treated as calls in all cases.
This commit is contained in:
parent
fafe498238
commit
eb50969781
Notes:
sideshowbarker
2024-07-17 03:46:19 +09:00
Author: https://github.com/Hendiadyoin1 Commit: https://github.com/SerenityOS/serenity/commit/eb50969781 Pull-request: https://github.com/SerenityOS/serenity/pull/15754 Reviewed-by: https://github.com/alimpfard ✅ Reviewed-by: https://github.com/davidot Reviewed-by: https://github.com/kleinesfilmroellchen ✅ Reviewed-by: https://github.com/linusg
@ -233,6 +233,7 @@ Bytecode::PassManager& Interpreter::optimization_pipeline(Interpreter::Optimizat
|
||||
pm->add<Passes::MergeBlocks>();
|
||||
pm->add<Passes::GenerateCFG>();
|
||||
pm->add<Passes::PlaceBlocks>();
|
||||
pm->add<Passes::EliminateLoads>();
|
||||
} else {
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
197
Userland/Libraries/LibJS/Bytecode/Pass/LoadElimination.cpp
Normal file
197
Userland/Libraries/LibJS/Bytecode/Pass/LoadElimination.cpp
Normal file
@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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::must_create(number_of_registers, false);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 DeleteByValue:
|
||||
// These can trigger proxies, which call into user code
|
||||
// So these are treated like calls
|
||||
case GetByValue:
|
||||
case GetById:
|
||||
case PutByValue:
|
||||
case PutById:
|
||||
// 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:
|
||||
// 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.ptr_at(it.index()) = move(new_block);
|
||||
}
|
||||
|
||||
finished();
|
||||
}
|
||||
|
||||
}
|
@ -136,6 +136,15 @@ private:
|
||||
FILE* m_file { nullptr };
|
||||
};
|
||||
|
||||
class EliminateLoads : public Pass {
|
||||
public:
|
||||
EliminateLoads() = default;
|
||||
virtual ~EliminateLoads() override = default;
|
||||
|
||||
private:
|
||||
virtual void perform(PassPipelineExecutable&) override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ set(SOURCES
|
||||
Bytecode/Op.cpp
|
||||
Bytecode/Pass/DumpCFG.cpp
|
||||
Bytecode/Pass/GenerateCFG.cpp
|
||||
Bytecode/Pass/LoadElimination.cpp
|
||||
Bytecode/Pass/MergeBlocks.cpp
|
||||
Bytecode/Pass/PlaceBlocks.cpp
|
||||
Bytecode/Pass/UnifySameBlocks.cpp
|
||||
|
Loading…
Reference in New Issue
Block a user