blob: d04826de6a7bdabea6420818eabb604897dfd00b [file] [log] [blame]
/*
* 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/config.h"
#include "berberis/base/logging.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