[backend] Add register allocation
Bug: 295189858
Test: mm and berberis_host_tests
Change-Id: Iae5d53c45fad76fc0dbd3e93e50fd1de4c2e717d
Merged-In: Iae5d53c45fad76fc0dbd3e93e50fd1de4c2e717d
diff --git a/backend/Android.bp b/backend/Android.bp
index 078a2a6..e39f2c8 100644
--- a/backend/Android.bp
+++ b/backend/Android.bp
@@ -174,6 +174,7 @@
srcs: [
"common/lifetime_analysis.cc",
"common/machine_ir_debug.cc",
+ "common/reg_alloc.cc",
"x86_64/code.cc",
"x86_64/code_debug.cc",
"x86_64/code_emit.cc",
diff --git a/backend/common/reg_alloc.cc b/backend/common/reg_alloc.cc
new file mode 100644
index 0000000..3da5d06
--- /dev/null
+++ b/backend/common/reg_alloc.cc
@@ -0,0 +1,400 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// At the moment, this implements more-or-less traditional linear scan register
+// allocation.
+//
+// Input is virtual register lifetimes list, sorted by begin. Each lifetime is
+// a list of continuous live ranges with lifetime holes in between. Each live
+// range tracks insns that actually use the virtual register.
+//
+// Allocator walks sorted lifetime list and allocates lifetimes to hard
+// registers. When lifetimes do not interfere, so live ranges of one lifetime
+// fit into holes of another lifetime, both lifetimes can be allocated to the
+// same hard register.
+//
+// If there is no available hard register, allocator selects hard register to
+// free. All lifetimes allocated to that hard register that interfere with
+// lifetime being allocated are spilled.
+//
+// Lifetime being spilled is split into tiny lifetimes, each for one insn
+// where that virtual register is used. If register is read by insn, reload
+// insn is added before, and if register is written by insn, spill insn is
+// added after.
+//
+// Tiny lifetimes originated from spilling still need to be allocated. For tiny
+// lifetimes that end before lifetime in favor of which they were spilled,
+// previously allocated hard register is used. Tiny lifetimes that begin after
+// are merged into a list of not yet allocated lifetimes.
+//
+// The problematic case is when tiny lifetime overlaps with begin of lifetime
+// in favor of which it was spilled. In this case previously allocated hard
+// register can't be used (otherwise it doesn't become free) and tiny lifetime
+// can't be allocated later according to order by begin. In this case spill is
+// considered impossible.
+//
+// The above is the most significant difference from classic linear scan
+// register allocation algorithms, which usually solve tiny lifetimes
+// allocation either by backtracking or using reserved registers. Hopefully
+// this new approach works if there are more suitable hard registers than can
+// be used in one insn, so there is always a suitable register that is not used
+// at the point of spill.
+//
+// TODO(b/232598137): this might blow up when lifetimes compete for some single
+// specific register (ecx for x86 shift insn)! Can be solved by generating code
+// that minimizes lifetimes of such registers - moves in right before and moves
+// out right after the insn using such register.
+//
+// TODO(b/232598137): investigate how code quality is affected when there are few
+// available hard registers (x86_32)!
+
+#include "berberis/backend/common/reg_alloc.h"
+
+#include <iterator> // std::next()
+#include <string>
+
+#include "berberis/backend/common/lifetime.h"
+#include "berberis/backend/common/lifetime_analysis.h"
+#include "berberis/backend/common/machine_ir.h"
+#include "berberis/base/arena_alloc.h"
+#include "berberis/base/arena_list.h"
+#include "berberis/base/arena_vector.h"
+#include "berberis/base/logging.h"
+#include "berberis/runtime_primitives/config.h"
+
+// #define LOG_REG_ALLOC(...) ALOGE(__VA_ARGS__)
+#define LOG_REG_ALLOC(...) ((void)0)
+
+namespace berberis {
+
+namespace {
+
+// Lifetimes themselves are owned by lifetime list populated by lifetime
+// analysis. Use list of pointers to track lifetimes currently allocated
+// to particular hard register.
+using VRegLifetimePtrList = ArenaList<VRegLifetime*>;
+
+// How to spill one virtual register.
+struct VRegLifetimeSpill {
+ VRegLifetimePtrList::iterator lifetime;
+ SplitPos realloc_pos;
+
+ VRegLifetimeSpill(VRegLifetimePtrList::iterator i, const SplitPos& p)
+ : lifetime(i), realloc_pos(p) {}
+};
+
+// Every possible spill should have some smaller weight (CHECKed).
+const int kInfiniteSpillWeight = 99999;
+
+// Track what virtual registers are currently allocated to this particular
+// hard register, and how to spill them.
+class HardRegAllocation {
+ public:
+ explicit HardRegAllocation(Arena* arena)
+ : arena_(arena), lifetimes_(arena), new_lifetime_(nullptr), spills_(arena) {}
+
+ HardRegAllocation(const HardRegAllocation& other) = default;
+
+ // If new_lifetime doesn't interfere with lifetimes currently allocated
+ // to this hard register, allocate new_lifetime to this register as well.
+ bool TryAssign(VRegLifetime* new_lifetime);
+
+ // If TryAssign returned false:
+ // Check if it is possible to spill all lifetimes allocated to this hard
+ // register that interfere with new_lifetime, and return spill weight.
+ int ConsiderSpill(VRegLifetime* new_lifetime);
+
+ // If ConsiderSpill returned non-infinite weight:
+ // Given spill is possible, actually spill lifetimes that interfere with
+ // new_lifetime to spill_slot. Insert newly created tiny lifetimes into
+ // 'lifetimes' list, starting at position 'pos'.
+ void SpillAndAssign(VRegLifetime* new_lifetime,
+ int spill_slot,
+ VRegLifetimeList* lifetimes,
+ VRegLifetimeList::iterator pos);
+
+ private:
+ // Arena for allocations.
+ Arena* arena_;
+ // Lifetimes currently allocated to this hard register.
+ VRegLifetimePtrList lifetimes_;
+
+ // Last lifetime being allocated, for CHECKing.
+ // TODO(b/232598137): probably use this for ConsiderSpill and SpillAndAssign?
+ // This looks more natural...
+ VRegLifetime* new_lifetime_;
+
+ // How to free this register for last considered new lifetime.
+ // This is here for the following reasons:
+ // - it is highly coupled with lifetimes_
+ // - to avoid reallocating this for every spill consideration
+ ArenaVector<VRegLifetimeSpill> spills_;
+};
+
+bool HardRegAllocation::TryAssign(VRegLifetime* new_lifetime) {
+ // TODO(b/232598137): had to disable the check below! The problem is that when
+ // new_lifetime_ is split so that there remains no live ranges, we can't
+ // call begin() for it. Seems this place requires some rethinking, as such
+ // case means we can simply reorder lifetimes instead of actually splitting...
+ // Check lifetimes are processed in order by increasing begin.
+ // CHECK(!new_lifetime_ || new_lifetime_->begin() <= new_lifetime->begin());
+ new_lifetime_ = new_lifetime;
+
+ for (auto curr = lifetimes_.begin(); curr != lifetimes_.end();) {
+ VRegLifetime* curr_lifetime = *curr;
+
+ if (curr_lifetime->end() <= new_lifetime->begin()) {
+ // Curr lifetime ends before new lifetime starts, expire it.
+ curr = lifetimes_.erase(curr);
+ } else if (curr_lifetime->TestInterference(*new_lifetime)) {
+ // Lifetimes interfere, can't assign.
+ return false;
+ } else {
+ ++curr;
+ }
+ }
+
+ // No lifetimes interfere with new, can assign.
+ lifetimes_.push_back(new_lifetime);
+ return true;
+}
+
+int HardRegAllocation::ConsiderSpill(VRegLifetime* new_lifetime) {
+ CHECK_EQ(new_lifetime_, new_lifetime);
+
+ spills_.clear();
+ int weight = 0;
+
+ for (auto curr = lifetimes_.begin(); curr != lifetimes_.end(); ++curr) {
+ VRegLifetime* curr_lifetime = *curr;
+
+ if (!curr_lifetime->TestInterference(*new_lifetime)) {
+ // No interference, no need to spill.
+ continue;
+ }
+
+ SplitPos split_pos;
+ SplitKind split_kind = curr_lifetime->FindSplitPos(new_lifetime->begin(), &split_pos);
+ if (split_kind == SPLIT_IMPOSSIBLE) {
+ // Lifetimes interfere in such a way that spill is not possible.
+ return kInfiniteSpillWeight;
+ } else if (split_kind == SPLIT_CONFLICT) {
+ // A use within this lifetime conflicts with first use in 'new_lifetime'.
+ // If we spill it, it will compete with 'new_lifetime' at reallocation,
+ // and if it can only use register suitable for 'new_lifetime' as well,
+ // 'new_lifetime' can be evicted back, resulting in double spill.
+ if (curr_lifetime->GetRegClass()->IsSubsetOf(new_lifetime->GetRegClass())) {
+ return kInfiniteSpillWeight;
+ }
+ }
+
+ // Record spill.
+ spills_.push_back(VRegLifetimeSpill(curr, split_pos));
+ // Evicting tiny lifetime is free.
+ if (curr_lifetime->GetSpill() == -1) {
+ weight += curr_lifetime->spill_weight();
+ }
+ }
+
+ CHECK_LT(weight, kInfiniteSpillWeight);
+ return weight;
+}
+
+// Same as std::list::merge, but starting from a 'pos' position.
+void MergeVRegLifetimeList(VRegLifetimeList* dst,
+ VRegLifetimeList::iterator dst_pos,
+ VRegLifetimeList* src) {
+ while (!src->empty()) {
+ auto curr = src->begin();
+ for (; dst_pos != dst->end(); ++dst_pos) {
+ if (curr->begin() < dst_pos->begin()) {
+ break;
+ }
+ }
+ dst->splice(dst_pos, *src, curr);
+ }
+}
+
+void HardRegAllocation::SpillAndAssign(VRegLifetime* new_lifetime,
+ int spill_slot,
+ VRegLifetimeList* lifetimes,
+ VRegLifetimeList::iterator pos) {
+ CHECK_EQ(new_lifetime_, new_lifetime);
+ CHECK(!spills_.empty());
+
+ for (const auto& spill : spills_) {
+ VRegLifetime* spill_lifetime = *spill.lifetime;
+
+ // Assign spill slot.
+ // Lifetimes being spilled do not interfere, can share spill slot!
+ // TODO(b/232598137): evicted tiny lifetime have spill slot already.
+ // If we only evict tiny lifetimes, we might not need new spill_slot!
+ // Allocate spill slot here when needed.
+ if (spill_lifetime->GetSpill() == -1) {
+ spill_lifetime->SetSpill(spill_slot);
+ }
+
+ // Split spilled lifetime into tiny lifetimes, enqueue them for allocation.
+ VRegLifetimeList split(arena_);
+ spill_lifetime->Split(spill.realloc_pos, &split);
+ MergeVRegLifetimeList(lifetimes, pos, &split);
+
+ // Expire spilled lifetime.
+ lifetimes_.erase(spill.lifetime);
+ }
+
+ // Spilled all interfering lifetimes, can assign.
+ lifetimes_.push_back(new_lifetime);
+}
+
+// Simple register allocator.
+// Walk list of lifetimes sorted by begin and allocates in order.
+// Modifies lifetimes that have been spilled and adds tiny lifetimes split
+// from spilled lifetimes to the same list.
+class VRegLifetimeAllocator {
+ public:
+ VRegLifetimeAllocator(MachineIR* machine_ir, VRegLifetimeList* lifetimes)
+ : machine_ir_(machine_ir),
+ lifetimes_(lifetimes),
+ allocations_(config::kMaxHardRegs,
+ HardRegAllocation(machine_ir->arena()),
+ machine_ir->arena()) {}
+
+ void Allocate();
+
+ private:
+ void AllocateLifetime(VRegLifetimeList::iterator lifetime_it);
+
+ bool TryAssignHardReg(VRegLifetime* lifetime, MachineReg hard_reg);
+
+ int ConsiderSpillHardReg(MachineReg hard_reg, VRegLifetime* lifetime);
+
+ void SpillAndAssignHardReg(MachineReg hard_reg, VRegLifetimeList::iterator curr);
+
+ void RewriteAllocatedLifetimes();
+
+ MachineIR* machine_ir_;
+
+ VRegLifetimeList* lifetimes_;
+
+ ArenaVector<HardRegAllocation> allocations_;
+};
+
+int VRegLifetimeAllocator::ConsiderSpillHardReg(MachineReg hard_reg, VRegLifetime* lifetime) {
+ return allocations_[hard_reg.reg()].ConsiderSpill(lifetime);
+}
+
+bool VRegLifetimeAllocator::TryAssignHardReg(VRegLifetime* curr_lifetime, MachineReg hard_reg) {
+ if (allocations_[hard_reg.reg()].TryAssign(curr_lifetime)) {
+ curr_lifetime->set_hard_reg(hard_reg);
+ LOG_REG_ALLOC(".. to %s\n", GetMachineHardRegDebugName(hard_reg));
+ return true;
+ }
+ return false;
+}
+
+void VRegLifetimeAllocator::SpillAndAssignHardReg(MachineReg hard_reg,
+ VRegLifetimeList::iterator curr) {
+ auto next = std::next(curr);
+ allocations_[hard_reg.reg()].SpillAndAssign(&*curr, machine_ir_->AllocSpill(), lifetimes_, next);
+ curr->set_hard_reg(hard_reg);
+ LOG_REG_ALLOC(".. to %s (after spill)\n", GetMachineHardRegDebugName(hard_reg));
+}
+
+void VRegLifetimeAllocator::AllocateLifetime(VRegLifetimeList::iterator lifetime_it) {
+ VRegLifetime* lifetime = &*lifetime_it;
+ const MachineRegClass* reg_class = lifetime->GetRegClass();
+
+ LOG_REG_ALLOC(
+ "allocating lifetime %s:\n%s", reg_class->GetDebugName(), lifetime->GetDebugString().c_str());
+
+ // First try preferred register.
+ MachineReg pref_reg = lifetime->FindMoveHint()->hard_reg();
+ if (reg_class->HasReg(pref_reg) && TryAssignHardReg(lifetime, pref_reg)) {
+ return;
+ }
+
+ // Walk registers from reg class.
+ for (MachineReg hard_reg : *reg_class) {
+ if (hard_reg != pref_reg && TryAssignHardReg(lifetime, hard_reg)) {
+ return;
+ }
+ }
+
+ LOG_REG_ALLOC("... failed to find free hard reg, will try spilling");
+
+ // Walk registers again, consider each for spilling.
+ int best_spill_weight = kInfiniteSpillWeight;
+ MachineReg best_reg{0};
+ for (MachineReg hard_reg : *reg_class) {
+ int spill_weight = ConsiderSpillHardReg(hard_reg, lifetime);
+ LOG_REG_ALLOC(
+ "... consider spilling %s, weight %d", GetMachineHardRegDebugName(hard_reg), spill_weight);
+ if (best_spill_weight > spill_weight) {
+ best_spill_weight = spill_weight;
+ best_reg = hard_reg;
+ }
+ }
+
+ // Spill register with best spill weight.
+ CHECK_LT(best_spill_weight, kInfiniteSpillWeight);
+ SpillAndAssignHardReg(best_reg, lifetime_it);
+}
+
+void VRegLifetimeAllocator::RewriteAllocatedLifetimes() {
+ for (auto& lifetime : *lifetimes_) {
+ lifetime.Rewrite(machine_ir_);
+ }
+}
+
+void VRegLifetimeAllocator::Allocate() {
+ for (auto lifetime_it = lifetimes_->begin(); lifetime_it != lifetimes_->end(); ++lifetime_it) {
+ AllocateLifetime(lifetime_it);
+ }
+ RewriteAllocatedLifetimes();
+}
+
+void CollectLifetimes(const MachineIR* machine_ir, VRegLifetimeList* lifetimes) {
+ VRegLifetimeAnalysis lifetime_analysis(machine_ir->arena(), 2 * machine_ir->NumVReg(), lifetimes);
+
+ // Not 'const' because we need pointer to modifiable bb->insn_list().
+ for (auto* bb : machine_ir->bb_list()) {
+ for (const auto reg : bb->live_in()) {
+ lifetime_analysis.SetLiveIn(reg);
+ }
+
+ for (auto insn_it = bb->insn_list().begin(); insn_it != bb->insn_list().end(); ++insn_it) {
+ lifetime_analysis.AddInsn(MachineInsnListPosition(&(bb->insn_list()), insn_it));
+ }
+
+ for (const auto reg : bb->live_out()) {
+ lifetime_analysis.SetLiveOut(reg);
+ }
+ lifetime_analysis.EndBasicBlock();
+ }
+}
+
+} // namespace
+
+void AllocRegs(MachineIR* machine_ir) {
+ VRegLifetimeList lifetimes(machine_ir->arena());
+ CollectLifetimes(machine_ir, &lifetimes);
+
+ VRegLifetimeAllocator allocator(machine_ir, &lifetimes);
+ allocator.Allocate();
+}
+
+} // namespace berberis
diff --git a/backend/include/berberis/backend/common/reg_alloc.h b/backend/include/berberis/backend/common/reg_alloc.h
new file mode 100644
index 0000000..c390cf5
--- /dev/null
+++ b/backend/include/berberis/backend/common/reg_alloc.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef BERBERIS_BACKEND_COMMON_REG_ALLOC_H_
+#define BERBERIS_BACKEND_COMMON_REG_ALLOC_H_
+
+namespace berberis {
+
+class MachineIR;
+
+void AllocRegs(MachineIR* machine_ir);
+
+} // namespace berberis
+
+#endif // BERBERIS_BACKEND_COMMON_REG_ALLOC_H_
diff --git a/runtime_primitives/include/berberis/runtime_primitives/config.h b/runtime_primitives/include/berberis/runtime_primitives/config.h
index 54fa647..26011dd 100644
--- a/runtime_primitives/include/berberis/runtime_primitives/config.h
+++ b/runtime_primitives/include/berberis/runtime_primitives/config.h
@@ -44,6 +44,8 @@
inline constexpr bool kLinkJumpsBetweenRegions = !kAllJumpsExitGeneratedCode;
// Guest page size. Always 4K for now.
inline constexpr size_t kGuestPageSize = 4096;
+// Number of hard registers assumed by the register allocator.
+inline constexpr uint32_t kMaxHardRegs = 64u;
} // namespace berberis::config