From aa58d146c2476e554377c38de4821b10ac0499bf Mon Sep 17 00:00:00 2001 From: water111 <48171810+water111@users.noreply.github.com> Date: Sun, 1 Aug 2021 17:46:55 -0400 Subject: [PATCH] [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 --- common/util/Range.h | 2 + docs/markdown/progress-notes/changelog.md | 4 +- goal_src/engine/math/matrix.gc | 8 + goal_src/kernel/gkernel.gc | 18 +- goalc/CMakeLists.txt | 4 +- goalc/compiler/CodeGenerator.h | 1 + goalc/compiler/Compiler.cpp | 32 +- goalc/compiler/Compiler.h | 11 + goalc/compiler/Env.h | 2 +- goalc/compiler/IR.cpp | 7 +- goalc/compiler/IR.h | 2 +- goalc/compiler/compilation/Atoms.cpp | 1 + .../compiler/compilation/CompilerControl.cpp | 16 + goalc/compiler/compilation/Function.cpp | 17 +- goalc/compiler/compilation/Math.cpp | 4 + goalc/compiler/compilation/Type.cpp | 40 +- goalc/emitter/ObjectGenerator.cpp | 8 + goalc/emitter/ObjectGenerator.h | 13 +- goalc/regalloc/Allocator.cpp | 371 +++-- goalc/regalloc/Allocator.h | 225 ++- goalc/regalloc/Allocator_v2.cpp | 1213 +++++++++++++++++ goalc/regalloc/Allocator_v2.h | 6 + goalc/regalloc/allocate.cpp | 260 ---- goalc/regalloc/allocate.h | 118 -- goalc/regalloc/allocate_common.cpp | 54 - goalc/regalloc/allocate_common.h | 237 ---- goalc/regalloc/allocator_interface.cpp | 379 +++++ goalc/regalloc/allocator_interface.h | 209 +++ .../with_game/test-method-replace.gc | 1 + 29 files changed, 2314 insertions(+), 949 deletions(-) create mode 100644 goalc/regalloc/Allocator_v2.cpp create mode 100644 goalc/regalloc/Allocator_v2.h delete mode 100644 goalc/regalloc/allocate.cpp delete mode 100644 goalc/regalloc/allocate.h delete mode 100644 goalc/regalloc/allocate_common.cpp delete mode 100644 goalc/regalloc/allocate_common.h create mode 100644 goalc/regalloc/allocator_interface.cpp create mode 100644 goalc/regalloc/allocator_interface.h diff --git a/common/util/Range.h b/common/util/Range.h index f44f0555db..99f3018109 100644 --- a/common/util/Range.h +++ b/common/util/Range.h @@ -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; } diff --git a/docs/markdown/progress-notes/changelog.md b/docs/markdown/progress-notes/changelog.md index a74af4956d..9b7bfa52e8 100644 --- a/docs/markdown/progress-notes/changelog.md +++ b/docs/markdown/progress-notes/changelog.md @@ -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. \ No newline at end of file +- 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 \ No newline at end of file diff --git a/goal_src/engine/math/matrix.gc b/goal_src/engine/math/matrix.gc index 75bb41f644..50388f9566 100644 --- a/goal_src/engine/math/matrix.gc +++ b/goal_src/engine/math/matrix.gc @@ -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)) diff --git a/goal_src/kernel/gkernel.gc b/goal_src/kernel/gkernel.gc index 52bfc34f13..8d604fa6fc 100644 --- a/goal_src/kernel/gkernel.gc +++ b/goal_src/kernel/gkernel.gc @@ -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) ) ) diff --git a/goalc/CMakeLists.txt b/goalc/CMakeLists.txt index 02824890e8..ebbb50264e 100644 --- a/goalc/CMakeLists.txt +++ b/goalc/CMakeLists.txt @@ -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) diff --git a/goalc/compiler/CodeGenerator.h b/goalc/compiler/CodeGenerator.h index 6dad7e76a4..56d65c8931 100644 --- a/goalc/compiler/CodeGenerator.h +++ b/goalc/compiler/CodeGenerator.h @@ -17,6 +17,7 @@ class CodeGenerator { public: CodeGenerator(FileEnv* env, DebugInfo* debug_info); std::vector run(const TypeSystem* ts); + emitter::ObjectGeneratorStats get_obj_stats() const { return m_gen.get_stats(); } private: void do_function(FunctionEnv* env, int f_idx); diff --git a/goalc/compiler/Compiler.cpp b/goalc/compiler/Compiler.cpp index 37f702d729..6c14dcfa0e 100644 --- a/goalc/compiler/Compiler.cpp +++ b/goalc/compiler/Compiler.cpp @@ -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 Compiler::codegen_object_file(FileEnv* env) { @@ -239,6 +265,8 @@ std::vector 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()); diff --git a/goalc/compiler/Compiler.h b/goalc/compiler/Compiler.h index 2e126358ba..f1321e28e2 100644 --- a/goalc/compiler/Compiler.h +++ b/goalc/compiler/Compiler.h @@ -79,6 +79,14 @@ class Compiler { std::unique_ptr 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 lookup_symbol_infos_starting_with(const std::string& prefix) const; std::vector* 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); diff --git a/goalc/compiler/Env.h b/goalc/compiler/Env.h index 90ad79ab77..4a9d4edab5 100644 --- a/goalc/compiler/Env.h +++ b/goalc/compiler/Env.h @@ -13,7 +13,7 @@ #include #include #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" diff --git a/goalc/compiler/IR.cpp b/goalc/compiler/IR.cpp index 818ad96e6f..1b1c509b69 100644 --- a/goalc/compiler/IR.cpp +++ b/goalc/compiler/IR.cpp @@ -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); diff --git a/goalc/compiler/IR.h b/goalc/compiler/IR.h index 5f34b4b684..c56da37ab9 100644 --- a/goalc/compiler/IR.h +++ b/goalc/compiler/IR.h @@ -2,7 +2,7 @@ #include #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" diff --git a/goalc/compiler/compilation/Atoms.cpp b/goalc/compiler/compilation/Atoms.cpp index a27f5514a2..76df77923c 100644 --- a/goalc/compiler/compilation/Atoms.cpp +++ b/goalc/compiler/compilation/Atoms.cpp @@ -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}, diff --git a/goalc/compiler/compilation/CompilerControl.cpp b/goalc/compiler/compilation/CompilerControl.cpp index 05deb05568..9a96edac32 100644 --- a/goalc/compiler/compilation/CompilerControl.cpp +++ b/goalc/compiler/compilation/CompilerControl.cpp @@ -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(); } \ No newline at end of file diff --git a/goalc/compiler/compilation/Function.cpp b/goalc/compiler/compilation/Function.cpp index d282f5a025..e473d007db 100644 --- a/goalc/compiler/compilation/Function.cpp +++ b/goalc/compiler/compilation/Function.cpp @@ -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(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(ireg, reset_args_for_coloring.at(i)); + } + // compile the function, iterating through the body. Val* result = nullptr; bool first_thing = true; diff --git a/goalc/compiler/compilation/Math.cpp b/goalc/compiler/compilation/Math.cpp index 882f36fe01..2f85e2ae3a 100644 --- a/goalc/compiler/compilation/Math.cpp +++ b/goalc/compiler/compilation/Math.cpp @@ -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(result_moved, result); + return result_moved; } return result; diff --git a/goalc/compiler/compilation/Type.cpp b/goalc/compiler/compilation/Type.cpp index 8ad02235b3..f347364bd7 100644 --- a/goalc/compiler/compilation/Type.cpp +++ b/goalc/compiler/compilation/Type.cpp @@ -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(std::vector{input})); + method_env->emit(std::make_unique(std::vector{input_arg})); + + auto input = method_env->make_gpr(structure_type->get_name()); + method_env->emit_ir(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(std::vector{input})); + method_env->emit(std::make_unique(std::vector{input_arg})); + + auto input = method_env->make_gpr(bitfield_type->get_name()); + method_env->emit_ir(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(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(ireg, reset_args_for_coloring.at(i)); + } + // compile the function! Val* result = nullptr; bool first_thing = true; diff --git a/goalc/emitter/ObjectGenerator.cpp b/goalc/emitter/ObjectGenerator.cpp index 52c4c68732..3133345cda 100644 --- a/goalc/emitter/ObjectGenerator.cpp +++ b/goalc/emitter/ObjectGenerator.cpp @@ -615,4 +615,12 @@ std::vector ObjectGenerator::generate_header_v3() { push_data(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 \ No newline at end of file diff --git a/goalc/emitter/ObjectGenerator.h b/goalc/emitter/ObjectGenerator.h index ccf351595b..819e1dc7de 100644 --- a/goalc/emitter/ObjectGenerator.h +++ b/goalc/emitter/ObjectGenerator.h @@ -5,9 +5,6 @@ #pragma once -#ifndef JAK_OBJECTGENERATOR_H -#define JAK_OBJECTGENERATOR_H - #include #include #include @@ -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 m_pointer_links_by_seg; std::vector m_all_function_records; + + ObjectGeneratorStats m_stats; }; } // namespace emitter - -#endif // JAK_OBJECTGENERATOR_H diff --git a/goalc/regalloc/Allocator.cpp b/goalc/regalloc/Allocator.cpp index 9c20a5ae3f..addae2ae09 100644 --- a/goalc/regalloc/Allocator.cpp +++ b/goalc/regalloc/Allocator.cpp @@ -3,87 +3,17 @@ * Implementation of register allocation algorithms */ -#include +#include #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 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& 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& blocks, - const std::vector& 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& blocks, - const std::vector& 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& 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 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; +} \ No newline at end of file diff --git a/goalc/regalloc/Allocator.h b/goalc/regalloc/Allocator.h index 1d9181ec98..f86d246fbe 100644 --- a/goalc/regalloc/Allocator.h +++ b/goalc/regalloc/Allocator.h @@ -1,34 +1,208 @@ #pragma once -#ifndef JAK_ALLOCATOR_H -#define JAK_ALLOCATOR_H - #include +#include #include "IRegSet.h" #include #include "IRegister.h" -#include "allocate.h" +#include "allocator_interface.h" -struct RegAllocBasicBlock { - std::vector instr_idx; - std::vector succ; - std::vector pred; - std::vector 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& instructions); - bool analyze_liveliness_phase2(std::vector& blocks, - const std::vector& instructions); - void analyze_liveliness_phase3(std::vector& blocks, - const std::vector& instructions); - std::string print(const std::vector& 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 is_alive; + std::vector 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; + + // 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 basic_blocks; + ControlFlowAnalysisCache control_flow; std::vector live_ranges; int max_var = -1; std::vector 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> 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); \ No newline at end of file diff --git a/goalc/regalloc/Allocator_v2.cpp b/goalc/regalloc/Allocator_v2.cpp new file mode 100644 index 0000000000..846d3731e6 --- /dev/null +++ b/goalc/regalloc/Allocator_v2.cpp @@ -0,0 +1,1213 @@ +#include +#include +#include +#include "third-party/fmt/core.h" +#include "common/util/Range.h" +#include "Allocator_v2.h" + +/*! + Documentation: + + Compared to the first version, Allocator2 has several changes. + + First, the data structures used don't support splitting variables within the allocator. + Allocator1 never actually did any splitting but supported this. The reasons for this are: + - I can't figure out how the allocator can decide how to safely split, so any splitting would need + to happen outside of the allocator. Deciding when to insert splits seems like a pretty unsolved + problem, and harder to do greedily than adding in too many splits and coalescing. + - The OpenGOAL compiler doesn't reuse variables almost ever, meaning the only places where we + could insert splits is with user variables. + - Splitting user variables requires SSA, which would make sense to do outside the allocator. + - Splitting is likely to be highly ineffective on our automatically decompiled code because it + literally splits as much as safely possible as part of the register -> variable process. + + Second: We have a smarter approach for picking registers. + We do a few different passes: + - Do constrained allocations. + - Allocate for variables which cross a function boundary, trying to move-eliminate with previous. + - Allocate variables that could possibly be move eliminated with a previous allocation. + - (repeat the above step until no more are found) + - Allocate remaining variables + + Third: We have a new approach for spilling. + In the old allocator we had to reserve registers for use only in spill loads/stores. + In this design, we don't do this, but instead demote variables to the stack when we run out. + This means that we must support changing the assignment of a variable. + + All the "fancy" features of Allocator v1 will be supported by default: + - Fancy coloring (use interference graph, not live range overlap) + - Move Eliminator (try to eliminate move instructions) + - Allow Read Write Same Reg + + Future improvements: + - Within a basic block, try to drop load/store instructions for stack ops. + */ + +namespace { + +// set this to restrict the allocator to a small subset of registers. +// this has the effect of adding many spills even to very simple functions. +// It can be used to test less common spilling situations. +constexpr bool torture_test_spills = false; + +/*! + * The VarAssignment is the per-variable information used by the allocator. + * It "has_info" over a variable's live range. + * It also tracks individual instructions within this range and can tell you if the variable + * is actually alive at a given instruction within its live range. + */ +class VarAssignment { + public: + // The assignment state. + // Register: permanently in a register. + // Stack: by default in the stack, moved to temporary register as needed for use. + enum class Kind { UNASSIGNED, STACK, REGISTER }; + + // unassigned by default. + // last_live is inclusive + VarAssignment(int first_live, int last_live, int var) : m_first_live(first_live), m_var_idx(var) { + m_live.resize(1 + last_live - first_live, false); + } + + // Setup functions: + + // mark the given instruction as live. + void mark_live(int instr) { + m_live.at(instr - m_first_live) = true; + m_seen = true; + } + + // mark this variable as having a function call inside its live range. + // this will make it prefer saved registers. + void mark_crossing_function() { m_crosses_function_call = true; } + + // Get info functions + + // the number of instructions in the live range. + int range_size() { return m_live.size(); } + + // get the index of the first instruction in the live range + int first_live() const { return m_first_live; } + + // get the index of the last instruction in the live range (includes this instruction) + int last_live() const { return m_first_live + m_live.size() - 1; } + + // is the given instruction within the live range? + bool has_info_at(int instr) const { return instr >= first_live() && instr <= last_live(); } + + // is the variable "live" at the given instruction? + // This is true if it is "liveout", or if the instruction reads/write the register. + bool live(int instr) const { return has_info_at(instr) && m_live.at(instr - m_first_live); } + + // is the variable unassigned? + bool unassigned() const { return m_kind == Kind::UNASSIGNED; } + + // is the variable assigned to a register or to a stack slot + bool assigned() const { return m_kind != Kind::UNASSIGNED; } + + // is the variable assigned to any register? + bool assigned_to_reg() const { return m_kind == Kind::REGISTER; } + + bool assigned_to_stack() const { return m_kind == Kind::STACK; } + + // is the variable assigned to the given register? + bool assigned_to_reg(emitter::Register reg) const { + return assigned_to_reg() && m_assigned_register == reg; + } + + // get the register that this variable is assigned to. Must be assigned_to_reg() + emitter::Register reg() const { + assert(assigned_to_reg()); + return m_assigned_register; + } + + // get the variable index of the variable + int var() const { return m_var_idx; } + + // have we "seen" this variable in the IR? + bool seen() const { return m_seen; } + + // does the variable's live range cross a function call? + bool crosses_function() const { return m_crosses_function_call; } + + bool locked() const { return m_locked; } + + // Assignment functions: + + // put this variable in the given register and constrain it there (prevent spilling) + void constrain_to_register(const emitter::Register& reg) { + assert(unassigned()); + m_kind = Kind::REGISTER; + m_locked = true; + m_assigned_register = reg; + } + + void assign_to_register(const emitter::Register& reg) { + assert(unassigned()); + m_kind = Kind::REGISTER; + m_locked = false; + m_assigned_register = reg; + } + + void assign_to_stack(int slot) { + assert(unassigned()); + m_kind = Kind::STACK; + m_locked = false; + m_stack_temp_regs.resize(m_live.size()); + m_stack_slot = slot; + } + + void demote_to_stack(int slot) { + assert(assigned_to_reg()); + m_kind = Kind::STACK; + m_locked = false; + m_stack_temp_regs.resize(m_live.size()); + m_stack_slot = slot; + } + + void set_stack_slot_reg(const emitter::Register& reg, int instr_idx) { + assert(assigned_to_stack()); + assert(!m_stack_temp_regs.at(instr_idx - first_live())); + m_stack_temp_regs.at(instr_idx - first_live()) = reg; + } + + void clear_stack_slot_regs() { + for (auto& x : m_stack_temp_regs) { + x = {}; + } + } + + emitter::Register get_stack_slot_reg(int instr_idx) const { + assert(assigned_to_stack()); + assert(m_stack_temp_regs.at(instr_idx - first_live())); + return *m_stack_temp_regs.at(instr_idx - first_live()); + } + + bool stack_bonus_op_needs_reg(const emitter::Register& reg, int instr_idx) const { + assert(assigned_to_stack()); + auto& stack_reg = m_stack_temp_regs.at(instr_idx - m_first_live); + return stack_reg && (*stack_reg == reg); + } + + // result get + std::vector make_assignment_vector() const { + std::vector asses; + asses.reserve(m_live.size()); + if (assigned_to_reg()) { + for (int i = 0; i < (int)m_live.size(); i++) { + Assignment a; + a.kind = Assignment::Kind::REGISTER; + a.reg = m_assigned_register; + asses.push_back(a); + } + return asses; + } else if (assigned_to_stack()) { + for (int i = 0; i < (int)m_live.size(); i++) { + Assignment a; + a.kind = Assignment::Kind::STACK; + a.stack_slot = m_stack_slot; + auto& slot_reg = m_stack_temp_regs.at(i); + if (slot_reg) { + a.kind = Assignment::Kind::REGISTER; + a.reg = *slot_reg; + } + asses.push_back(a); + } + return asses; + } else { + assert(false); + } + } + + const std::vector live_vector() const { return m_live; } + + private: + // common info + Kind m_kind = Kind::UNASSIGNED; + int m_first_live = -1; // index of instr where we are first alive + int m_var_idx = -1; // which variable we are + std::vector m_live; // where we are live, starting at m_first_live. + + // register assignment only + emitter::Register m_assigned_register; // if we are REGISTER assigned, our register. + bool m_locked = false; // if we are a REGISTER, means we must stay in the given register. + bool m_crosses_function_call = false; + bool m_seen = false; + + // stack assignment only + int m_stack_slot = -1; + // starting at m_first_live, the temporary register we're assigned for stack slots. + std::vector> m_stack_temp_regs; +}; + +struct RACache { + ControlFlowAnalysisCache control_flow; + std::vector vars; // per var + std::vector was_allocated; // per var + std::vector used_var; // per var + std::vector iregs; + std::vector stack_ops; // per instr. + std::unordered_map var_to_stack_slot; + // list of live vars per instruction. + std::vector> live_per_instruction; + + std::vector liveout_per_instr; + int current_stack_slot = 0; + bool used_stack = false; + bool failed_alloc = false; + + struct Stats { + int var_count = 0; + int assign_passes = 0; + int num_spilled_vars = 0; + int num_spill_ops = 0; + } stats; +}; + +struct AssignmentOrder { + std::vector xmms, gprs; +}; + +AssignmentOrder REG_saved_first_order = { + {emitter::XMM8, emitter::XMM9, emitter::XMM10, emitter::XMM11, emitter::XMM12, emitter::XMM13, + emitter::XMM14, emitter::XMM15, emitter::XMM7, emitter::XMM6, emitter::XMM5, emitter::XMM4, + emitter::XMM3, emitter::XMM2, emitter::XMM1, emitter::XMM0}, + {emitter::RBX, emitter::RBP, emitter::R12, emitter::R11, emitter::R10, emitter::R9, emitter::R8, + emitter::RCX, emitter::RDX, emitter::RSI, emitter::RDI, emitter::RAX}}; + +AssignmentOrder REG_temp_first_order = { + {emitter::XMM7, emitter::XMM6, emitter::XMM5, emitter::XMM4, emitter::XMM3, emitter::XMM2, + emitter::XMM1, emitter::XMM0, emitter::XMM8, emitter::XMM9, emitter::XMM10, emitter::XMM11, + emitter::XMM12, emitter::XMM13, emitter::XMM14, emitter::XMM15}, + {emitter::R9, emitter::R8, emitter::RCX, emitter::RDX, emitter::RSI, emitter::RDI, emitter::RAX, + emitter::RBX, emitter::RBP, emitter::R12, emitter::R11, emitter::R10}}; + +AssignmentOrder REG_extra_hard_order = { + {emitter::XMM7, emitter::XMM6, emitter::XMM5, emitter::XMM4, emitter::XMM3, emitter::XMM2, + emitter::XMM1, emitter::XMM0, emitter::XMM8, emitter::XMM9}, + {emitter::R9, emitter::RSI, emitter::RDI, emitter::RAX, emitter::RBP, emitter::R12}}; + +AssignmentOrder REG_temp_only_order = {{emitter::XMM7, emitter::XMM6, emitter::XMM5, emitter::XMM4, + emitter::XMM3, emitter::XMM2, emitter::XMM1, emitter::XMM0}, + {emitter::R9, emitter::R8, emitter::RCX, emitter::RDX, + emitter::RSI, emitter::RDI, emitter::RAX}}; +std::vector allowable_local_var_move_elim = { + emitter::R9, emitter::R8, emitter::RCX, emitter::RDX, emitter::RSI, emitter::RDI, + emitter::RAX, emitter::RBX, emitter::RBP, emitter::R12, emitter::R11, emitter::R10, + emitter::XMM7, emitter::XMM6, emitter::XMM5, emitter::XMM4, emitter::XMM3, emitter::XMM2, + emitter::XMM1, emitter::XMM0, emitter::XMM8, emitter::XMM9, emitter::XMM10, emitter::XMM11, + emitter::XMM12, emitter::XMM13, emitter::XMM14, emitter::XMM15}; + +const std::vector& get_alloc_order(int var_idx, + const AllocationInput& in, + const RACache& cache, + bool saved_first) { + bool is_gpr = + emitter::reg_class_to_hw(cache.iregs.at(var_idx).reg_class) == emitter::HWRegKind::GPR; + if (in.is_asm_function) { + if (is_gpr) { + return REG_temp_only_order.gprs; + } else { + return REG_temp_only_order.xmms; + } + } else { + if (torture_test_spills) { + if (is_gpr) { + return REG_extra_hard_order.gprs; + } else { + return REG_extra_hard_order.xmms; + } + } + if (saved_first) { + if (is_gpr) { + return REG_saved_first_order.gprs; + } else { + return REG_saved_first_order.xmms; + } + } else { + if (is_gpr) { + return REG_temp_first_order.gprs; + } else { + return REG_temp_only_order.xmms; + } + } + } +} + +/*! + * Determine the instruction where each variable first becomes live. + * Return value is per-variable, the index of the instruction where it is first live. + */ +std::vector> find_live_range_instr(const AllocationInput& input, + ControlFlowAnalysisCache& cfa) { + std::vector> result; + result.resize(input.max_vars, Range(INT32_MAX, INT32_MIN)); + + for (auto& block : cfa.basic_blocks) { + for (auto instr_idx : block.instr_idx) { + const auto& inst = input.instructions.at(instr_idx); + for (auto& rd : inst.read) { + result.at(rd.id).first() = std::min(result.at(rd.id).first(), instr_idx); + result.at(rd.id).last() = std::max(result.at(rd.id).last(), instr_idx); + } + for (auto& wr : inst.write) { + result.at(wr.id).first() = std::min(result.at(wr.id).first(), instr_idx); + result.at(wr.id).last() = std::max(result.at(wr.id).last(), instr_idx); + } + } + + assert(block.live.size() == block.instr_idx.size()); + for (uint32_t i = 0; i < block.live.size(); i++) { + for (int j = 0; j < block.live[i].size(); j++) { + if (block.live[i][j]) { + int instr_idx = block.instr_idx.at(i); + result.at(j).first() = std::min(result.at(j).first(), instr_idx); + result.at(j).last() = std::max(result.at(j).last(), instr_idx); + } + } + } + } + + // make us alive at any constrained instruction. todo, if this happens is this a sign of an issue + for (auto& con : input.constraints) { + if (!con.contrain_everywhere) { + result.at(con.ireg.id).first() = std::min(result.at(con.ireg.id).first(), con.instr_idx); + result.at(con.ireg.id).last() = std::max(result.at(con.ireg.id).last(), con.instr_idx); + } + } + + return result; +} + +/*! + * Initialize all VarAssignments as unassigned, with the appropriate live range. + */ +std::vector initialize_unassigned(const std::vector>& live_ranges, + const AllocationInput& input, + ControlFlowAnalysisCache& cfa) { + // allocate + std::vector result; + result.reserve(input.max_vars); + assert(input.max_vars == (int)live_ranges.size()); + int var_idx = 0; + for (auto lr : live_ranges) { + result.emplace_back(lr.first(), lr.last(), var_idx++); + } + + // now compute the ranges + for (auto& block : cfa.basic_blocks) { + // from var use + for (auto instr_id : block.instr_idx) { + auto& inst = input.instructions.at(instr_id); + for (auto& rd : inst.read) { + result.at(rd.id).mark_live(instr_id); + } + for (auto& wr : inst.write) { + result.at(wr.id).mark_live(instr_id); + } + } + + // and liveliness analysis + assert(block.live.size() == block.instr_idx.size()); + for (uint32_t instr = 0; instr < block.live.size(); instr++) { + for (int var = 0; var < block.live[instr].size(); var++) { + if (block.live[instr][var]) { + result.at(var).mark_live(block.instr_idx.at(instr)); + auto& i = input.instructions.at(block.instr_idx.at(instr)); + if (!i.clobber.empty()) { + result.at(var).mark_crossing_function(); + } + } + } + } + } + + // make us alive at any constrained instruction. todo, if this happens is this a sign of an issue + for (auto& con : input.constraints) { + if (!con.contrain_everywhere) { + result.at(con.ireg.id).mark_live(con.instr_idx); + } + } + + return result; +} + +/*! + * Populates the control flow analysis cache and: + * - iregs + * - used_var + * - initializes was allocated. + */ +void do_liveliness_analysis(const AllocationInput& input, RACache* cache) { + find_basic_blocks(&cache->control_flow, input); + cache->stats.var_count = input.max_vars; + cache->was_allocated.resize(input.max_vars, false); + cache->iregs.resize(input.max_vars); + cache->used_var.resize(input.max_vars); + cache->stack_ops.resize(input.instructions.size()); + + for (auto& instr : input.instructions) { + for (auto& wr : instr.write) { + cache->iregs.at(wr.id) = wr; + cache->used_var.at(wr.id) = true; + } + + for (auto& rd : instr.read) { + cache->iregs.at(rd.id) = rd; + cache->used_var.at(rd.id) = true; + } + } + + // phase 1 + 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(input.instructions); + } + + // phase 2 + bool changed = false; + do { + changed = false; + for (auto& block : cache->control_flow.basic_blocks) { + if (block.analyze_liveliness_phase2(cache->control_flow.basic_blocks, input.instructions)) { + changed = true; + } + } + } while (changed); + + // phase 3 + for (auto& block : cache->control_flow.basic_blocks) { + block.analyze_liveliness_phase3(cache->control_flow.basic_blocks, input.instructions); + } + + // phase 4 + auto live_ranges = find_live_range_instr(input, cache->control_flow); + cache->vars = initialize_unassigned(live_ranges, input, cache->control_flow); + + // cache a list of live ranges which are live at each instruction. + // filters out unseen lr's as well. + // this makes instr * lr1 * lr2 loop much faster! + cache->live_per_instruction.resize(input.instructions.size()); + for (u32 var_idx = 0; var_idx < cache->vars.size(); var_idx++) { + auto& lr = cache->vars.at(var_idx); + if (cache->used_var.at(var_idx)) { + for (int i = lr.first_live(); i <= lr.last_live(); i++) { + if (lr.live(i)) { + cache->live_per_instruction.at(i).push_back(var_idx); + } + } + } + } + if (input.debug_settings.print_analysis) { + // print_analysis(input, &cache); TODO + } + + cache->liveout_per_instr.resize(input.instructions.size()); + for (const auto& block : cache->control_flow.basic_blocks) { + for (int idx_in_block = 0; idx_in_block < (int)block.instr_idx.size(); idx_in_block++) { + int intsr_idx = block.instr_idx.at(idx_in_block); + cache->liveout_per_instr.at(intsr_idx) = block.live.at(idx_in_block); + } + } +} + +/*! + * Assign variables with registers constraints to registers. + */ +void do_constrained_alloc(RACache* cache, const AllocationInput& input, bool trace_debug) { + for (auto& constr : input.constraints) { + auto var_id = constr.ireg.id; + if (trace_debug) { + fmt::print("[RA] Apply constraint {}\n", constr.to_string()); + } + cache->vars.at(var_id).constrain_to_register(constr.desired_register); + } +} + +/*! + * Helper for safe_overlap. Assumes a is the first one. + */ +bool safe_overlap_reads_first(const AllocationInput& in, + RACache& cache, + const VarAssignment& a, + const VarAssignment& b, + int instr_idx) { + // it is safe to do something like + // add var_a, var_b and put var_a and var_b in the same. + auto& instr = in.instructions.at(instr_idx); + // should read a, then a goes dead. + if (!cache.liveout_per_instr.at(instr_idx)[a.var()] && instr.reads(a.var()) && + !instr.writes(a.var()) && instr.writes(b.var()) && !instr.reads(b.var())) { + return true; + } + return false; +} + +/*! + * Is it safe for these two to overlap? + * In the case where you have add var_a, var_b, you can have var_b + */ +bool safe_overlap(const AllocationInput& in, + RACache& cache, + const VarAssignment& a, + const VarAssignment& b, + int instr_idx) { + return safe_overlap_reads_first(in, cache, a, b, instr_idx) || + safe_overlap_reads_first(in, cache, b, a, instr_idx); +} + +/*! + * After assigning constrained registers, check to see if all constraints can be satisfied. + */ +bool check_constrained_alloc(RACache* cache, const AllocationInput& in) { + bool ok = true; + + // first, check that each constraint is actually satisfied. + // if not, it means that there are two constraints on the same thing. + for (auto& constr : in.constraints) { + auto& lr = cache->vars.at(constr.ireg.id); + for (int i = lr.first_live(); i <= lr.last_live(); i++) { + if (lr.assigned()) { + if (!lr.assigned_to_reg(constr.desired_register)) { + fmt::print("[RegAlloc Error] There are conflicting constraints on {}: {} and {}\n", + constr.ireg.to_string(), constr.desired_register.print(), "???"); + ok = false; + } + } + } + } + + // second, check that no constraints are overlapping. + // this can occur in two cases: + // - OpenGOAL IR generation is messed up. + // - programmer actually asked for this. + // In the second case, rlet will put both rletted variables into the same ireg, so we won't + // see it from the register allocation. + for (uint32_t i = 0; i < in.instructions.size(); i++) { + for (auto idx1 : cache->live_per_instruction.at(i)) { + auto& lr1 = cache->vars.at(idx1); + for (auto idx2 : cache->live_per_instruction.at(i)) { + if (idx1 == idx2) { + continue; + } + auto& lr2 = cache->vars.at(idx2); + if (lr1.assigned_to_reg() && lr2.assigned_to_reg()) { + if (lr1.reg() == lr2.reg() && !safe_overlap(in, *cache, lr1, lr2, i)) { + // todo, this error won't be helpful + fmt::print( + "[RegAlloc Error] {} Cannot satisfy constraints at instruction {} due to " + "constraints " + "on {} and {}, both are assigned to register {}\n", + in.function_name, i, lr1.var(), lr2.var(), lr1.reg().print()); + ok = false; + } + } + } + } + } + return ok; +} + +std::vector var_indices_of_function_crossers_large_to_small(const AllocationInput& input, + RACache& cache) { + std::vector result; + + for (int var_idx = 0; var_idx < input.max_vars; var_idx++) { + auto& info = cache.vars.at(var_idx); + if (info.seen() && info.crosses_function()) { + result.push_back(var_idx); + } + } + + std::sort(result.begin(), result.end(), [&](int a, int b) { + return cache.vars.at(a).range_size() > cache.vars.at(b).range_size(); + }); + + return result; +} + +template +bool vector_contains(const std::vector& vec, const T& obj) { + for (const auto& x : vec) { + if (x == obj) { + return true; + } + } + return false; +} + +/*! + * Is it okay to assign the given variable to the register? + */ +bool check_register_assign_at(const AllocationInput& input, + RACache& cache, + int var_idx, + int instr_idx, + emitter::Register reg) { + // Step 1: check other assignments + + // look at everybody else in the interference graph + for (int other_idx : cache.live_per_instruction.at(instr_idx)) { + // don't check ourselves + if (other_idx == var_idx) { + continue; + } + + // we are both live here. + const auto& other_var = cache.vars.at(other_idx); + if (other_var.unassigned()) { + // okay! + } else if (other_var.assigned_to_reg()) { + if (other_var.assigned_to_reg(reg)) { + // assigned to the same register as us! + if (!safe_overlap(input, cache, cache.vars.at(var_idx), other_var, instr_idx)) { + return false; + } + } + } else { + // assigned to stack TODO + if (other_var.stack_bonus_op_needs_reg(reg, instr_idx)) { + return false; + } + } + } + + // Step 2: check clobbers and excludes. + // The model for clobber is that each instruction reads, clobbers, then writes. + // so in some cases it's okay to clobber. + + // loop over our live range + + const auto& instr = input.instructions.at(instr_idx); + + if (vector_contains(instr.clobber, reg)) { + // there's two cases where this is okay. + // 1: if we aren't live-out. The clobber won't clobber anything. + if (!cache.liveout_per_instr.at(instr_idx)[var_idx]) { + // ok + } else { + // otherwise, we need to write it. + // if (!instr.writes(var_idx)) { + return false; + // } + // 2: we write it after the clobber. + } + } + + if (vector_contains(instr.exclude, reg)) { + return false; + } + + return true; +} + +/*! + * Is it okay to assign the given variable to the register? + */ +bool check_register_assign(const AllocationInput& input, + RACache& cache, + int var_idx, + emitter::Register reg) { + auto& this_var = cache.vars.at(var_idx); + + // Step 1: check other assignments + + // loop over our live range + for (int instr = this_var.first_live(); instr <= this_var.last_live(); instr++) { + // and leave out the ones where we're dead + if (!this_var.live(instr)) { + continue; + } + + // look at everybody else in the interference graph + for (int other_idx : cache.live_per_instruction.at(instr)) { + // don't check ourselves + if (other_idx == var_idx) { + continue; + } + + // we are both live here. + const auto& other_var = cache.vars.at(other_idx); + if (other_var.unassigned()) { + // skip unassigned. + continue; + } else if (other_var.assigned_to_reg()) { + if (other_var.assigned_to_reg(reg)) { + // assigned to the same register as us! + if (!safe_overlap(input, cache, this_var, other_var, instr)) { + ; + return false; + } + } + } else { + // assigned to stack TODO + if (other_var.stack_bonus_op_needs_reg(reg, instr)) { + return false; + } + } + } + } + + // Step 2: check clobbers and excludes. + // The model for clobber is that each instruction reads, clobbers, then writes. + // so in some cases it's okay to clobber. + + // loop over our live range + for (int instr_idx = this_var.first_live(); instr_idx < this_var.last_live(); instr_idx++) { + // and leave out the ones where we're dead + if (!this_var.live(instr_idx)) { + continue; + } + + const auto& instr = input.instructions.at(instr_idx); + + if (vector_contains(instr.clobber, reg)) { + // there's two cases where this is okay. + // 1: if we aren't live-out. The clobber won't clobber anything. + if (!cache.liveout_per_instr.at(instr_idx)[var_idx]) { + continue; + } else { + // otherwise, we need to write it. + if (!instr.writes(var_idx)) { + return false; + } + // 2: we write it after the clobber. + } + } + + if (vector_contains(instr.exclude, reg)) { + return false; + } + } + + return true; +} + +int get_stack_slot_for_var(int var, RACache* cache) { + int slot_size; + auto& info = cache->iregs.at(var); + switch (info.reg_class) { + case RegClass::INT_128: + slot_size = 2; + break; + case RegClass::VECTOR_FLOAT: + slot_size = 2; + break; + case RegClass::FLOAT: + slot_size = 1; // todo - this wastes some space + break; + case RegClass::GPR_64: + slot_size = 1; + break; + default: + assert(false); + } + auto kv = cache->var_to_stack_slot.find(var); + if (kv == cache->var_to_stack_slot.end()) { + if (slot_size == 2 && (cache->current_stack_slot & 1)) { + cache->current_stack_slot++; + } + auto slot = cache->current_stack_slot; + cache->current_stack_slot += slot_size; + cache->var_to_stack_slot[var] = slot; + return slot; + } else { + return kv->second; + } +} + +struct AssignmentSettings { + bool trace_debug = false; + bool prefer_saved = false; + bool only_move_eliminate_assigns = false; +}; + +bool setup_stack_bonus_ops(const AllocationInput& input, + RACache* cache, + int var_idx, + const AssignmentSettings& settings, + int my_slot); + +bool try_demote_stack(const AllocationInput& input, + RACache* cache, + int var_idx, + const AssignmentSettings& settings) { + auto& var = cache->vars.at(var_idx); + if (!var.assigned_to_reg() || var.locked()) { + return false; // not in a reg. + } + + int my_slot = get_stack_slot_for_var(var_idx, cache); + var.demote_to_stack(my_slot); + setup_stack_bonus_ops(input, cache, var_idx, settings, my_slot); + return true; +} + +/*! + * Stack "bonus" ops load and store arguments from the stack as needed. + * This may require temporary registers, which are found here. + */ +bool setup_stack_bonus_ops(const AllocationInput& input, + RACache* cache, + int var_idx, + const AssignmentSettings& settings, + int my_slot) { + auto& var = cache->vars.at(var_idx); + // loop over all possible instruction that might use this var + // bool successful = false; + // while (!successful) { +loop_top: + for (int instr_idx = var.first_live(); instr_idx <= var.last_live(); instr_idx++) { + // check out the instruction + auto& op = input.instructions.at(instr_idx); + bool is_read = op.reads(var_idx); + bool is_written = op.writes(var_idx); + // fmt::print("op {} {} {}\n", instr_idx, is_read, is_written); + if (!is_read && !is_written) { + continue; + } + // start setting up a bonus op. + StackOp::Op bonus; + bonus.reg_class = cache->iregs.at(var_idx).reg_class; + const auto& order = get_alloc_order(var_idx, input, *cache, false); + bool success = false; + + const auto& instr = input.instructions.at(instr_idx); + if (instr.is_move) { + int check_other_reg = is_written ? instr.read.front().id : instr.write.front().id; + auto& check_other_var = cache->vars.at(check_other_reg); + if (check_other_var.assigned_to_reg()) { + auto reg = check_other_var.reg(); + if (vector_contains(allowable_local_var_move_elim, reg)) { + if (check_register_assign_at(input, *cache, var_idx, instr_idx, reg)) { + var.set_stack_slot_reg(reg, instr_idx); + bonus.reg = reg; + bonus.slot = my_slot; + success = true; + goto success_check; + } + } + } + } + + for (auto reg : order) { + if (check_register_assign_at(input, *cache, var_idx, instr_idx, reg)) { + var.set_stack_slot_reg(reg, instr_idx); + bonus.reg = reg; + bonus.slot = my_slot; + success = true; + break; + } + } + + success_check: + if (!success) { + for (auto other_var_idx : cache->live_per_instruction.at(instr_idx)) { + if (other_var_idx == var_idx) { + continue; + } + + if (try_demote_stack(input, cache, other_var_idx, settings)) { + var.clear_stack_slot_regs(); + goto loop_top; + } + } + + fmt::print( + "In function {}, register allocator fell back to a highly inefficient strategy to create " + "a spill temporary register.\n", + input.function_name); + + for (int other_var_idx = 0; other_var_idx < input.max_vars; other_var_idx++) { + if (other_var_idx == var_idx) { + continue; + } + + auto& other_var = cache->vars.at(other_var_idx); + + if (other_var.seen() && (other_var.first_live() <= var.last_live()) && + (var.first_live() <= other_var.last_live())) { + if (try_demote_stack(input, cache, other_var_idx, settings)) { + var.clear_stack_slot_regs(); + goto loop_top; + } + } + } + + return false; + } + + bonus.load = is_read; + bonus.store = is_written; + + if (bonus.load || bonus.store) { + cache->stack_ops.at(instr_idx).ops.push_back(bonus); + if (bonus.load) { + cache->stats.num_spill_ops++; + } + if (bonus.store) { + cache->stats.num_spill_ops++; + } + } + } + + // } + + cache->stats.num_spilled_vars++; + + return true; +} + +/*! + * If we fail to put the variable in a register, this function will spill it to the + stack. + */ +bool handle_failed_register_allocation(const AllocationInput& input, + RACache* cache, + int var_idx, + const AssignmentSettings& settings) { + // so we couldn't find a register and we need to put this on the stack. Set on stack: + auto& var = cache->vars.at(var_idx); + int my_slot = get_stack_slot_for_var(var_idx, cache); + var.assign_to_stack(my_slot); + return setup_stack_bonus_ops(input, cache, var_idx, settings, my_slot); +} + +/*! + * Perform allocation for the given variable! + */ +bool run_assignment_on_var(const AllocationInput& input, + RACache* cache, + int var_idx, + const AssignmentSettings& settings) { + bool trace = settings.trace_debug; + auto& var = cache->vars.at(var_idx); + bool can_be_in_register = + input.force_on_stack_regs.find(var_idx) == input.force_on_stack_regs.end(); + if (var.unassigned() && var.seen()) { + bool assigned_to_reg = false; + + // first try move eliminators + auto& first_instr = input.instructions.at(var.first_live()); + auto& last_instr = input.instructions.at(var.last_live()); + + if (first_instr.is_move && can_be_in_register) { + int other_live_var_idx = first_instr.read.front().id; + + const auto& other_var = cache->vars.at(other_live_var_idx); + if (other_var.assigned_to_reg() && + safe_overlap(input, *cache, var, other_var, var.first_live())) { + if (vector_contains(allowable_local_var_move_elim, other_var.reg())) { + bool worked = check_register_assign(input, *cache, var_idx, other_var.reg()); + if (trace) { + fmt::print("m0 trying var {} in {}: {}\n", cache->iregs.at(var_idx).to_string(), + other_var.reg().print(), worked); + } + + if (worked) { + var.assign_to_register(other_var.reg()); + assigned_to_reg = true; + } + } + } + } + + if (!assigned_to_reg && last_instr.is_move && can_be_in_register) { + int other_live_var_idx = last_instr.write.front().id; + + const auto& other_var = cache->vars.at(other_live_var_idx); + if (trace && var_idx == 5) { + fmt::print(" consider {} {} {} {} [{} {}]\n", other_live_var_idx, + other_var.assigned_to_reg(), var.last_live() == other_var.first_live(), + safe_overlap(input, *cache, var, other_var, var.last_live()), var.last_live(), + other_var.first_live()); + } + + if (other_var.assigned_to_reg() && + safe_overlap(input, *cache, var, other_var, var.last_live())) { + if (vector_contains(allowable_local_var_move_elim, other_var.reg())) { + bool worked = check_register_assign(input, *cache, var_idx, other_var.reg()); + if (trace) { + fmt::print("m1 trying var {} in {}: {}\n", cache->iregs.at(var_idx).to_string(), + other_var.reg().print(), worked); + } + + if (worked) { + var.assign_to_register(other_var.reg()); + assigned_to_reg = true; + } + } + } + } + + if (!assigned_to_reg && !settings.only_move_eliminate_assigns && can_be_in_register) { + const auto& assign_order = get_alloc_order(var_idx, input, *cache, settings.prefer_saved); + for (auto& reg : assign_order) { + bool worked = check_register_assign(input, *cache, var_idx, reg); + if (trace) { + fmt::print("m2 trying var {} in {}: {}\n", cache->iregs.at(var_idx).to_string(), + reg.print(), worked); + } + if (worked) { + var.assign_to_register(reg); + assigned_to_reg = true; + break; + } + } + } + + if (!assigned_to_reg && !settings.only_move_eliminate_assigns) { + assigned_to_reg = handle_failed_register_allocation(input, cache, var_idx, settings); + if (!assigned_to_reg) { + cache->failed_alloc = true; + } + } + return assigned_to_reg; + } else { + return false; + } +} + +int run_assignment_on_some_vars(const AllocationInput& input, + RACache* cache, + const std::vector& vars_to_alloc, + const AssignmentSettings& settings) { + cache->stats.assign_passes++; + int assigned_count = 0; + + for (auto var_idx : vars_to_alloc) { + if (run_assignment_on_var(input, cache, var_idx, settings)) { + assigned_count++; + } + } + return assigned_count; +} + +int run_assignment_on_all_vars(const AllocationInput& input, + RACache* cache, + const AssignmentSettings& settings) { + cache->stats.assign_passes++; + int assigned_count = 0; + + for (int var_idx = 0; var_idx < input.max_vars; var_idx++) { + if (run_assignment_on_var(input, cache, var_idx, settings)) { + assigned_count++; + } + } + return assigned_count; +} +} // namespace + +AllocationResult allocate_registers_v2(const AllocationInput& input) { + AllocationResult result; + + // stores internal allocator state + RACache cache; + + // debug print + if (input.debug_settings.print_input) { + print_allocate_input(input); + } + + // STEP 1: Analysis: + do_liveliness_analysis(input, &cache); + + // STEP 2: Constrained allocation. + do_constrained_alloc(&cache, input, input.debug_settings.trace_debug_constraints); + check_constrained_alloc(&cache, input); + if (!check_constrained_alloc(&cache, input)) { + result.ok = false; + fmt::print("[RegAlloc Error] Register allocation has failed due to bad constraints.\n"); + return result; + } + + if (torture_test_spills) { + AssignmentSettings pick_up_new_settings; + run_assignment_on_all_vars(input, &cache, pick_up_new_settings); + } else { + // STEP 3: Function Crossing Allocation. + AssignmentSettings function_cross_settings; + function_cross_settings.only_move_eliminate_assigns = false; + function_cross_settings.prefer_saved = true; + auto func_cross_vars = var_indices_of_function_crossers_large_to_small(input, cache); + run_assignment_on_some_vars(input, &cache, func_cross_vars, function_cross_settings); + + AssignmentSettings branch_out_settings; + branch_out_settings.only_move_eliminate_assigns = true; + + AssignmentSettings pick_up_new_settings; + + int loop_count = 1; + while (loop_count) { + loop_count = run_assignment_on_all_vars(input, &cache, branch_out_settings); + } + + run_assignment_on_all_vars(input, &cache, pick_up_new_settings); + } + + result.ok = true; + + if (cache.failed_alloc) { + result.ok = false; + return result; + } + for (int var_idx = 0; var_idx < input.max_vars; var_idx++) { + auto& var = cache.vars.at(var_idx); + if (var.seen() && !var.assigned()) { + // fmt::print("av2: {} failed\n", input.function_name); + result.ok = false; + return result; + } + } + + 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.vars) { + for (int instr_idx = lr.first_live(); instr_idx <= lr.last_live(); instr_idx++) { + if (lr.assigned_to_reg()) { + if (lr.assigned_to_reg(sr)) { + uses_sr = true; + break; + } + } else if (lr.assigned_to_stack()) { + if (lr.stack_bonus_op_needs_reg(sr, instr_idx)) { + 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.vars) { + if (!lr.seen()) { + result.ass_as_ranges.push_back(AssignmentRange(0, {}, {})); + } else { + result.ass_as_ranges.push_back( + AssignmentRange(lr.first_live(), lr.live_vector(), lr.make_assignment_vector())); + } + } + result.stack_ops = std::move(cache.stack_ops); + + // final result print + if (input.debug_settings.print_result) { + print_result(input, result); + } + + result.num_spilled_vars = cache.stats.num_spilled_vars; + result.num_spills = cache.stats.num_spill_ops; + + return result; +} \ No newline at end of file diff --git a/goalc/regalloc/Allocator_v2.h b/goalc/regalloc/Allocator_v2.h new file mode 100644 index 0000000000..896a56e053 --- /dev/null +++ b/goalc/regalloc/Allocator_v2.h @@ -0,0 +1,6 @@ +#pragma once + +#include "goalc/regalloc/allocator_interface.h" + +// Allocator v2's interface +AllocationResult allocate_registers_v2(const AllocationInput& input); diff --git a/goalc/regalloc/allocate.cpp b/goalc/regalloc/allocate.cpp deleted file mode 100644 index 5e6bb24aa8..0000000000 --- a/goalc/regalloc/allocate.cpp +++ /dev/null @@ -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 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 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; -} \ No newline at end of file diff --git a/goalc/regalloc/allocate.h b/goalc/regalloc/allocate.h deleted file mode 100644 index 2376e50282..0000000000 --- a/goalc/regalloc/allocate.h +++ /dev/null @@ -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 -#include -#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 clobber; // written, but safe to use as input/output - std::vector exclude; // written, unsafe to use for input/output - std::vector write; // results go in here - std::vector read; // inputs go in here - std::vector 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> assignment; // variable, instruction - std::vector ass_as_ranges; // another format, maybe easier? - std::vector 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 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 instructions; // all instructions in the function - std::vector constraints; // all register constraints - std::unordered_set force_on_stack_regs; // registers which must be on the stack - int max_vars = -1; // maximum register id. - std::vector 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); diff --git a/goalc/regalloc/allocate_common.cpp b/goalc/regalloc/allocate_common.cpp deleted file mode 100644 index abc39bc46a..0000000000 --- a/goalc/regalloc/allocate_common.cpp +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/goalc/regalloc/allocate_common.h b/goalc/regalloc/allocate_common.h deleted file mode 100644 index 3219d201b6..0000000000 --- a/goalc/regalloc/allocate_common.h +++ /dev/null @@ -1,237 +0,0 @@ -#ifndef JAK_ALLOCATE_COMMON_H -#define JAK_ALLOCATE_COMMON_H -#include -#include -#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 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 is_alive; - std::vector 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; - - // 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 diff --git a/goalc/regalloc/allocator_interface.cpp b/goalc/regalloc/allocator_interface.cpp new file mode 100644 index 0000000000..bb96f885f7 --- /dev/null +++ b/goalc/regalloc/allocator_interface.cpp @@ -0,0 +1,379 @@ +/*! + * @file allocate.cpp + * Runs the register allocator. + */ + +#include + +#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 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 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& 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& blocks, + const std::vector& 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& blocks, + const std::vector& 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& 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; +} diff --git a/goalc/regalloc/allocator_interface.h b/goalc/regalloc/allocator_interface.h new file mode 100644 index 0000000000..b011096d5f --- /dev/null +++ b/goalc/regalloc/allocator_interface.h @@ -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 +#include +#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 clobber; // written, but safe to use as input/output + std::vector exclude; // written, unsafe to use for input/output + std::vector write; // results go in here + std::vector read; // inputs go in here + std::vector 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 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& live, + const std::vector& 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 m_live; + std::vector m_ass; +}; + +/*! + * Result of the allocate_registers algorithm + */ +struct AllocationResult { + bool ok = false; // did it work? + // std::vector> assignment; // variable, instruction + std::vector ass_as_ranges; // another format, maybe easier? + std::vector 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 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 instructions; // all instructions in the function + std::vector constraints; // all register constraints + std::unordered_set force_on_stack_regs; // registers which must be on the stack + int max_vars = -1; // maximum register id. + std::vector 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 instr_idx; + std::vector succ; + std::vector pred; + std::vector 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& instructions); + bool analyze_liveliness_phase2(std::vector& blocks, + const std::vector& instructions); + void analyze_liveliness_phase3(std::vector& blocks, + const std::vector& instructions); + std::string print(const std::vector& insts); + std::string print_summary(); +}; + +struct ControlFlowAnalysisCache { + std::vector 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); \ No newline at end of file diff --git a/test/goalc/source_templates/with_game/test-method-replace.gc b/test/goalc/source_templates/with_game/test-method-replace.gc index fe707fe57c..d4062b698d 100644 --- a/test/goalc/source_templates/with_game/test-method-replace.gc +++ b/test/goalc/source_templates/with_game/test-method-replace.gc @@ -12,4 +12,5 @@ (let ((obj (new 'static 'type-with-weird-relocate :foo 123))) (relocate obj (the kheap 1) (the (pointer uint8) 2)) + 0 )