[goalc] register allocator v2 (#731)

* clean up allocator interface to be simpler

* working on functions without spills

* working for all

* fix missing includes for windows

* more windows includes

* initialize regs to zero so printing value unintiailized by game code is repeatable
This commit is contained in:
water111
2021-08-01 17:46:55 -04:00
committed by GitHub
parent b8f80e435c
commit aa58d146c2
29 changed files with 2314 additions and 949 deletions
+2
View File
@@ -21,6 +21,8 @@ class Range {
Range(const T& start, const T& end) : m_start(start), m_end(end) {}
const T& first() const { return m_start; }
const T& last() const { return m_end; }
T& first() { return m_start; }
T& last() { return m_end; }
bool contains(T& val) const { return val >= m_start && val < m_end; }
bool empty() const { return m_end <= m_start; }
T size() const { return m_end - m_start; }
+3 -1
View File
@@ -184,4 +184,6 @@
- Multiple variables assigned to the same register using `:reg` in `rlet` (or overlapping with `self` in a behavior) will now be merged to a single variable instead of causing a compiler error. Variables will have their own type, but they will all be an alias of the same exact register.
- Stack arrays of uint128 will now be 16-byte aligned instead of sometimes only 8.
- Inline arrays of structures are now allowed with `stack-no-clear`.
- Creating arrays on the stack now must be done with `stack-no-clear` as they are not memset to 0 or constructed in any way.
- Creating arrays on the stack now must be done with `stack-no-clear` as they are not memset to 0 or constructed in any way.
- The register allocator has been dramatically improved and generates ~5x fewer spill instructions and is able to eliminate more moves.
- Added a `(print-debug-compiler-stats)` form to print out statistics related to register allocation and move elimination
+8
View File
@@ -1262,6 +1262,14 @@
(vf15 :class vf)
)
(init-vf0-vector)
;; added to make the right most column always zero.
;; it seems like they didn't care about these values
;; but this makes the test repeatable, and there
;; really could be anything here. This function is unused anyway
(.xor.vf vf13 vf13 vf13)
(.xor.vf vf14 vf14 vf14)
(.xor.vf vf15 vf15 vf15)
(.lvf vf10 (&-> src vector 0 quad))
(.lvf vf11 (&-> src vector 1 quad))
(.lvf vf12 (&-> src vector 2 quad))
+12 -6
View File
@@ -889,6 +889,8 @@
(set! obj (the cpu-thread (-> obj process)))
;; resume!
(.jr temp)
(.add a4 a4)
(.add a5 a5)
)
(none)
)
@@ -2322,7 +2324,7 @@
This stub remaps these saved registers to argument registers.
It also creates a return trampoline to return-from-thread-dead"
(declare (asm-func none)
;(print-asm)
;;(print-asm)
)
(rlet ((s0 :reg rbx :type uint)
@@ -2330,11 +2332,13 @@
(s2 :reg r10 :type uint)
(s3 :reg r11 :type uint)
(s4 :reg r12 :type uint)
(a0 :reg rdi :type uint) ; ok
(a1 :reg rsi :type uint) ; ok
(a2 :reg rdx :type uint) ; ok
(a3 :reg rcx :type uint) ; ok
(a0 :reg rdi :type uint) ; ok
(a1 :reg rsi :type uint) ; ok
(a2 :reg rdx :type uint) ; ok
(a3 :reg rcx :type uint) ; ok
(off :reg r15 :type uint)
(a4 :reg r8 :type uint)
(a5 :reg r9 :type uint)
(temp :reg rax)
)
@@ -2352,7 +2356,9 @@
(.add :color #f s0 off)
(.jr :color #f s0)
(.add a4 a4)
(.add a5 a5)
)
)
+2 -2
View File
@@ -39,8 +39,8 @@ add_library(compiler
make/Tools.cpp
regalloc/IRegister.cpp
regalloc/Allocator.cpp
regalloc/allocate.cpp
regalloc/allocate_common.cpp
regalloc/allocator_interface.cpp
regalloc/Allocator_v2.cpp
)
target_link_libraries(compiler common Zydis)
+1
View File
@@ -17,6 +17,7 @@ class CodeGenerator {
public:
CodeGenerator(FileEnv* env, DebugInfo* debug_info);
std::vector<u8> run(const TypeSystem* ts);
emitter::ObjectGeneratorStats get_obj_stats() const { return m_gen.get_stats(); }
private:
void do_function(FunctionEnv* env, int f_idx);
+30 -2
View File
@@ -5,7 +5,8 @@
#include "IR.h"
#include "common/link_types.h"
#include "goalc/make/Tools.h"
#include "goalc/regalloc/allocate.h"
#include "goalc/regalloc/Allocator.h"
#include "goalc/regalloc/Allocator_v2.h"
#include "third-party/fmt/core.h"
using namespace goos;
@@ -198,6 +199,7 @@ Val* Compiler::compile_error_guard(const goos::Object& code, Env* env) {
}
void Compiler::color_object_file(FileEnv* env) {
int num_spills_in_file = 0;
for (auto& f : env->functions()) {
AllocationInput input;
input.is_asm_function = f->is_asm_func;
@@ -215,6 +217,7 @@ void Compiler::color_object_file(FileEnv* env) {
input.max_vars = f->max_vars();
input.constraints = f->constraints();
input.stack_slots_for_stack_vars = f->stack_slots_used_for_stack_vars();
input.function_name = f->name();
if (m_settings.debug_print_regalloc) {
input.debug_settings.print_input = true;
@@ -223,8 +226,31 @@ void Compiler::color_object_file(FileEnv* env) {
input.debug_settings.allocate_log_level = 2;
}
f->set_allocations(allocate_registers(input));
m_debug_stats.total_funcs++;
auto regalloc_result_2 = allocate_registers_v2(input);
if (regalloc_result_2.ok) {
if (regalloc_result_2.num_spilled_vars > 0) {
// fmt::print("Function {} has {} spilled vars.\n", f->name(),
// regalloc_result_2.num_spilled_vars);
}
num_spills_in_file += regalloc_result_2.num_spills;
f->set_allocations(regalloc_result_2);
} else {
fmt::print(
"Warning: function {} failed register allocation with the v2 allocator. Falling back to "
"the v1 allocator.\n",
f->name());
m_debug_stats.funcs_requiring_v1_allocator++;
auto regalloc_result = allocate_registers(input);
m_debug_stats.num_spills_v1 += regalloc_result.num_spills;
num_spills_in_file += regalloc_result.num_spills;
f->set_allocations(regalloc_result);
}
}
m_debug_stats.num_spills += num_spills_in_file;
}
std::vector<u8> Compiler::codegen_object_file(FileEnv* env) {
@@ -239,6 +265,8 @@ std::vector<u8> Compiler::codegen_object_file(FileEnv* env) {
fmt::print("{}\n", debug_info->disassemble_function_by_name(f->name(), &ok));
}
}
auto stats = gen.get_obj_stats();
m_debug_stats.num_moves_eliminated += stats.moves_eliminated;
return result;
} catch (std::exception& e) {
throw_compiler_error_no_code("Error during codegen: {}", e.what());
+11
View File
@@ -79,6 +79,14 @@ class Compiler {
std::unique_ptr<ReplWrapper> m_repl;
MakeSystem m_make;
struct DebugStats {
int num_spills = 0;
int num_spills_v1 = 0;
int num_moves_eliminated = 0;
int total_funcs = 0;
int funcs_requiring_v1_allocator = 0;
} m_debug_stats;
std::set<std::string> lookup_symbol_infos_starting_with(const std::string& prefix) const;
std::vector<SymbolInfo>* lookup_exact_name_info(const std::string& name) const;
bool get_true_or_false(const goos::Object& form, const goos::Object& boolean);
@@ -517,6 +525,9 @@ class Compiler {
Env* env);
Val* compile_load_project(const goos::Object& form, const goos::Object& rest, Env* env);
Val* compile_make(const goos::Object& form, const goos::Object& rest, Env* env);
Val* compile_print_debug_compiler_stats(const goos::Object& form,
const goos::Object& rest,
Env* env);
// ControlFlow
Condition compile_condition(const goos::Object& condition, Env* env, bool invert);
+1 -1
View File
@@ -13,7 +13,7 @@
#include <memory>
#include <vector>
#include "common/type_system/TypeSpec.h"
#include "goalc/regalloc/allocate.h"
#include "goalc/regalloc/allocator_interface.h"
#include "common/goos/Object.h"
#include "StaticObject.h"
#include "Label.h"
+5 -2
View File
@@ -13,7 +13,7 @@ Register get_reg(const RegVal* rv, const AllocationResult& allocs, emitter::IR_R
auto reg = rv->rlet_constraint().value();
if (rv->ireg().id < int(range.size())) {
auto& lr = range.at(rv->ireg().id);
if (irec.ir_id >= lr.min && irec.ir_id <= lr.max) {
if (lr.has_info_at(irec.ir_id)) {
auto ass_reg = range.at(rv->ireg().id).get(irec.ir_id);
if (ass_reg.kind == Assignment::Kind::REGISTER) {
assert(ass_reg.reg == reg);
@@ -41,7 +41,7 @@ int get_stack_offset(const RegVal* rv, const AllocationResult& allocs) {
} else {
assert(rv->forced_on_stack());
auto& ass = allocs.ass_as_ranges.at(rv->ireg().id);
auto stack_slot = allocs.get_slot_for_spill(ass.assignment.at(0).stack_slot);
auto stack_slot = allocs.get_slot_for_spill(ass.stack_slot());
assert(stack_slot >= 0);
return stack_slot * 8;
}
@@ -103,6 +103,7 @@ void regset_common(emitter::ObjectGenerator* gen,
if (src_class == RegClass::GPR_64 && dst_class == RegClass::GPR_64) {
if (src_reg == dst_reg) {
// eliminate move
gen->count_eliminated_move();
gen->add_instr(IGen::null(), irec);
} else {
gen->add_instr(IGen::mov_gpr64_gpr64(dst_reg, src_reg), irec);
@@ -110,6 +111,7 @@ void regset_common(emitter::ObjectGenerator* gen,
} else if (src_class == RegClass::FLOAT && dst_class == RegClass::FLOAT) {
if (src_reg == dst_reg) {
// eliminate move
gen->count_eliminated_move();
gen->add_instr(IGen::null(), irec);
} else {
gen->add_instr(IGen::mov_xmm32_xmm32(dst_reg, src_reg), irec);
@@ -117,6 +119,7 @@ void regset_common(emitter::ObjectGenerator* gen,
} else if (src_is_xmm128 && dst_is_xmm128) {
if (src_reg == dst_reg) {
// eliminate move
gen->count_eliminated_move();
gen->add_instr(IGen::null(), irec);
} else {
gen->add_instr(IGen::mov_vf_vf(dst_reg, src_reg), irec);
+1 -1
View File
@@ -2,7 +2,7 @@
#include <string>
#include "CodeGenerator.h"
#include "goalc/regalloc/allocate.h"
#include "goalc/regalloc/allocator_interface.h"
#include "Val.h"
#include "goalc/emitter/ObjectGenerator.h"
#include "goalc/emitter/Register.h"
+1
View File
@@ -153,6 +153,7 @@ const std::unordered_map<
{"add-macro-to-autocomplete", &Compiler::compile_add_macro_to_autocomplete},
{"load-project", &Compiler::compile_load_project},
{"make", &Compiler::compile_make},
{"print-debug-compiler-stats", &Compiler::compile_print_debug_compiler_stats},
// CONDITIONAL COMPILATION
{"#cond", &Compiler::compile_gscond},
@@ -603,5 +603,21 @@ Val* Compiler::compile_make(const goos::Object& form, const goos::Object& rest,
}
m_make.make(args.unnamed.at(0).as_string()->data, force, verbose);
return get_none();
}
Val* Compiler::compile_print_debug_compiler_stats(const goos::Object& form,
const goos::Object& rest,
Env*) {
auto args = get_va(form, rest);
va_check(form, args, {}, {});
fmt::print("Spill operations (total): {}\n", m_debug_stats.num_spills);
fmt::print("Spill operations (v1 only): {}\n", m_debug_stats.num_spills_v1);
fmt::print("Eliminated moves: {}\n", m_debug_stats.num_moves_eliminated);
fmt::print("Total functions: {}\n", m_debug_stats.total_funcs);
fmt::print("Functions requiring v1: {}\n", m_debug_stats.funcs_requiring_v1_allocator);
fmt::print("Size of autocomplete prefix tree: {}\n", m_symbol_info.symbol_count());
return get_none();
}
+12 -5
View File
@@ -206,14 +206,13 @@ Val* Compiler::compile_lambda(const goos::Object& form, const goos::Object& rest
for (u32 i = 0; i < lambda.params.size(); i++) {
IRegConstraint constr;
constr.instr_idx = 0; // constraint at function start
auto ireg = new_func_env->make_ireg(
auto ireg_arg = new_func_env->make_ireg(
lambda.params.at(i).type, arg_regs.at(i).is_gpr() ? RegClass::GPR_64 : RegClass::INT_128);
ireg->mark_as_settable();
constr.ireg = ireg->ireg();
ireg_arg->mark_as_settable();
constr.ireg = ireg_arg->ireg();
constr.desired_register = arg_regs.at(i);
new_func_env->params[lambda.params.at(i).name] = ireg;
new_func_env->constrain(constr);
reset_args_for_coloring.push_back(ireg);
reset_args_for_coloring.push_back(ireg_arg);
}
if (args.has_named("behavior")) {
@@ -244,6 +243,14 @@ Val* Compiler::compile_lambda(const goos::Object& form, const goos::Object& rest
func_block_env->end_label = Label(new_func_env.get());
func_block_env->emit(std::make_unique<IR_ValueReset>(reset_args_for_coloring));
for (u32 i = 0; i < lambda.params.size(); i++) {
auto ireg = new_func_env->make_ireg(
lambda.params.at(i).type, arg_regs.at(i).is_gpr() ? RegClass::GPR_64 : RegClass::INT_128);
ireg->mark_as_settable();
new_func_env->params[lambda.params.at(i).name] = ireg;
new_func_env->emit_ir<IR_RegSet>(ireg, reset_args_for_coloring.at(i));
}
// compile the function, iterating through the body.
Val* result = nullptr;
bool first_thing = true;
+4
View File
@@ -419,6 +419,10 @@ Val* Compiler::compile_div(const goos::Object& form, const goos::Object& rest, E
IntegerMathKind::UDIV_32, result,
to_math_type(form, val, math_type, env)->to_gpr(env)));
}
auto result_moved = env->make_gpr(first_type);
env->emit_ir<IR_RegSet>(result_moved, result);
return result_moved;
}
return result;
+27 -13
View File
@@ -215,15 +215,18 @@ Val* Compiler::generate_inspector_for_structure_type(const goos::Object& form,
method_env->set_segment(DEBUG_SEGMENT);
// Create a register which will hold the input to the inspect method
auto input = method_env->make_gpr(structure_type->get_name());
auto input_arg = method_env->make_gpr(structure_type->get_name());
// "Constrain" this register to be the register that the function argument is passed in
IRegConstraint constraint;
constraint.instr_idx = 0; // constraint at the start of the function
constraint.ireg = input->ireg(); // constrain this register
constraint.instr_idx = 0; // constraint at the start of the function
constraint.ireg = input_arg->ireg(); // constrain this register
constraint.desired_register = emitter::gRegInfo.get_gpr_arg_reg(0); // to the first argument
method_env->constrain(constraint);
// Inform the compiler that `input`'s value will be written to `rdi` (first arg register)
method_env->emit(std::make_unique<IR_ValueReset>(std::vector<RegVal*>{input}));
method_env->emit(std::make_unique<IR_ValueReset>(std::vector<RegVal*>{input_arg}));
auto input = method_env->make_gpr(structure_type->get_name());
method_env->emit_ir<IR_RegSet>(input, input_arg);
// there's a special case for children of process.
if (m_ts.fully_defined_type_exists("process") &&
@@ -291,11 +294,11 @@ Val* Compiler::generate_inspector_for_bitfield_type(const goos::Object& form,
method_env->set_segment(DEBUG_SEGMENT);
// Create a register which will hold the input to the inspect method
auto input = method_env->make_gpr(bitfield_type->get_name());
auto input_arg = method_env->make_gpr(bitfield_type->get_name());
// "Constrain" this register to be the register that the function argument is passed in
IRegConstraint constraint;
constraint.instr_idx = 0; // constraint at the start of the function
constraint.ireg = input->ireg(); // constrain this register
constraint.instr_idx = 0; // constraint at the start of the function
constraint.ireg = input_arg->ireg(); // constrain this register
if (bitfield_128) {
constraint.desired_register = emitter::gRegInfo.get_xmm_arg_reg(0); // to the first argument
} else {
@@ -304,7 +307,10 @@ Val* Compiler::generate_inspector_for_bitfield_type(const goos::Object& form,
method_env->constrain(constraint);
// Inform the compiler that `input`'s value will be written to `rdi` (first arg register)
method_env->emit(std::make_unique<IR_ValueReset>(std::vector<RegVal*>{input}));
method_env->emit(std::make_unique<IR_ValueReset>(std::vector<RegVal*>{input_arg}));
auto input = method_env->make_gpr(bitfield_type->get_name());
method_env->emit_ir<IR_RegSet>(input, input_arg);
RegVal* type_name =
compile_get_sym_obj(bitfield_type->get_name(), method_env.get())->to_gpr(method_env.get());
@@ -473,14 +479,14 @@ Val* Compiler::compile_defmethod(const goos::Object& form, const goos::Object& _
for (u32 i = 0; i < lambda.params.size(); i++) {
IRegConstraint constr;
constr.instr_idx = 0; // constraint at function start
auto ireg = new_func_env->make_ireg(
auto ireg_arg = new_func_env->make_ireg(
lambda.params.at(i).type, arg_regs.at(i).is_gpr() ? RegClass::GPR_64 : RegClass::INT_128);
ireg->mark_as_settable();
constr.ireg = ireg->ireg();
ireg_arg->mark_as_settable();
constr.ireg = ireg_arg->ireg();
constr.desired_register = arg_regs.at(i);
new_func_env->params[lambda.params.at(i).name] = ireg;
new_func_env->constrain(constr);
reset_args_for_coloring.push_back(ireg);
reset_args_for_coloring.push_back(ireg_arg);
}
auto method_info = m_ts.lookup_method(symbol_string(type_name), symbol_string(method_name));
@@ -511,6 +517,14 @@ Val* Compiler::compile_defmethod(const goos::Object& form, const goos::Object& _
func_block_env->end_label = Label(new_func_env.get());
func_block_env->emit(std::make_unique<IR_ValueReset>(reset_args_for_coloring));
for (u32 i = 0; i < lambda.params.size(); i++) {
auto ireg = new_func_env->make_ireg(
lambda.params.at(i).type, arg_regs.at(i).is_gpr() ? RegClass::GPR_64 : RegClass::INT_128);
ireg->mark_as_settable();
new_func_env->params[lambda.params.at(i).name] = ireg;
new_func_env->emit_ir<IR_RegSet>(ireg, reset_args_for_coloring.at(i));
}
// compile the function!
Val* result = nullptr;
bool first_thing = true;
+8
View File
@@ -615,4 +615,12 @@ std::vector<u8> ObjectGenerator::generate_header_v3() {
push_data<uint32_t>(64 + 4 + total_link_size, result); // todo, make these numbers less magic.
return result;
}
ObjectGeneratorStats ObjectGenerator::get_stats() const {
return m_stats;
}
void ObjectGenerator::count_eliminated_move() {
m_stats.moves_eliminated++;
}
} // namespace emitter
+8 -5
View File
@@ -5,9 +5,6 @@
#pragma once
#ifndef JAK_OBJECTGENERATOR_H
#define JAK_OBJECTGENERATOR_H
#include <cstring>
#include <map>
#include <string>
@@ -56,6 +53,10 @@ struct StaticRecord {
int static_id = -1;
};
struct ObjectGeneratorStats {
int moves_eliminated = 0;
};
class ObjectGenerator {
public:
ObjectGenerator() = default;
@@ -90,6 +91,8 @@ class ObjectGenerator {
int offset);
void link_instruction_to_function(const InstructionRecord& instr,
const FunctionRecord& target_func);
ObjectGeneratorStats get_stats() const;
void count_eliminated_move();
private:
void handle_temp_static_type_links(int seg);
@@ -226,7 +229,7 @@ class ObjectGenerator {
seg_vector<PointerLink> m_pointer_links_by_seg;
std::vector<FunctionRecord> m_all_function_records;
ObjectGeneratorStats m_stats;
};
} // namespace emitter
#endif // JAK_OBJECTGENERATOR_H
+153 -218
View File
@@ -3,87 +3,17 @@
* Implementation of register allocation algorithms
*/
#include <algorithm>
#include <stdexcept>
#include "third-party/fmt/core.h"
#include "Allocator.h"
#include "goalc/regalloc/allocator_interface.h"
/*!
* Find basic blocks and add block link info.
*/
void find_basic_blocks(RegAllocCache* cache, const AllocationInput& in) {
std::vector<int> dividers;
dividers.push_back(0);
dividers.push_back(in.instructions.size());
// loop over instructions, finding jump targets
for (uint32_t i = 0; i < in.instructions.size(); i++) {
const auto& instr = in.instructions[i];
if (!instr.jumps.empty()) {
dividers.push_back(i + 1);
for (auto dest : instr.jumps) {
dividers.push_back(dest);
}
}
}
// sort dividers, and make blocks
std::sort(dividers.begin(), dividers.end(), [](int a, int b) { return a < b; });
for (uint32_t i = 0; i < dividers.size() - 1; i++) {
if (dividers[i] != dividers[i + 1]) {
// new basic block!
RegAllocBasicBlock block;
for (int j = dividers[i]; j < dividers[i + 1]; j++) {
block.instr_idx.push_back(j);
}
block.idx = cache->basic_blocks.size();
cache->basic_blocks.push_back(block);
}
}
if (!cache->basic_blocks.empty()) {
cache->basic_blocks.front().is_entry = true;
cache->basic_blocks.back().is_exit = true;
}
auto find_basic_block_to_target = [&](int instr) {
bool found = false;
uint32_t result = -1;
for (uint32_t i = 0; i < cache->basic_blocks.size(); i++) {
if (!cache->basic_blocks[i].instr_idx.empty() &&
cache->basic_blocks[i].instr_idx.front() == instr) {
assert(!found);
found = true;
result = i;
}
}
if (!found) {
printf("[RegAlloc Error] couldn't find basic block beginning with instr %d of %d\n", instr,
int(in.instructions.size()));
}
assert(found);
return result;
};
// link blocks
for (auto& block : cache->basic_blocks) {
assert(!block.instr_idx.empty());
auto& last_instr = in.instructions.at(block.instr_idx.back());
if (last_instr.fallthrough) {
// try to link to next block:
int next_idx = block.idx + 1;
if (next_idx < (int)cache->basic_blocks.size()) {
cache->basic_blocks.at(next_idx).pred.push_back(block.idx);
block.succ.push_back(next_idx);
}
}
for (auto target : last_instr.jumps) {
cache->basic_blocks.at(find_basic_block_to_target(target)).pred.push_back(block.idx);
block.succ.push_back(find_basic_block_to_target(target));
}
std::string LiveInfo::print_assignment() {
std::string result = "Assignment for var " + std::to_string(var) + "\n";
for (uint32_t i = 0; i < assignment.size(); i++) {
result += fmt::format("i[{:3d}] {}\n", i + min, assignment.at(i).to_string());
}
return result;
}
namespace {
@@ -96,7 +26,7 @@ void compute_live_ranges(RegAllocCache* cache, const AllocationInput& in) {
cache->live_ranges.resize(cache->max_var, LiveInfo(in.instructions.size(), 0));
// now compute the ranges
for (auto& block : cache->basic_blocks) {
for (auto& block : cache->control_flow.basic_blocks) {
// from var use
for (auto instr_id : block.instr_idx) {
auto& inst = in.instructions.at(instr_id);
@@ -147,7 +77,7 @@ void analyze_liveliness(RegAllocCache* cache, const AllocationInput& in) {
}
// phase 1
for (auto& block : cache->basic_blocks) {
for (auto& block : cache->control_flow.basic_blocks) {
block.live.resize(block.instr_idx.size());
block.dead.resize(block.instr_idx.size());
block.analyze_liveliness_phase1(in.instructions);
@@ -157,16 +87,16 @@ void analyze_liveliness(RegAllocCache* cache, const AllocationInput& in) {
bool changed = false;
do {
changed = false;
for (auto& block : cache->basic_blocks) {
if (block.analyze_liveliness_phase2(cache->basic_blocks, in.instructions)) {
for (auto& block : cache->control_flow.basic_blocks) {
if (block.analyze_liveliness_phase2(cache->control_flow.basic_blocks, in.instructions)) {
changed = true;
}
}
} while (changed);
// phase 3
for (auto& block : cache->basic_blocks) {
block.analyze_liveliness_phase3(cache->basic_blocks, in.instructions);
for (auto& block : cache->control_flow.basic_blocks) {
block.analyze_liveliness_phase3(cache->control_flow.basic_blocks, in.instructions);
}
// phase 4
@@ -194,141 +124,6 @@ void analyze_liveliness(RegAllocCache* cache, const AllocationInput& in) {
}
}
void RegAllocBasicBlock::analyze_liveliness_phase1(const std::vector<RegAllocInstr>& instructions) {
for (int i = instr_idx.size(); i-- > 0;) {
auto ii = instr_idx.at(i);
auto& instr = instructions.at(ii);
auto& lv = live.at(i);
auto& dd = dead.at(i);
// make all read live out
lv.clear();
for (auto& x : instr.read) {
lv.insert(x.id);
}
// kill things which are overwritten
dd.clear();
for (auto& x : instr.write) {
if (!lv[x.id]) {
dd.insert(x.id);
}
}
use.bitwise_and_not(dd);
use.bitwise_or(lv);
defs.bitwise_and_not(lv);
defs.bitwise_or(dd);
}
}
bool RegAllocBasicBlock::analyze_liveliness_phase2(std::vector<RegAllocBasicBlock>& blocks,
const std::vector<RegAllocInstr>& instructions) {
(void)instructions;
bool changed = false;
auto out = defs;
for (auto s : succ) {
out.bitwise_or(blocks.at(s).input);
}
IRegSet in = use;
IRegSet temp = out;
temp.bitwise_and_not(defs);
in.bitwise_or(temp);
if (in != input || out != output) {
changed = true;
input = in;
output = out;
}
return changed;
}
void RegAllocBasicBlock::analyze_liveliness_phase3(std::vector<RegAllocBasicBlock>& blocks,
const std::vector<RegAllocInstr>& instructions) {
(void)instructions;
IRegSet live_local;
for (auto s : succ) {
live_local.bitwise_or(blocks.at(s).input);
}
for (int i = instr_idx.size(); i-- > 0;) {
auto& lv = live.at(i);
auto& dd = dead.at(i);
IRegSet new_live = live_local;
new_live.bitwise_and_not(dd);
new_live.bitwise_or(lv);
lv = live_local;
live_local = new_live;
}
}
std::string RegAllocBasicBlock::print_summary() {
std::string result = "block " + std::to_string(idx) + "\nsucc: ";
for (auto s : succ) {
result += std::to_string(s) + " ";
}
result += "\npred: ";
for (auto p : pred) {
result += std::to_string(p) + " ";
}
result += "\nuse: ";
for (int x = 0; x < use.size(); x++) {
if (use[x]) {
result += std::to_string(x) + " ";
}
}
result += "\ndef: ";
for (int x = 0; x < defs.size(); x++) {
if (defs[x]) {
result += std::to_string(x) + " ";
}
}
result += "\ninput: ";
for (int x = 0; x < input.size(); x++) {
if (input[x]) {
result += std::to_string(x) + " ";
}
}
result += "\noutput: ";
for (int x = 0; x < output.size(); x++) {
if (output[x]) {
result += std::to_string(x) + " ";
}
}
return result;
}
std::string RegAllocBasicBlock::print(const std::vector<RegAllocInstr>& insts) {
std::string result = print_summary() + "\n";
int k = 0;
for (auto instr : instr_idx) {
std::string line = insts.at(instr).print();
constexpr int pad_len = 30;
if (line.length() < pad_len) {
// line.insert(line.begin(), pad_len - line.length(), ' ');
line.append(pad_len - line.length(), ' ');
}
result += " " + line + " live: ";
for (int x = 0; x < live.at(k).size(); x++) {
if (live.at(k)[x]) {
result += std::to_string(x) + " ";
}
}
result += "\n";
k++;
}
return result;
}
/*!
* Assign registers which are constrained. If constraints are inconsistent, won't succeed in
* satisfying them (of course), and won't error either. Use check_constrained_alloc to confirm
@@ -794,6 +589,12 @@ bool try_spill_coloring(int var, RegAllocCache* cache, const AllocationInput& in
if (bonus.load || bonus.store) {
cache->stack_ops.at(instr).ops.push_back(bonus);
if (bonus.load) {
cache->stats.num_spill_ops++;
}
if (bonus.store) {
cache->stats.num_spill_ops++;
}
}
}
return true;
@@ -910,3 +711,137 @@ bool run_allocator(RegAllocCache* cache, const AllocationInput& in, int debug_tr
}
return true;
}
namespace {
/*!
* Print out the state of the RegAllocCache after doing analysis.
*/
void print_analysis(const AllocationInput& in, RegAllocCache* cache) {
fmt::print("[RegAlloc] Basic Blocks\n");
fmt::print("-----------------------------------------------------------------\n");
for (auto& b : cache->control_flow.basic_blocks) {
fmt::print("{}\n", b.print(in.instructions));
}
printf("[RegAlloc] Alive Info\n");
printf("-----------------------------------------------------------------\n");
// align to where we start putting live stuff
printf(" %30s ", "");
for (int i = 0; i < cache->max_var; i++) {
printf("%2d ", i);
}
printf("\n");
printf("_________________________________________________________________\n");
for (uint32_t i = 0; i < in.instructions.size(); i++) {
std::vector<bool> ids_live;
std::string lives;
ids_live.resize(cache->max_var, false);
for (int j = 0; j < cache->max_var; j++) {
if (cache->live_ranges.at(j).is_live_at_instr(i)) {
ids_live.at(j) = true;
}
}
for (uint32_t j = 0; j < ids_live.size(); j++) {
if (ids_live[j]) {
char buff[256];
sprintf(buff, "%2d ", (int)j);
lives.append(buff);
} else {
lives.append(".. ");
}
}
if (in.debug_instruction_names.size() == in.instructions.size()) {
std::string code_str = in.debug_instruction_names.at(i);
if (code_str.length() >= 50) {
code_str = code_str.substr(0, 48);
code_str.push_back('~');
}
printf("[%03d] %30s -> %s\n", (int)i, code_str.c_str(), lives.c_str());
} else {
printf("[%03d] %30s -> %s\n", (int)i, "???", lives.c_str());
}
}
}
} // namespace
/*!
* The top-level register allocation algorithm!
*/
AllocationResult allocate_registers(const AllocationInput& input) {
AllocationResult result;
RegAllocCache cache;
cache.is_asm_func = input.is_asm_function;
// if desired, print input for debugging.
if (input.debug_settings.print_input) {
print_allocate_input(input);
}
// first step is analysis
find_basic_blocks(&cache.control_flow, input);
analyze_liveliness(&cache, input);
if (input.debug_settings.print_analysis) {
print_analysis(input, &cache);
}
// do constraints first, to get them out of the way
do_constrained_alloc(&cache, input, input.debug_settings.trace_debug_constraints);
// the user may have specified invalid constraints, so we should attempt to find conflicts now
// rather than having the register allocation mysteriously fail later on or silently ignore a
// constraint.
if (!check_constrained_alloc(&cache, input)) {
result.ok = false;
fmt::print("[RegAlloc Error] Register allocation has failed due to bad constraints.\n");
return result;
}
// do the allocations!
if (!run_allocator(&cache, input, input.debug_settings.allocate_log_level)) {
result.ok = false;
fmt::print("[RegAlloc Error] Register allocation has failed.\n");
return result;
}
// prepare the result
result.ok = true;
result.needs_aligned_stack_for_spills = cache.used_stack;
result.stack_slots_for_spills = cache.current_stack_slot;
result.stack_slots_for_vars = input.stack_slots_for_stack_vars;
// check for use of saved registers
for (auto sr : emitter::gRegInfo.get_all_saved()) {
bool uses_sr = false;
for (auto& lr : cache.live_ranges) {
for (int instr_idx = lr.min; instr_idx <= lr.max; instr_idx++) {
if (lr.get(instr_idx).reg == sr) {
uses_sr = true;
break;
}
}
if (uses_sr) {
break;
}
}
if (uses_sr) {
result.used_saved_regs.push_back(sr);
}
}
// result.ass_as_ranges = std::move(cache.live_ranges);
for (auto& lr : cache.live_ranges) {
result.ass_as_ranges.push_back(AssignmentRange(lr.min, lr.is_alive, lr.assignment));
}
result.stack_ops = std::move(cache.stack_ops);
// final result print
if (input.debug_settings.print_result) {
print_result(input, result);
}
result.num_spills = cache.stats.num_spill_ops;
return result;
}
+201 -24
View File
@@ -1,34 +1,208 @@
#pragma once
#ifndef JAK_ALLOCATOR_H
#define JAK_ALLOCATOR_H
#include <vector>
#include <stdexcept>
#include "IRegSet.h"
#include <unordered_map>
#include "IRegister.h"
#include "allocate.h"
#include "allocator_interface.h"
struct RegAllocBasicBlock {
std::vector<int> instr_idx;
std::vector<int> succ;
std::vector<int> pred;
std::vector<IRegSet> live, dead;
IRegSet use, defs, input, output;
bool is_entry = false;
bool is_exit = false;
int idx = -1;
void analyze_liveliness_phase1(const std::vector<RegAllocInstr>& instructions);
bool analyze_liveliness_phase2(std::vector<RegAllocBasicBlock>& blocks,
const std::vector<RegAllocInstr>& instructions);
void analyze_liveliness_phase3(std::vector<RegAllocBasicBlock>& blocks,
const std::vector<RegAllocInstr>& instructions);
std::string print(const std::vector<RegAllocInstr>& insts);
std::string print_summary();
// with this on, gaps in usage of registers allow other variables to steal registers.
// this reduces stack spills/moves, but may make register allocation slower.
constexpr bool enable_fancy_coloring = true;
// will attempt to allocate in a way to reduce the number of moves.
constexpr bool move_eliminator = true;
constexpr bool allow_read_write_same_reg = true;
// Indication of where a variable is live and what assignment it has at each point in the range.
struct LiveInfo {
public:
LiveInfo(int start, int end) : min(start), max(end) {}
// min, max are inclusive.
// meaning the variable written for the first time at min, and read for the last time at max.
int min, max;
std::vector<bool> is_alive;
std::vector<int> indices_of_alive;
// which variable is this?
int var = -1;
// have we actually seen this variable in the code?
bool seen = false;
// does this variable have a constraint?
bool has_constraint = false;
// the assignment of this variable at each instruction in [min, max]
std::vector<Assignment> assignment;
// a hint on where to put this variable.
Assignment best_hint;
/*!
* Add an instruction id where this variable is live.
*/
void add_live_instruction(int value) {
assert(value >= 0);
if (value > max)
max = value;
if (value < min)
min = value;
indices_of_alive.push_back(value);
// remember that this variable is actually used
seen = true;
}
/*!
* Is the given instruction contained in the live range?
*/
bool is_live_at_instr(int value) const {
if (value >= min && value <= max) {
if (enable_fancy_coloring) {
return is_alive.at(value - min);
} else {
return true;
}
}
return false;
}
/*!
* Are we alive at idx, but not alive at idx - 1 (or idx - 1 doesn't exist)
*/
bool becomes_live_at_instr(int idx) {
if (enable_fancy_coloring) {
if (idx == min)
return true;
if (idx < min || idx > max)
return false;
assert(idx > min);
return is_alive.at(idx - min) && !is_alive.at(idx - min - 1);
} else {
return idx == min;
}
}
/*!
* Are we alive at idx, but not alive at idx + 1 (or idx + 1 doesn't exist)
*/
bool dies_next_at_instr(int idx) {
if (enable_fancy_coloring) {
if (idx == max)
return true;
if (idx < min || idx > max)
return false;
assert(idx < max);
return is_alive.at(idx - min) && !is_alive.at(idx - min + 1);
} else {
return idx == max;
}
}
/*!
* Resize Live Range after instructions have been added. Do this before assigning.
*/
void prepare_for_allocation(int id) {
var = id;
if (!seen)
return; // don't do any prep for a variable which isn't used.
assert(max - min >= 0);
assignment.resize(max - min + 1);
is_alive.resize(max - min + 1);
for (auto& x : indices_of_alive) {
is_alive.at(x - min) = true;
}
}
/*!
* Lock an assignment at a given instruction.
* Will overwrite any previous assignment here
* Will set best_hint to this assignment.
*/
void constrain_at_one(int id, emitter::Register reg) {
assert(id >= min && id <= max);
Assignment ass;
ass.reg = reg;
ass.kind = Assignment::Kind::REGISTER;
assignment.at(id - min) = ass;
has_constraint = true;
best_hint = ass;
}
/*!
* Lock an assignment everywhere.
*/
void constrain_everywhere(emitter::Register reg) {
Assignment ass;
ass.reg = reg;
ass.kind = Assignment::Kind::REGISTER;
for (int i = min; i <= max; i++) {
assignment.at(i - min) = ass;
}
has_constraint = true;
best_hint = ass;
}
/*!
* At the given instruction, does the given assignment conflict with this one?
*/
bool conflicts_at(int id, Assignment ass) {
assert(id >= min && id <= max);
return assignment.at(id - min).occupies_same_reg(ass);
}
/*!
* At the given instruction, does the given assignment conflict with this one?
*/
bool conflicts_at(int id, emitter::Register reg) {
assert(id >= min && id <= max);
Assignment ass;
ass.reg = reg;
ass.kind = Assignment::Kind::REGISTER;
return assignment.at(id - min).occupies_same_reg(ass);
}
/*!
* Assign variable to the given assignment at all instructions
* Throws if this would require modifying a currently set assignment.
*/
void assign_no_overwrite(Assignment ass) {
assert(seen);
assert(ass.is_assigned());
for (int i = min; i <= max; i++) {
auto& a = assignment.at(i - min);
if (a.is_assigned() && !(a.occupies_same_reg(ass))) {
throw std::runtime_error("assign_no_overwrite failed!");
} else {
a = ass;
}
}
}
/*!
* Get the assignment at the given instruction.
*/
const Assignment& get(int id) const {
assert(id >= min && id <= max);
return assignment.at(id - min);
}
int size() const { return 1 + max - min; }
bool overlaps(const LiveInfo& other) const {
auto overlap_min = std::max(min, other.min);
auto overlap_max = std::min(max, other.max);
return overlap_min <= overlap_max;
}
std::string print_assignment();
};
struct RegAllocCache {
std::vector<RegAllocBasicBlock> basic_blocks;
ControlFlowAnalysisCache control_flow;
std::vector<LiveInfo> live_ranges;
int max_var = -1;
std::vector<bool> was_colored;
@@ -39,13 +213,16 @@ struct RegAllocCache {
bool used_stack = false;
bool is_asm_func = false;
struct Stats {
int num_spill_ops = 0;
} stats;
std::vector<std::vector<int>> live_ranges_by_instr;
};
void find_basic_blocks(RegAllocCache* cache, const AllocationInput& in);
void find_basic_blocks(ControlFlowAnalysisCache* cache, const AllocationInput& in);
void analyze_liveliness(RegAllocCache* cache, const AllocationInput& in);
void do_constrained_alloc(RegAllocCache* cache, const AllocationInput& in, bool trace_debug);
bool check_constrained_alloc(RegAllocCache* cache, const AllocationInput& in);
bool run_allocator(RegAllocCache* cache, const AllocationInput& in, int debug_trace);
#endif // JAK_ALLOCATOR_H
AllocationResult allocate_registers(const AllocationInput& input);
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include "goalc/regalloc/allocator_interface.h"
// Allocator v2's interface
AllocationResult allocate_registers_v2(const AllocationInput& input);
-260
View File
@@ -1,260 +0,0 @@
/*!
* @file allocate.cpp
* Runs the register allocator.
*/
#include "third-party/fmt/core.h"
#include "allocate.h"
#include "Allocator.h"
namespace {
/*!
* Print out the input data for debugging.
*/
void print_allocate_input(const AllocationInput& in) {
fmt::print("[RegAlloc] Debug Input Program:\n");
if (in.instructions.size() == in.debug_instruction_names.size()) {
for (size_t i = 0; i < in.instructions.size(); i++) {
// fmt::print(" [{}] {} -> {}\n", in.debug_instruction_names.at(i),
// in.instructions.at(i).print());
fmt::print(" [{:3d}] {:30} -> {:30}\n", i, in.debug_instruction_names.at(i),
in.instructions.at(i).print());
}
} else {
for (const auto& instruction : in.instructions) {
fmt::print(" [{:3d}] {}\n", instruction.print());
}
}
fmt::print("[RegAlloc] Debug Input Constraints:\n");
for (const auto& c : in.constraints) {
fmt::print(" {}\n", c.to_string());
}
fmt::print("\n");
}
/*!
* Print out the state of the RegAllocCache after doing analysis.
*/
void print_analysis(const AllocationInput& in, RegAllocCache* cache) {
fmt::print("[RegAlloc] Basic Blocks\n");
fmt::print("-----------------------------------------------------------------\n");
for (auto& b : cache->basic_blocks) {
fmt::print("{}\n", b.print(in.instructions));
}
printf("[RegAlloc] Alive Info\n");
printf("-----------------------------------------------------------------\n");
// align to where we start putting live stuff
printf(" %30s ", "");
for (int i = 0; i < cache->max_var; i++) {
printf("%2d ", i);
}
printf("\n");
printf("_________________________________________________________________\n");
for (uint32_t i = 0; i < in.instructions.size(); i++) {
std::vector<bool> ids_live;
std::string lives;
ids_live.resize(cache->max_var, false);
for (int j = 0; j < cache->max_var; j++) {
if (cache->live_ranges.at(j).is_live_at_instr(i)) {
ids_live.at(j) = true;
}
}
for (uint32_t j = 0; j < ids_live.size(); j++) {
if (ids_live[j]) {
char buff[256];
sprintf(buff, "%2d ", j);
lives.append(buff);
} else {
lives.append(".. ");
}
}
if (in.debug_instruction_names.size() == in.instructions.size()) {
std::string code_str = in.debug_instruction_names.at(i);
if (code_str.length() >= 50) {
code_str = code_str.substr(0, 48);
code_str.push_back('~');
}
printf("[%03d] %30s -> %s\n", i, code_str.c_str(), lives.c_str());
} else {
printf("[%03d] %30s -> %s\n", i, "???", lives.c_str());
}
}
}
/*!
* Print the result of register allocation for debugging.
*/
void print_result(const AllocationInput& in, const AllocationResult& result) {
printf("[RegAlloc] result:\n");
printf("-----------------------------------------------------------------\n");
for (uint32_t i = 0; i < in.instructions.size(); i++) {
std::vector<bool> ids_live;
std::string lives;
ids_live.resize(in.max_vars, false);
for (int j = 0; j < in.max_vars; j++) {
if (result.ass_as_ranges.at(j).is_live_at_instr(i)) {
lives += std::to_string(j) + " " + result.ass_as_ranges.at(j).get(i).to_string() + " ";
}
}
std::string code_str;
if (in.debug_instruction_names.size() == in.instructions.size()) {
code_str = in.debug_instruction_names.at(i);
}
if (code_str.length() >= 50) {
code_str = code_str.substr(0, 48);
code_str.push_back('~');
}
printf("[%03d] %30s | %30s | %30s\n", i, code_str.c_str(), lives.c_str(),
result.stack_ops.at(i).print().c_str());
}
}
} // namespace
/*!
* The top-level register allocation algorithm!
*/
AllocationResult allocate_registers(const AllocationInput& input) {
AllocationResult result;
RegAllocCache cache;
cache.is_asm_func = input.is_asm_function;
// if desired, print input for debugging.
if (input.debug_settings.print_input) {
print_allocate_input(input);
}
// first step is analysis
find_basic_blocks(&cache, input);
analyze_liveliness(&cache, input);
if (input.debug_settings.print_analysis) {
print_analysis(input, &cache);
}
// do constraints first, to get them out of the way
do_constrained_alloc(&cache, input, input.debug_settings.trace_debug_constraints);
// the user may have specified invalid constraints, so we should attempt to find conflicts now
// rather than having the register allocation mysteriously fail later on or silently ignore a
// constraint.
if (!check_constrained_alloc(&cache, input)) {
result.ok = false;
fmt::print("[RegAlloc Error] Register allocation has failed due to bad constraints.\n");
return result;
}
// do the allocations!
if (!run_allocator(&cache, input, input.debug_settings.allocate_log_level)) {
result.ok = false;
fmt::print("[RegAlloc Error] Register allocation has failed.\n");
return result;
}
// prepare the result
result.ok = true;
result.needs_aligned_stack_for_spills = cache.used_stack;
result.stack_slots_for_spills = cache.current_stack_slot;
result.stack_slots_for_vars = input.stack_slots_for_stack_vars;
// copy over the assignment result
result.assignment.resize(cache.max_var);
for (size_t i = 0; i < result.assignment.size(); i++) {
auto& x = result.assignment[i];
x.resize(input.instructions.size());
const auto& lr = cache.live_ranges.at(i);
for (int j = lr.min; j <= lr.max; j++) {
x.at(j) = lr.get(j);
}
}
// check for use of saved registers
for (auto sr : emitter::gRegInfo.get_all_saved()) {
bool uses_sr = false;
for (auto& lr : cache.live_ranges) {
for (int instr_idx = lr.min; instr_idx <= lr.max; instr_idx++) {
if (lr.get(instr_idx).reg == sr) {
uses_sr = true;
break;
}
}
if (uses_sr) {
break;
}
}
if (uses_sr) {
result.used_saved_regs.push_back(sr);
}
}
result.ass_as_ranges = std::move(cache.live_ranges);
result.stack_ops = std::move(cache.stack_ops);
// final result print
if (input.debug_settings.print_result) {
print_result(input, result);
}
return result;
}
/*!
* Print for debugging
*/
std::string RegAllocInstr::print() const {
bool first = true;
std::string result = "(";
if (!write.empty()) {
first = false;
result += "(write";
for (auto& i : write) {
result += " " + i.to_string();
}
result += ")";
}
if (!read.empty()) {
if (!first) {
result += " ";
}
first = false;
result += "(read";
for (auto& i : read) {
result += " " + i.to_string();
}
result += ")";
}
if (!clobber.empty()) {
if (!first) {
result += " ";
}
first = false;
result += "(clobber";
for (auto& i : clobber) {
result += " " + i.print();
}
result += ")";
}
if (!jumps.empty()) {
if (!first) {
result += " ";
}
first = false;
result += "(jumps";
for (auto& i : jumps) {
result += " " + std::to_string(i);
}
result += ")";
}
result += ")";
return result;
}
-118
View File
@@ -1,118 +0,0 @@
#pragma once
/*!
* @file allocate.h
* Interface for the register allocator.
*
* The IR is translated to RegAllocInstrs, which are added to a RegAllocFunc.
* These, plus any additional info are put in a AllocationInput, which is processed by the
* allocate_registers algorithm.
*/
#include <vector>
#include <unordered_set>
#include "goalc/emitter/Register.h"
#include "IRegister.h"
#include "allocate_common.h"
/*!
* Information about an instruction needed for register allocation.
* The model is this:
* instruction reads all read registers
* instruction writes junk into all clobber registers
* instruction writes all write registers
*
* The "exclude" registers cannot be used at any time during this instruction, for any reason.
* Possibly because the actual implementation requires using it.
*/
struct RegAllocInstr {
std::vector<emitter::Register> clobber; // written, but safe to use as input/output
std::vector<emitter::Register> exclude; // written, unsafe to use for input/output
std::vector<IRegister> write; // results go in here
std::vector<IRegister> read; // inputs go in here
std::vector<int> jumps; // RegAllocInstr indexes of possible jumps
bool fallthrough = true; // can it fall through to the next instruction
bool is_move = false; // is this a move?
std::string print() const;
/*!
* Does this read IReg id?
*/
bool reads(int id) const {
for (const auto& x : read) {
if (x.id == id)
return true;
}
return false;
}
/*!
* Does this write IReg id?
*/
bool writes(int id) const {
for (const auto& x : write) {
if (x.id == id)
return true;
}
return false;
}
};
/*!
* Result of the allocate_registers algorithm
*/
struct AllocationResult {
bool ok = false; // did it work?
std::vector<std::vector<Assignment>> assignment; // variable, instruction
std::vector<LiveInfo> ass_as_ranges; // another format, maybe easier?
std::vector<emitter::Register> used_saved_regs; // which saved regs get clobbered?
int stack_slots_for_spills = 0; // how many space on the stack do we need?
int stack_slots_for_vars = 0;
std::vector<StackOp> stack_ops; // additional instructions to spill/restore
bool needs_aligned_stack_for_spills = false;
// we put the variables before the spills so the variables are 16-byte aligned.
int total_stack_slots() const { return stack_slots_for_spills + stack_slots_for_vars; }
int get_slot_for_var(int slot) const {
assert(slot < stack_slots_for_vars);
return slot;
}
int get_slot_for_spill(int slot) const {
assert(slot < stack_slots_for_spills);
return slot + stack_slots_for_vars;
}
};
/*!
* Input to the allocate_registers algorithm
*/
struct AllocationInput {
std::vector<RegAllocInstr> instructions; // all instructions in the function
std::vector<IRegConstraint> constraints; // all register constraints
std::unordered_set<int> force_on_stack_regs; // registers which must be on the stack
int max_vars = -1; // maximum register id.
std::vector<std::string> debug_instruction_names; // optional, for debug prints
int stack_slots_for_stack_vars = 0;
bool is_asm_function = false;
struct {
bool print_input = false;
bool print_analysis = false;
bool trace_debug_constraints = false;
int allocate_log_level = 0;
bool print_result = false;
} debug_settings;
/*!
* Add instruction and return its idx.
*/
int add_instruction(const RegAllocInstr& instr) {
instructions.push_back(instr);
return int(instructions.size()) - 1;
}
};
AllocationResult allocate_registers(const AllocationInput& input);
-54
View File
@@ -1,54 +0,0 @@
#include "third-party/fmt/core.h"
#include "allocate_common.h"
std::string StackOp::print() const {
std::string result;
bool added = false;
for (const auto& op : ops) {
if (op.load) {
result += fmt::format("{} <- [{:2d}], ", emitter::gRegInfo.get_info(op.reg).name, op.slot);
added = true;
}
if (op.store) {
result += fmt::format("{} -> [{:2d}], ", emitter::gRegInfo.get_info(op.reg).name, op.slot);
added = true;
}
}
if (added) {
result.pop_back();
result.pop_back();
}
return result;
}
std::string Assignment::to_string() const {
std::string result;
if (spilled) {
result += "*";
}
switch (kind) {
case Kind::STACK:
result += fmt::format("s[{:2d}]", stack_slot);
break;
case Kind::REGISTER:
result += emitter::gRegInfo.get_info(reg).name;
break;
case Kind::UNASSIGNED:
result += "unassigned";
break;
default:
assert(false);
}
return result;
}
std::string LiveInfo::print_assignment() {
std::string result = "Assignment for var " + std::to_string(var) + "\n";
for (uint32_t i = 0; i < assignment.size(); i++) {
result += fmt::format("i[{:3d}] {}\n", i + min, assignment.at(i).to_string());
}
return result;
}
-237
View File
@@ -1,237 +0,0 @@
#ifndef JAK_ALLOCATE_COMMON_H
#define JAK_ALLOCATE_COMMON_H
#include <vector>
#include <stdexcept>
#include "goalc/emitter/Register.h"
/*!
* An operation that's added to an Instruction so that it loads/stores things from the stack if
* needed for spilling.
*/
struct StackOp {
struct Op {
int slot = -1;
emitter::Register reg;
RegClass reg_class = RegClass::INVALID;
bool load = false; // load from reg before instruction?
bool store = false; // store into reg after instruction?
};
std::vector<Op> ops;
std::string print() const;
};
/*!
* The assignment of an IRegister to a real Register.
* For a single IR Instruction.
*/
struct Assignment {
enum class Kind { STACK, REGISTER, UNASSIGNED } kind = Kind::UNASSIGNED;
emitter::Register reg = -1; //! where the IRegister is now
int stack_slot = -1; //! index of the slot, if we are ever spilled
bool spilled = false; //! are we ever spilled
std::string to_string() const;
bool occupies_same_reg(const Assignment& other) const { return other.reg == reg && (reg != -1); }
bool occupies_reg(emitter::Register other_reg) const { return reg == other_reg && (reg != -1); }
bool is_assigned() const { return kind != Kind::UNASSIGNED; }
};
// with this on, gaps in usage of registers allow other variables to steal registers.
// this reduces stack spills/moves, but may make register allocation slower.
constexpr bool enable_fancy_coloring = true;
// will attempt to allocate in a way to reduce the number of moves.
constexpr bool move_eliminator = true;
constexpr bool allow_read_write_same_reg = true;
// Indication of where a variable is live and what assignment it has at each point in the range.
struct LiveInfo {
public:
LiveInfo(int start, int end) : min(start), max(end) {}
// min, max are inclusive.
// meaning the variable written for the first time at min, and read for the last time at max.
int min, max;
std::vector<bool> is_alive;
std::vector<int> indices_of_alive;
// which variable is this?
int var = -1;
// have we actually seen this variable in the code?
bool seen = false;
// does this variable have a constraint?
bool has_constraint = false;
// the assignment of this variable at each instruction in [min, max]
std::vector<Assignment> assignment;
// a hint on where to put this variable.
Assignment best_hint;
/*!
* Add an instruction id where this variable is live.
*/
void add_live_instruction(int value) {
assert(value >= 0);
if (value > max)
max = value;
if (value < min)
min = value;
indices_of_alive.push_back(value);
// remember that this variable is actually used
seen = true;
}
/*!
* Is the given instruction contained in the live range?
*/
bool is_live_at_instr(int value) const {
if (value >= min && value <= max) {
if (enable_fancy_coloring) {
return is_alive.at(value - min);
} else {
return true;
}
}
return false;
}
/*!
* Are we alive at idx, but not alive at idx - 1 (or idx - 1 doesn't exist)
*/
bool becomes_live_at_instr(int idx) {
if (enable_fancy_coloring) {
if (idx == min)
return true;
if (idx < min || idx > max)
return false;
assert(idx > min);
return is_alive.at(idx - min) && !is_alive.at(idx - min - 1);
} else {
return idx == min;
}
}
/*!
* Are we alive at idx, but not alive at idx + 1 (or idx + 1 doesn't exist)
*/
bool dies_next_at_instr(int idx) {
if (enable_fancy_coloring) {
if (idx == max)
return true;
if (idx < min || idx > max)
return false;
assert(idx < max);
return is_alive.at(idx - min) && !is_alive.at(idx - min + 1);
} else {
return idx == max;
}
}
/*!
* Resize Live Range after instructions have been added. Do this before assigning.
*/
void prepare_for_allocation(int id) {
var = id;
if (!seen)
return; // don't do any prep for a variable which isn't used.
assert(max - min >= 0);
assignment.resize(max - min + 1);
is_alive.resize(max - min + 1);
for (auto& x : indices_of_alive) {
is_alive.at(x - min) = true;
}
}
/*!
* Lock an assignment at a given instruction.
* Will overwrite any previous assignment here
* Will set best_hint to this assignment.
*/
void constrain_at_one(int id, emitter::Register reg) {
assert(id >= min && id <= max);
Assignment ass;
ass.reg = reg;
ass.kind = Assignment::Kind::REGISTER;
assignment.at(id - min) = ass;
has_constraint = true;
best_hint = ass;
}
/*!
* Lock an assignment everywhere.
*/
void constrain_everywhere(emitter::Register reg) {
Assignment ass;
ass.reg = reg;
ass.kind = Assignment::Kind::REGISTER;
for (int i = min; i <= max; i++) {
assignment.at(i - min) = ass;
}
has_constraint = true;
best_hint = ass;
}
/*!
* At the given instruction, does the given assignment conflict with this one?
*/
bool conflicts_at(int id, Assignment ass) {
assert(id >= min && id <= max);
return assignment.at(id - min).occupies_same_reg(ass);
}
/*!
* At the given instruction, does the given assignment conflict with this one?
*/
bool conflicts_at(int id, emitter::Register reg) {
assert(id >= min && id <= max);
Assignment ass;
ass.reg = reg;
ass.kind = Assignment::Kind::REGISTER;
return assignment.at(id - min).occupies_same_reg(ass);
}
/*!
* Assign variable to the given assignment at all instructions
* Throws if this would require modifying a currently set assignment.
*/
void assign_no_overwrite(Assignment ass) {
assert(seen);
assert(ass.is_assigned());
for (int i = min; i <= max; i++) {
auto& a = assignment.at(i - min);
if (a.is_assigned() && !(a.occupies_same_reg(ass))) {
throw std::runtime_error("assign_no_overwrite failed!");
} else {
a = ass;
}
}
}
/*!
* Get the assignment at the given instruction.
*/
const Assignment& get(int id) const {
assert(id >= min && id <= max);
return assignment.at(id - min);
}
int size() const { return 1 + max - min; }
bool overlaps(const LiveInfo& other) const {
auto overlap_min = std::max(min, other.min);
auto overlap_max = std::min(max, other.max);
return overlap_min <= overlap_max;
}
std::string print_assignment();
};
#endif // JAK_ALLOCATE_COMMON_H
+379
View File
@@ -0,0 +1,379 @@
/*!
* @file allocate.cpp
* Runs the register allocator.
*/
#include <algorithm>
#include "third-party/fmt/core.h"
#include "allocator_interface.h"
/*!
* Print out the input data for debugging.
*/
void print_allocate_input(const AllocationInput& in) {
fmt::print("[RegAlloc] Debug Input Program:\n");
if (in.instructions.size() == in.debug_instruction_names.size()) {
for (size_t i = 0; i < in.instructions.size(); i++) {
// fmt::print(" [{}] {} -> {}\n", in.debug_instruction_names.at(i),
// in.instructions.at(i).print());
fmt::print(" [{:3d}] {:30} -> {:30}\n", i, in.debug_instruction_names.at(i),
in.instructions.at(i).print());
}
} else {
for (const auto& instruction : in.instructions) {
fmt::print(" [{:3d}] {}\n", instruction.print());
}
}
fmt::print("[RegAlloc] Debug Input Constraints:\n");
for (const auto& c : in.constraints) {
fmt::print(" {}\n", c.to_string());
}
fmt::print("\n");
}
/*!
* Print the result of register allocation for debugging.
*/
void print_result(const AllocationInput& in, const AllocationResult& result) {
printf("[RegAlloc] result:\n");
printf("-----------------------------------------------------------------\n");
for (uint32_t i = 0; i < in.instructions.size(); i++) {
std::vector<bool> ids_live;
std::string lives;
ids_live.resize(in.max_vars, false);
for (int j = 0; j < in.max_vars; j++) {
if (result.ass_as_ranges.at(j).is_live_at_instr(i)) {
lives += std::to_string(j) + " " + result.ass_as_ranges.at(j).get(i).to_string() + " ";
}
}
std::string code_str;
if (in.debug_instruction_names.size() == in.instructions.size()) {
code_str = in.debug_instruction_names.at(i);
}
if (code_str.length() >= 50) {
code_str = code_str.substr(0, 48);
code_str.push_back('~');
}
printf("[%03d] %30s | %30s | %30s\n", (int)i, code_str.c_str(), lives.c_str(),
result.stack_ops.at(i).print().c_str());
}
}
std::string Assignment::to_string() const {
std::string result;
if (spilled) {
result += "*";
}
switch (kind) {
case Kind::STACK:
result += fmt::format("s[{:2d}]", stack_slot);
break;
case Kind::REGISTER:
result += emitter::gRegInfo.get_info(reg).name;
break;
case Kind::UNASSIGNED:
result += "unassigned";
break;
default:
assert(false);
}
return result;
}
/*!
* Find basic blocks and add block link info.
*/
void find_basic_blocks(ControlFlowAnalysisCache* cache, const AllocationInput& in) {
std::vector<int> dividers;
dividers.push_back(0);
dividers.push_back(in.instructions.size());
// loop over instructions, finding jump targets
for (uint32_t i = 0; i < in.instructions.size(); i++) {
const auto& instr = in.instructions[i];
if (!instr.jumps.empty()) {
dividers.push_back(i + 1);
for (auto dest : instr.jumps) {
dividers.push_back(dest);
}
}
}
// sort dividers, and make blocks
std::sort(dividers.begin(), dividers.end(), [](int a, int b) { return a < b; });
for (uint32_t i = 0; i < dividers.size() - 1; i++) {
if (dividers[i] != dividers[i + 1]) {
// new basic block!
RegAllocBasicBlock block;
for (int j = dividers[i]; j < dividers[i + 1]; j++) {
block.instr_idx.push_back(j);
}
block.idx = cache->basic_blocks.size();
cache->basic_blocks.push_back(block);
}
}
if (!cache->basic_blocks.empty()) {
cache->basic_blocks.front().is_entry = true;
cache->basic_blocks.back().is_exit = true;
}
auto find_basic_block_to_target = [&](int instr) {
bool found = false;
uint32_t result = -1;
for (uint32_t i = 0; i < cache->basic_blocks.size(); i++) {
if (!cache->basic_blocks[i].instr_idx.empty() &&
cache->basic_blocks[i].instr_idx.front() == instr) {
assert(!found);
found = true;
result = i;
}
}
if (!found) {
printf("[RegAlloc Error] couldn't find basic block beginning with instr %d of %d\n", instr,
int(in.instructions.size()));
}
assert(found);
return result;
};
// link blocks
for (auto& block : cache->basic_blocks) {
assert(!block.instr_idx.empty());
auto& last_instr = in.instructions.at(block.instr_idx.back());
if (last_instr.fallthrough) {
// try to link to next block:
int next_idx = block.idx + 1;
if (next_idx < (int)cache->basic_blocks.size()) {
cache->basic_blocks.at(next_idx).pred.push_back(block.idx);
block.succ.push_back(next_idx);
}
}
for (auto target : last_instr.jumps) {
cache->basic_blocks.at(find_basic_block_to_target(target)).pred.push_back(block.idx);
block.succ.push_back(find_basic_block_to_target(target));
}
}
}
void RegAllocBasicBlock::analyze_liveliness_phase1(const std::vector<RegAllocInstr>& instructions) {
for (int i = instr_idx.size(); i-- > 0;) {
auto ii = instr_idx.at(i);
auto& instr = instructions.at(ii);
auto& lv = live.at(i);
auto& dd = dead.at(i);
// make all read live out
lv.clear();
for (auto& x : instr.read) {
lv.insert(x.id);
}
// kill things which are overwritten
dd.clear();
for (auto& x : instr.write) {
if (!lv[x.id]) {
dd.insert(x.id);
}
}
use.bitwise_and_not(dd);
use.bitwise_or(lv);
defs.bitwise_and_not(lv);
defs.bitwise_or(dd);
}
}
bool RegAllocBasicBlock::analyze_liveliness_phase2(std::vector<RegAllocBasicBlock>& blocks,
const std::vector<RegAllocInstr>& instructions) {
(void)instructions;
bool changed = false;
auto out = defs;
for (auto s : succ) {
out.bitwise_or(blocks.at(s).input);
}
IRegSet in = use;
IRegSet temp = out;
temp.bitwise_and_not(defs);
in.bitwise_or(temp);
if (in != input || out != output) {
changed = true;
input = in;
output = out;
}
return changed;
}
void RegAllocBasicBlock::analyze_liveliness_phase3(std::vector<RegAllocBasicBlock>& blocks,
const std::vector<RegAllocInstr>& instructions) {
(void)instructions;
IRegSet live_local;
for (auto s : succ) {
live_local.bitwise_or(blocks.at(s).input);
}
for (int i = instr_idx.size(); i-- > 0;) {
auto& lv = live.at(i);
auto& dd = dead.at(i);
IRegSet new_live = live_local;
new_live.bitwise_and_not(dd);
new_live.bitwise_or(lv);
lv = live_local;
live_local = new_live;
}
}
std::string RegAllocBasicBlock::print_summary() {
std::string result = "block " + std::to_string(idx) + "\nsucc: ";
for (auto s : succ) {
result += std::to_string(s) + " ";
}
result += "\npred: ";
for (auto p : pred) {
result += std::to_string(p) + " ";
}
result += "\nuse: ";
for (int x = 0; x < use.size(); x++) {
if (use[x]) {
result += std::to_string(x) + " ";
}
}
result += "\ndef: ";
for (int x = 0; x < defs.size(); x++) {
if (defs[x]) {
result += std::to_string(x) + " ";
}
}
result += "\ninput: ";
for (int x = 0; x < input.size(); x++) {
if (input[x]) {
result += std::to_string(x) + " ";
}
}
result += "\noutput: ";
for (int x = 0; x < output.size(); x++) {
if (output[x]) {
result += std::to_string(x) + " ";
}
}
return result;
}
std::string RegAllocBasicBlock::print(const std::vector<RegAllocInstr>& insts) {
std::string result = print_summary() + "\n";
int k = 0;
for (auto instr : instr_idx) {
std::string line = insts.at(instr).print();
constexpr int pad_len = 30;
if (line.length() < pad_len) {
// line.insert(line.begin(), pad_len - line.length(), ' ');
line.append(pad_len - line.length(), ' ');
}
result += " " + line + " live: ";
for (int x = 0; x < live.at(k).size(); x++) {
if (live.at(k)[x]) {
result += std::to_string(x) + " ";
}
}
result += "\n";
k++;
}
return result;
}
/*!
* Print for debugging
*/
std::string RegAllocInstr::print() const {
bool first = true;
std::string result = "(";
if (!write.empty()) {
first = false;
result += "(write";
for (auto& i : write) {
result += " " + i.to_string();
}
result += ")";
}
if (!read.empty()) {
if (!first) {
result += " ";
}
first = false;
result += "(read";
for (auto& i : read) {
result += " " + i.to_string();
}
result += ")";
}
if (!clobber.empty()) {
if (!first) {
result += " ";
}
first = false;
result += "(clobber";
for (auto& i : clobber) {
result += " " + i.print();
}
result += ")";
}
if (!jumps.empty()) {
if (!first) {
result += " ";
}
first = false;
result += "(jumps";
for (auto& i : jumps) {
result += " " + std::to_string(i);
}
result += ")";
}
result += ")";
return result;
}
std::string StackOp::print() const {
std::string result;
bool added = false;
for (const auto& op : ops) {
if (op.load) {
result += fmt::format("{} <- [{:2d}], ", emitter::gRegInfo.get_info(op.reg).name, op.slot);
added = true;
}
if (op.store) {
result += fmt::format("{} -> [{:2d}], ", emitter::gRegInfo.get_info(op.reg).name, op.slot);
added = true;
}
}
if (added) {
result.pop_back();
result.pop_back();
}
return result;
}
+209
View File
@@ -0,0 +1,209 @@
#pragma once
/*!
* @file allocate.h
* Interface for the register allocator.
*
* The IR is translated to RegAllocInstrs, which are added to a RegAllocFunc.
* These, plus any additional info are put in a AllocationInput, which is processed by the
* allocate_registers algorithm.
*/
#include <vector>
#include <unordered_set>
#include "goalc/emitter/Register.h"
#include "goalc/regalloc/IRegSet.h"
#include "goalc/regalloc/IRegister.h"
/*!
* Information about an instruction needed for register allocation.
* The model is this:
* instruction reads all read registers
* instruction writes junk into all clobber registers
* instruction writes all write registers
*
* The "exclude" registers cannot be used at any time during this instruction, for any reason.
* Possibly because the actual implementation requires using it.
*/
struct RegAllocInstr {
std::vector<emitter::Register> clobber; // written, but safe to use as input/output
std::vector<emitter::Register> exclude; // written, unsafe to use for input/output
std::vector<IRegister> write; // results go in here
std::vector<IRegister> read; // inputs go in here
std::vector<int> jumps; // RegAllocInstr indexes of possible jumps
bool fallthrough = true; // can it fall through to the next instruction
bool is_move = false; // is this a move?
std::string print() const;
/*!
* Does this read IReg id?
*/
bool reads(int id) const {
for (const auto& x : read) {
if (x.id == id)
return true;
}
return false;
}
/*!
* Does this write IReg id?
*/
bool writes(int id) const {
for (const auto& x : write) {
if (x.id == id)
return true;
}
return false;
}
};
/*!
* An operation that's added to an Instruction so that it loads/stores things from the stack if
* needed for spilling.
*/
struct StackOp {
struct Op {
int slot = -1;
emitter::Register reg;
RegClass reg_class = RegClass::INVALID;
bool load = false; // load from reg before instruction?
bool store = false; // store into reg after instruction?
};
std::vector<Op> ops;
std::string print() const;
};
/*!
* The assignment of an IRegister to a real Register.
* For a single IR Instruction.
*/
struct Assignment {
enum class Kind { STACK, REGISTER, UNASSIGNED } kind = Kind::UNASSIGNED;
emitter::Register reg = -1; //! where the IRegister is now
int stack_slot = -1; //! index of the slot, if we are ever spilled
bool spilled = false; //! are we ever spilled
std::string to_string() const;
bool occupies_same_reg(const Assignment& other) const { return other.reg == reg && (reg != -1); }
bool occupies_reg(emitter::Register other_reg) const { return reg == other_reg && (reg != -1); }
bool is_assigned() const { return kind != Kind::UNASSIGNED; }
};
class AssignmentRange {
public:
AssignmentRange(int start_instr,
const std::vector<bool>& live,
const std::vector<Assignment>& assignments)
: m_start(start_instr), m_live(live), m_ass(assignments) {
m_end = start_instr + live.size() - 1;
assert(m_live.size() == m_ass.size());
}
bool is_live_at_instr(int instr) const {
return has_info_at(instr) && m_live.at(instr - m_start);
}
const Assignment& get(int instr) const { return m_ass.at(instr - m_start); }
bool has_info_at(int instr) const { return instr >= m_start && instr <= m_end; }
int stack_slot() const { return m_ass.at(0).stack_slot; }
private:
int m_start = -1;
int m_end = -1; // INCLUSIVE!
std::vector<bool> m_live;
std::vector<Assignment> m_ass;
};
/*!
* Result of the allocate_registers algorithm
*/
struct AllocationResult {
bool ok = false; // did it work?
// std::vector<std::vector<Assignment>> assignment; // variable, instruction
std::vector<AssignmentRange> ass_as_ranges; // another format, maybe easier?
std::vector<emitter::Register> used_saved_regs; // which saved regs get clobbered?
int stack_slots_for_spills = 0; // how many space on the stack do we need?
int stack_slots_for_vars = 0;
std::vector<StackOp> stack_ops; // additional instructions to spill/restore
bool needs_aligned_stack_for_spills = false;
int num_spills = 0;
int num_spilled_vars = 0;
// we put the variables before the spills so the variables are 16-byte aligned.
int total_stack_slots() const { return stack_slots_for_spills + stack_slots_for_vars; }
int get_slot_for_var(int slot) const {
assert(slot < stack_slots_for_vars);
return slot;
}
int get_slot_for_spill(int slot) const {
assert(slot < stack_slots_for_spills);
return slot + stack_slots_for_vars;
}
};
/*!
* Input to the allocate_registers algorithm
*/
struct AllocationInput {
std::vector<RegAllocInstr> instructions; // all instructions in the function
std::vector<IRegConstraint> constraints; // all register constraints
std::unordered_set<int> force_on_stack_regs; // registers which must be on the stack
int max_vars = -1; // maximum register id.
std::vector<std::string> debug_instruction_names; // optional, for debug prints
int stack_slots_for_stack_vars = 0;
bool is_asm_function = false;
std::string function_name;
int allocator_version = 1;
struct {
bool print_input = false;
bool print_analysis = false;
bool trace_debug_constraints = false;
int allocate_log_level = 0;
bool print_result = false;
} debug_settings;
/*!
* Add instruction and return its idx.
*/
int add_instruction(const RegAllocInstr& instr) {
instructions.push_back(instr);
return int(instructions.size()) - 1;
}
};
struct RegAllocBasicBlock {
std::vector<int> instr_idx;
std::vector<int> succ;
std::vector<int> pred;
std::vector<IRegSet> live, dead;
IRegSet use, defs, input, output;
bool is_entry = false;
bool is_exit = false;
int idx = -1;
void analyze_liveliness_phase1(const std::vector<RegAllocInstr>& instructions);
bool analyze_liveliness_phase2(std::vector<RegAllocBasicBlock>& blocks,
const std::vector<RegAllocInstr>& instructions);
void analyze_liveliness_phase3(std::vector<RegAllocBasicBlock>& blocks,
const std::vector<RegAllocInstr>& instructions);
std::string print(const std::vector<RegAllocInstr>& insts);
std::string print_summary();
};
struct ControlFlowAnalysisCache {
std::vector<RegAllocBasicBlock> basic_blocks;
};
void print_allocate_input(const AllocationInput& in);
void print_result(const AllocationInput& in, const AllocationResult& result);
void find_basic_blocks(ControlFlowAnalysisCache* cache, const AllocationInput& in);
@@ -12,4 +12,5 @@
(let ((obj (new 'static 'type-with-weird-relocate :foo 123)))
(relocate obj (the kheap 1) (the (pointer uint8) 2))
0
)