blob: c5d4f208f656b87bd81f8f19e5061ee084ae1e35 [file] [edit]
// Copyright 2019 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#include "iree/compiler/Dialect/VM/Analysis/RegisterAllocation.h"
#include <algorithm>
#include <utility>
#include "iree/compiler/Dialect/Util/IR/UtilTypes.h"
#include "iree/compiler/Dialect/VM/Analysis/LinearScan/LiveIntervals.h"
#include "iree/compiler/Dialect/VM/Analysis/LinearScan/RegisterBank.h"
#include "iree/compiler/Dialect/VM/IR/VMOps.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/FormatVariadic.h"
#include "mlir/IR/Attributes.h"
#include "mlir/IR/Builders.h"
#include "mlir/IR/Dominance.h"
namespace mlir::iree_compiler {
static Attribute getStrArrayAttr(Builder &builder,
ArrayRef<std::string> values) {
return builder.getStrArrayAttr(llvm::map_to_vector<8>(
values, [](const std::string &value) { return StringRef(value); }));
}
// static
LogicalResult RegisterAllocation::annotateIR(IREE::VM::FuncOp funcOp) {
RegisterAllocation registerAllocation;
if (failed(registerAllocation.recalculate(funcOp))) {
funcOp.emitOpError() << "failed to allocate registers for function";
return failure();
}
Builder builder(funcOp.getContext());
for (auto &block : funcOp.getBlocks()) {
SmallVector<std::string, 8> blockRegStrs;
blockRegStrs.reserve(block.getNumArguments());
for (auto blockArg : block.getArguments()) {
auto reg = registerAllocation.map_[blockArg];
blockRegStrs.push_back(reg.toString());
}
block.front().setAttr("block_registers",
getStrArrayAttr(builder, blockRegStrs));
for (auto &op : block.getOperations()) {
// Emit operand registers with MOVE bits for ref operands.
if (op.getNumOperands() > 0) {
SmallVector<std::string, 8> operandRegStrs;
operandRegStrs.reserve(op.getNumOperands());
// For branch operations, use remapSuccessorRegisters to get the
// correct MOVE bits for branch arguments.
// Build a map from operand index to the correct register with MOVE bit.
llvm::DenseMap<unsigned, Register> branchOperandRegs;
if (auto branchOp = dyn_cast<BranchOpInterface>(&op)) {
for (unsigned i = 0; i < op.getNumSuccessors(); ++i) {
auto srcDstRegs =
registerAllocation.remapSuccessorRegisters(&op, i);
auto succOperands =
branchOp.getSuccessorOperands(i).getForwardedOperands();
if (succOperands.empty()) {
continue;
}
unsigned baseIdx = succOperands.getBeginOperandIndex();
// remapSuccessorRegisters only returns pairs where src != dst.
// For display, we need ALL operands with correct MOVE bits.
// Re-compute with the same logic as remapSuccessorRegisters.
(void)srcDstRegs; // Unused - we recompute for display.
// Build lastOccurrence map: only the last occurrence of a ref value
// in this successor's operand list can get MOVE (matches
// remapSuccessorRegisters logic).
llvm::DenseMap<Value, unsigned> lastOccurrence;
for (auto [idx, operand] : llvm::enumerate(succOperands)) {
if (isa<IREE::VM::RefType>(operand.getType())) {
lastOccurrence[operand] = idx;
}
}
for (auto [localIdx, operand] : llvm::enumerate(succOperands)) {
unsigned globalIdx = baseIdx + localIdx;
auto srcReg = registerAllocation.mapToRegister(operand);
// Apply same MOVE logic as remapSuccessorRegisters.
if (srcReg.isRef()) {
bool isLastOccurrence =
(lastOccurrence.lookup(operand) == localIdx);
bool isLastUse =
registerAllocation.liveness_.isLastRealValueUse(
operand, &op, globalIdx);
if (isLastOccurrence && isLastUse) {
srcReg.setMove(true);
}
}
branchOperandRegs[globalIdx] = srcReg;
}
}
}
for (auto &operand : op.getOpOperands()) {
unsigned idx = operand.getOperandNumber();
Register reg;
if (branchOperandRegs.count(idx)) {
// Use the pre-computed register with correct MOVE bit.
reg = branchOperandRegs[idx];
} else {
// Use standard mapUseToRegister for non-branch operands.
reg = registerAllocation.mapUseToRegister(operand.get(), &op, idx);
}
operandRegStrs.push_back(reg.toString());
}
op.setAttr("operand_registers",
getStrArrayAttr(builder, operandRegStrs));
}
if (op.getNumResults() == 0) {
continue;
}
SmallVector<std::string, 8> regStrs;
regStrs.reserve(op.getNumResults());
for (auto result : op.getResults()) {
auto reg = registerAllocation.map_[result];
regStrs.push_back(reg.toString());
}
op.setAttr("result_registers", getStrArrayAttr(builder, regStrs));
}
Operation *terminatorOp = block.getTerminator();
if (terminatorOp->getNumSuccessors() > 0) {
SmallVector<Attribute, 2> successorAttrs;
successorAttrs.reserve(terminatorOp->getNumSuccessors());
for (int i = 0; i < terminatorOp->getNumSuccessors(); ++i) {
auto srcDstRegs =
registerAllocation.remapSuccessorRegisters(terminatorOp, i);
SmallVector<std::string, 8> remappingStrs;
for (auto &srcDstReg : srcDstRegs) {
remappingStrs.push_back(llvm::formatv("{}->{}",
srcDstReg.first.toString(),
srcDstReg.second.toString())
.str());
}
successorAttrs.push_back(getStrArrayAttr(builder, remappingStrs));
}
terminatorOp->setAttr("remap_registers",
builder.getArrayAttr(successorAttrs));
}
}
return success();
}
// Sorts blocks in dominance order such that the entry block is first and
// all of the following blocks are dominated only by blocks that have come
// before them in the list. This ensures that we always know all registers for
// block live-in values as we walk the blocks.
static SmallVector<Block *, 8>
sortBlocksInDominanceOrder(IREE::VM::FuncOp funcOp) {
if (funcOp.getBlocks().size() == 1) {
// Dominance info cannot be computed for regions with one block.
return {&funcOp.getBlocks().front()};
}
DominanceInfo dominanceInfo(funcOp);
llvm::SmallSetVector<Block *, 8> unmarkedBlocks;
for (auto &block : funcOp.getBlocks()) {
unmarkedBlocks.insert(&block);
}
llvm::SmallSetVector<Block *, 8> markedBlocks;
std::function<void(Block *)> visit = [&](Block *block) {
if (markedBlocks.contains(block)) {
return;
}
for (auto *childBlock : dominanceInfo.getNode(block)->children()) {
visit(childBlock->getBlock());
}
markedBlocks.insert(block);
};
while (!unmarkedBlocks.empty()) {
visit(unmarkedBlocks.pop_back_val());
}
auto orderedBlocks = markedBlocks.takeVector();
std::reverse(orderedBlocks.begin(), orderedBlocks.end());
return orderedBlocks;
}
// Runs linear scan allocation for a single register bank.
// Processes intervals matching isRefBank, allocating from bank into active
// list. Updates map_ and intervalAllocations_ for each allocated interval.
LogicalResult RegisterAllocation::runLinearScan(
IREE::VM::FuncOp funcOp, const LiveIntervals &liveIntervals,
IREE::VM::RegisterBank &bank, SmallVectorImpl<const LiveInterval *> &active,
const llvm::DenseMap<Value, Value> &coalesceSource, bool isRefBank,
int &maxOrdinal) {
// Helper to expire old intervals.
// Use < (not <=) so intervals ending at currentStart are NOT expired yet.
// This prevents the bug where multiple block args with the same start
// position get assigned the same register.
auto expireOldIntervals = [&](uint32_t currentStart) {
while (!active.empty() && active.front()->end < currentStart) {
const LiveInterval *expired = active.front();
size_t byteWidth = isRefBank ? 0 : expired->byteWidth;
bank.release(intervalAllocations_[expired].assigned, byteWidth);
active.erase(active.begin());
}
};
// Helper to insert into active list (maintain sorted by end).
auto insertActive = [&](const LiveInterval *interval) {
auto it =
std::lower_bound(active.begin(), active.end(), interval,
[](const LiveInterval *a, const LiveInterval *b) {
return a->end < b->end;
});
active.insert(it, interval);
};
for (const LiveInterval *interval : liveIntervals.getSortedByStart()) {
// Filter by bank type.
if (interval->isRef != isRefBank) {
continue;
}
// Skip already-assigned values (entry block args).
if (map_.count(interval->value)) {
continue;
}
// Expire intervals that ended before this one starts.
expireOldIntervals(interval->start);
size_t byteWidth = isRefBank ? 0 : interval->byteWidth;
// Try coalescing: reuse register from a value whose interval ends exactly
// where ours begins.
int ordinal = -1;
auto coalesceIt = coalesceSource.find(interval->value);
if (coalesceIt != coalesceSource.end()) {
auto srcIt = map_.find(coalesceIt->second);
if (srcIt != map_.end()) {
int preferredOrdinal = srcIt->second.ordinal();
if (bank.allocateAt(preferredOrdinal, byteWidth)) {
ordinal = preferredOrdinal;
} else {
// The source register may still be "active" (not expired due to <
// check). Find and expire it to complete the hand-off.
for (auto it = active.begin(); it != active.end(); ++it) {
if (intervalAllocations_[*it].assigned == preferredOrdinal &&
(*it)->end == interval->start) {
size_t expiredByteWidth = isRefBank ? 0 : (*it)->byteWidth;
bank.release(intervalAllocations_[*it].assigned,
expiredByteWidth);
active.erase(it);
if (bank.allocateAt(preferredOrdinal, byteWidth)) {
ordinal = preferredOrdinal;
}
break;
}
}
}
}
}
// Fall back to first-fit allocation.
if (ordinal < 0) {
auto allocated = bank.allocate(byteWidth);
if (!allocated) {
return funcOp.emitError()
<< "register allocation failed (" << (isRefBank ? "ref" : "int")
<< " bank overflow)";
}
ordinal = *allocated;
}
// Store assignment in map.
intervalAllocations_[interval].assigned = ordinal;
if (isRefBank) {
map_[interval->value] = Register::getRef(interval->value.getType(),
ordinal, /*isMove=*/false);
} else {
map_[interval->value] =
Register::getValue(interval->value.getType(), ordinal);
}
// Add to active set.
insertActive(interval);
}
maxOrdinal = bank.getMaxUsed();
return success();
}
// Linear scan allocator implementation.
// Uses LiveIntervals for global register lifetime tracking and enables
// cross-block register reuse for both integer and reference registers.
LogicalResult RegisterAllocation::recalculate(IREE::VM::FuncOp funcOp) {
map_.clear();
intervalAllocations_.clear();
dominanceInfo_.reset();
maxI32RegisterOrdinal_ = -1;
maxRefRegisterOrdinal_ = -1;
scratchI32RegisterCount_ = 0;
scratchRefRegisterCount_ = 0;
// Build live intervals for global allocation.
LiveIntervals liveIntervals;
if (failed(liveIntervals.build(funcOp))) {
return funcOp.emitError() << "failed to build live intervals";
}
// Also compute liveness for MOVE bit computation (still needed).
if (failed(liveness_.recalculate(funcOp))) {
return funcOp.emitError()
<< "failed to calculate required liveness information";
}
// Compute dominance info for back-edge detection in MOVE bit logic.
// Only needed for functions with more than one block.
if (funcOp.getBlocks().size() > 1) {
dominanceInfo_.emplace(funcOp);
}
// Global register banks (single instance for entire function).
IREE::VM::RegisterBank intBank(Register::kInt32RegisterCount);
IREE::VM::RegisterBank refBank(Register::kRefRegisterCount);
// ===== PHASE A: Pre-allocate entry block args (monotonic, ABI) =====
// Also set the assigned field in the interval so it can be properly expired.
Block *entryBlock = &funcOp.getBlocks().front();
for (auto blockArg : entryBlock->getArguments()) {
Type type = blockArg.getType();
if (isa<IREE::VM::RefType>(type)) {
// Entry block ref args: monotonic allocation for ABI stability.
auto ordinal = refBank.allocate(0, /*fromEnd=*/true);
if (!ordinal) {
return funcOp.emitError()
<< "register allocation failed for entry block arg "
<< blockArg.getArgNumber() << " (ref bank overflow)";
}
map_[blockArg] = Register::getRef(type, *ordinal, /*isMove=*/false);
// Set assigned in interval for expiration tracking.
if (const LiveInterval *interval = liveIntervals.getInterval(blockArg)) {
intervalAllocations_[interval].assigned = *ordinal;
}
} else if (type.isIntOrFloat()) {
// Entry block int args: monotonic allocation for ABI stability.
size_t byteWidth = IREE::Util::getRoundedElementByteWidth(type);
auto ordinal = intBank.allocate(byteWidth, /*fromEnd=*/true);
if (!ordinal) {
return funcOp.emitError()
<< "register allocation failed for entry block arg "
<< blockArg.getArgNumber() << " (int bank overflow)";
}
map_[blockArg] = Register::getValue(type, *ordinal);
// Set assigned in interval for expiration tracking.
if (const LiveInterval *interval = liveIntervals.getInterval(blockArg)) {
intervalAllocations_[interval].assigned = *ordinal;
}
}
}
// ===== PHASE B: Compute coalescing candidates =====
// Find values that could potentially share registers via hand-off.
// A value V can inherit register from value U if:
// 1. U's interval ends exactly where V's begins (hand-off point)
// 2. U and V have compatible types (same register bank)
//
// We store U (the source) for each V (the dest), and resolve to register
// ordinals during allocation when U has been assigned.
llvm::DenseMap<Value, Value> coalesceSource;
auto recordCoalesceCandidate = [&](Value dest, Value src) {
if (dest.getType() != src.getType()) {
return;
}
auto srcInterval = liveIntervals.getInterval(src);
auto destInterval = liveIntervals.getInterval(dest);
if (!srcInterval || !destInterval) {
return;
}
// Only coalesce if intervals meet exactly (hand-off).
if (srcInterval->end != destInterval->start) {
return;
}
coalesceSource[dest] = src;
};
// Scan all operations for coalescing opportunities.
for (auto *block : liveIntervals.getBlockOrder()) {
// Block arguments can coalesce with branch operands from predecessors.
for (auto *pred : block->getPredecessors()) {
auto branchOp = dyn_cast<BranchOpInterface>(pred->getTerminator());
if (!branchOp) {
continue;
}
for (unsigned succIdx = 0;
succIdx < pred->getTerminator()->getNumSuccessors(); ++succIdx) {
if (pred->getTerminator()->getSuccessor(succIdx) != block) {
continue;
}
OperandRange operands =
branchOp.getSuccessorOperands(succIdx).getForwardedOperands();
for (auto [idx, operand] : llvm::enumerate(operands)) {
if (idx >= block->getNumArguments()) {
break;
}
recordCoalesceCandidate(block->getArgument(idx), operand);
}
}
}
// Operation results can coalesce with operands whose intervals end here.
for (auto &op : block->getOperations()) {
for (Value result : op.getResults()) {
for (Value operand : op.getOperands()) {
recordCoalesceCandidate(result, operand);
}
}
}
}
// ===== PHASE C+D: Linear scan for both register banks =====
// Active intervals sorted by end position (one list per bank).
SmallVector<const LiveInterval *> activeInt;
SmallVector<const LiveInterval *> activeRef;
// Helper to seed active list with pre-allocated entry block arg intervals.
// This allows their registers to be expired and reused.
auto seedActive = [&](SmallVectorImpl<const LiveInterval *> &active,
auto typeCheck) {
for (auto blockArg : entryBlock->getArguments()) {
if (typeCheck(blockArg.getType())) {
if (const LiveInterval *interval =
liveIntervals.getInterval(blockArg)) {
auto it = std::lower_bound(
active.begin(), active.end(), interval,
[](const LiveInterval *a, const LiveInterval *b) {
return a->end < b->end;
});
active.insert(it, interval);
}
}
}
};
seedActive(activeInt, [](Type t) { return t.isIntOrFloat(); });
seedActive(activeRef, [](Type t) { return isa<IREE::VM::RefType>(t); });
// Run linear scan for each bank.
if (failed(runLinearScan(funcOp, liveIntervals, intBank, activeInt,
coalesceSource, /*isRefBank=*/false,
maxI32RegisterOrdinal_))) {
return failure();
}
if (failed(runLinearScan(funcOp, liveIntervals, refBank, activeRef,
coalesceSource, /*isRefBank=*/true,
maxRefRegisterOrdinal_))) {
return failure();
}
// Verify bounds.
if (maxI32RegisterOrdinal_ > Register::kInt32RegisterCount ||
maxRefRegisterOrdinal_ > Register::kRefRegisterCount) {
return funcOp.emitError() << "function overflows stack register banks; "
"spilling to memory not yet implemented";
}
// Identify discards that can be elided because their operands were released
// via MOVE on preceding operations.
computeElidableDiscards(funcOp);
return success();
}
void RegisterAllocation::computeElidableDiscards(IREE::VM::FuncOp funcOp) {
discardOperandElidability_.clear();
// For each discard operand, check if it was released via MOVE on a preceding
// operation. An operand is elidable when:
// 1. The value has a "real" use (non-discard) before this discard in the
// same block, AND
// 2. That real use actually got MOVE set (isLastRealValueUse is true and
// the op implements RefMoveInterface)
//
// Values that only have the discard as their use in a block (e.g., coming
// from block arguments or definitions in predecessors with no real use
// before the discard) cannot be elided - the discard is the only thing
// releasing them.
for (auto &block : funcOp.getBlocks()) {
for (auto &op : block.getOperations()) {
auto discardOp = dyn_cast<IREE::VM::DiscardRefsOp>(&op);
if (!discardOp) {
continue;
}
SmallVector<bool> operandElidability;
for (Value ref : discardOp.getRefs()) {
// Check if there's a preceding real use in this block that got MOVE.
bool hasPrecedingMoveUse = false;
for (auto it = Block::iterator(discardOp); it != block.begin();) {
--it;
Operation *precedingOp = &*it;
for (OpOperand &operand : precedingOp->getOpOperands()) {
if (operand.get() == ref &&
!isa<IREE::VM::DiscardRefsOp>(precedingOp)) {
// Found a real use before the discard.
// Check if MOVE was actually set on this use.
if (liveness_.isLastRealValueUse(ref, precedingOp,
operand.getOperandNumber())) {
// Also check if the op implements RefMoveInterface and says
// this operand is movable.
if (auto moveOp =
dyn_cast<IREE::VM::RefMoveInterface>(precedingOp)) {
if (moveOp.isRefOperandMovable(operand.getOperandNumber())) {
hasPrecedingMoveUse = true;
}
}
}
break;
}
}
if (hasPrecedingMoveUse) {
break;
}
}
operandElidability.push_back(hasPrecedingMoveUse);
}
discardOperandElidability_[discardOp] = std::move(operandElidability);
}
}
}
Register RegisterAllocation::mapToRegister(Value value) const {
auto it = map_.find(value);
assert(it != map_.end());
return it->getSecond();
}
Register RegisterAllocation::mapUseToRegister(Value value, Operation *useOp,
int operandIndex) {
auto reg = mapToRegister(value);
if (reg.isRef() && liveness_.isLastRealValueUse(value, useOp, operandIndex)) {
// Set MOVE bit when this is the last real use of the value. Only set MOVE
// if the op implements RefMoveInterface and says this operand is movable.
// This prevents setting MOVE on ops that don't honor it in the runtime.
if (auto moveOp = dyn_cast<IREE::VM::RefMoveInterface>(useOp)) {
if (moveOp.isRefOperandMovable(operandIndex)) {
reg.setMove(true);
}
}
}
return reg;
}
// A feedback arc set containing the minimal list of cycle-causing edges.
// https://en.wikipedia.org/wiki/Feedback_arc_set
struct FeedbackArcSet {
using NodeID = Register;
using Edge = std::pair<NodeID, NodeID>;
// Edges making up a DAG (inputEdges - feedbackEdges).
SmallVector<Edge, 8> acyclicEdges;
// Edges of the feedback arc set that, if added to acyclicEdges, would cause
// cycles.
SmallVector<Edge, 8> feedbackEdges;
// Computes the FAS of a given directed graph that may contain cycles.
static FeedbackArcSet compute(ArrayRef<Edge> inputEdges) {
FeedbackArcSet result;
if (inputEdges.empty()) {
return result;
} else if (inputEdges.size() == 1) {
result.acyclicEdges.push_back(inputEdges.front());
return result;
}
struct FASNode {
NodeID id;
int indegree = 0;
int outdegree = 0;
};
// This should not be modified after creation in this loop. We take pointers
// to its entries so do not want to invalidate them with reallocation.
llvm::SmallDenseMap<NodeID, FASNode> nodes;
for (auto &edge : inputEdges) {
NodeID sourceID = edge.first.asBaseRegister();
NodeID sinkID = edge.second.asBaseRegister();
assert(sourceID != sinkID && "self-cycles not supported");
if (nodes.count(sourceID) == 0) {
nodes.insert({sourceID, {sourceID, 0, 0}});
}
if (nodes.count(sinkID) == 0) {
nodes.insert({sinkID, {sinkID, 0, 0}});
}
}
struct FASEdge {
FASNode *source;
FASNode *sink;
};
int maxOutdegree = 0;
int maxIndegree = 0;
SmallVector<FASEdge, 8> edges;
for (auto &edge : inputEdges) {
NodeID sourceID = edge.first.asBaseRegister();
NodeID sinkID = edge.second.asBaseRegister();
auto &sourceNode = nodes[sourceID];
++sourceNode.outdegree;
maxOutdegree = std::max(maxOutdegree, sourceNode.outdegree);
auto &sinkNode = nodes[sinkID];
++sinkNode.indegree;
maxIndegree = std::max(maxIndegree, sinkNode.indegree);
edges.push_back({&sourceNode, &sinkNode});
}
std::vector<SmallVector<FASNode *, 2>> buckets;
buckets.resize(std::max(maxOutdegree, maxIndegree) + 2);
auto nodeToBucketIndex = [&](FASNode *node) {
return node->indegree == 0 || node->outdegree == 0
? buckets.size() - 1
: std::abs(node->outdegree - node->indegree);
};
auto assignBucket = [&](FASNode *node) {
buckets[nodeToBucketIndex(node)].push_back(node);
};
auto removeBucket = [&](FASNode *node) {
int index = nodeToBucketIndex(node);
auto it = std::find(buckets[index].begin(), buckets[index].end(), node);
if (it != buckets[index].end()) {
buckets[index].erase(it);
}
};
llvm::SmallPtrSet<FASNode *, 8> remainingNodes;
for (auto &nodeEntry : nodes) {
assignBucket(&nodeEntry.getSecond());
remainingNodes.insert(&nodeEntry.getSecond());
}
auto removeNode = [&](FASNode *node) {
SmallVector<FASEdge> inEdges;
inEdges.reserve(node->indegree);
SmallVector<FASEdge> outEdges;
outEdges.reserve(node->outdegree);
for (auto &edge : edges) {
if (edge.sink == node) {
inEdges.push_back(edge);
}
if (edge.source == node) {
outEdges.push_back(edge);
}
}
bool collectInEdges = node->indegree <= node->outdegree;
bool collectOutEdges = !collectInEdges;
SmallVector<Edge> results;
for (auto &edge : inEdges) {
if (edge.source == node) {
continue;
}
if (collectInEdges) {
results.push_back({edge.source->id, edge.sink->id});
}
removeBucket(edge.source);
--edge.source->outdegree;
assert(edge.source->outdegree >= 0 && "outdegree has become negative");
assignBucket(edge.source);
}
for (auto &edge : outEdges) {
if (edge.sink == node) {
continue;
}
if (collectOutEdges) {
results.push_back({edge.source->id, edge.sink->id});
}
removeBucket(edge.sink);
--edge.sink->indegree;
assert(edge.sink->indegree >= 0 && "indegree has become negative");
assignBucket(edge.sink);
}
remainingNodes.erase(node);
edges.erase(std::remove_if(edges.begin(), edges.end(),
[&](const FASEdge &edge) {
return edge.source == node ||
edge.sink == node;
}),
edges.end());
return results;
};
auto ends = buckets.back();
while (!remainingNodes.empty()) {
while (!ends.empty()) {
auto *node = ends.front();
ends.erase(ends.begin());
removeNode(node);
}
if (remainingNodes.empty()) {
break;
}
for (ssize_t i = buckets.size() - 1; i >= 0; --i) {
if (buckets[i].empty()) {
continue;
}
auto *bucket = buckets[i].front();
buckets[i].erase(buckets[i].begin());
auto feedbackEdges = removeNode(bucket);
result.feedbackEdges.append(feedbackEdges.begin(), feedbackEdges.end());
break;
}
}
// Build the DAG of the remaining edges now that we've isolated the ones
// that cause cycles.
llvm::SmallSetVector<NodeID, 8> acyclicNodes;
SmallVector<Edge, 8> acyclicEdges;
for (auto &inputEdge : inputEdges) {
auto it = std::find_if(result.feedbackEdges.begin(),
result.feedbackEdges.end(), [&](const Edge &edge) {
return edge.first == inputEdge.first &&
edge.second == inputEdge.second;
});
if (it == result.feedbackEdges.end()) {
acyclicEdges.push_back(inputEdge);
acyclicNodes.insert(inputEdge.first.asBaseRegister());
acyclicNodes.insert(inputEdge.second.asBaseRegister());
}
}
// Topologically sort the DAG so that we don't overwrite anything.
llvm::SmallSetVector<NodeID, 8> unmarkedNodes = acyclicNodes;
llvm::SmallSetVector<NodeID, 8> markedNodes;
std::function<void(NodeID)> visit = [&](NodeID node) {
if (markedNodes.contains(node)) {
return;
}
for (auto &edge : acyclicEdges) {
if (edge.first != node) {
continue;
}
visit(edge.second);
}
markedNodes.insert(node);
};
while (!unmarkedNodes.empty()) {
visit(unmarkedNodes.pop_back_val());
}
for (auto node : markedNodes.takeVector()) {
for (auto &edge : acyclicEdges) {
if (edge.first != node) {
continue;
}
result.acyclicEdges.push_back({edge.first, edge.second});
}
}
return result;
}
};
SmallVector<std::pair<Register, Register>, 8>
RegisterAllocation::remapSuccessorRegisters(Operation *op, int successorIndex) {
auto branchOp = cast<BranchOpInterface>(op);
auto *targetBlock = op->getSuccessor(successorIndex);
auto targetOperands =
branchOp.getSuccessorOperands(successorIndex).getForwardedOperands();
// Get the base operand index for this successor's operands in the branch op.
// This is needed to query isLastRealValueUse with the correct global index.
// Note: getBeginOperandIndex() asserts on empty ranges, so we handle that.
unsigned baseOperandIndex =
targetOperands.empty() ? 0 : targetOperands.getBeginOperandIndex();
return remapSuccessorRegisters(op, targetBlock, targetOperands,
baseOperandIndex);
}
SmallVector<std::pair<Register, Register>, 8>
RegisterAllocation::remapSuccessorRegisters(Location loc, Block *targetBlock,
OperandRange targetOperands) {
// Legacy overload - called without branch op context, so no MOVE bits.
// This path is only used for testing/annotation where we don't have the op.
return remapSuccessorRegisters(/*branchOp=*/nullptr, targetBlock,
targetOperands, /*baseOperandIndex=*/0);
}
// Computes register remapping for branch operands from source block to target.
//
// This function handles the MOVE bit logic for ref operands:
// - MOVE transfers ownership (runtime nulls source after copy)
// - No MOVE retains (runtime increments refcount of destination)
//
// MOVE is set when all three conditions are true:
// 1. This is the last occurrence of the ref value in this successor's list
// (handles same ref appearing multiple times like `^bb(%ref, %ref)`)
// 2. The ref is not live after this branch (checked via isLastRealValueUse)
// 3. The branch op supports MOVE on this operand (RefMoveInterface)
//
// MOVE on back-edges (loops) is CORRECT:
// - Ownership transfers to the next iteration's block argument
// - NOT MOVE would leak: source retains AND destination retains = refcount++
// - At loop exit, source register would be non-null causing spurious release
//
// Returns a list of (src, dst) register pairs that need to be remapped.
// Pairs where src == dst (coalesced) are omitted.
SmallVector<std::pair<Register, Register>, 8>
RegisterAllocation::remapSuccessorRegisters(Operation *branchOp,
Block *targetBlock,
OperandRange targetOperands,
unsigned baseOperandIndex) {
// Compute the initial directed graph of register movements.
// This may contain cycles ([reg 0->1], [reg 1->0], ...) that would not be
// possible to evaluate as a direct remapping.
SmallVector<std::pair<Register, Register>, 8> srcDstRegs;
// Build last-occurrence map for ref operands in this successor's list.
// Only the last occurrence of a value can get MOVE (for multi-use cases like
// passing same ref to both branches of cond_br).
llvm::DenseMap<Value, unsigned> lastOccurrence;
for (auto [idx, operand] : llvm::enumerate(targetOperands)) {
if (isa<IREE::VM::RefType>(operand.getType())) {
lastOccurrence[operand] = idx;
}
}
for (auto [idx, operand] : llvm::enumerate(targetOperands)) {
auto srcReg = mapToRegister(operand);
BlockArgument targetArg = targetBlock->getArgument(idx);
auto dstReg = mapToRegister(targetArg);
// Apply MOVE bit logic for ref registers.
if (branchOp && srcReg.isRef()) {
// Check if this is the last occurrence of this value in the operand list.
bool isLastOccurrence = (lastOccurrence.lookup(operand) == idx);
// Compute global operand index for liveness query.
unsigned globalOperandIndex = baseOperandIndex + idx;
// Check if this is the last real use of the value (not live after branch,
// not used elsewhere in the function after this point).
bool isLastUse =
liveness_.isLastRealValueUse(operand, branchOp, globalOperandIndex);
// Check if the branch op supports MOVE on this operand.
bool isMovable = false;
if (auto moveOp = dyn_cast<IREE::VM::RefMoveInterface>(branchOp)) {
isMovable = moveOp.isRefOperandMovable(globalOperandIndex);
}
// Set MOVE if:
// 1. This is the last occurrence in this successor's operand list
// 2. This is the last real use of the value (not live after branch)
// 3. The branch op supports MOVE on this operand
// Note: MOVE is correct even on back-edges. The ref ownership transfers
// to the destination block arg, which is correct for loop-carried values.
if (isLastOccurrence && isLastUse && isMovable) {
srcReg.setMove(true);
}
}
if (srcReg != dstReg) {
srcDstRegs.push_back({srcReg, dstReg});
}
}
// Compute the feedback arc set to determine which edges are the ones inducing
// cycles, if any. This also provides us a DAG that we can trivially remap
// without worrying about cycles.
auto feedbackArcSet = FeedbackArcSet::compute(srcDstRegs);
assert(feedbackArcSet.acyclicEdges.size() +
feedbackArcSet.feedbackEdges.size() ==
srcDstRegs.size() &&
"lost an edge during feedback arc set computation");
// If there's no cycles we can simply use the sorted DAG produced.
if (feedbackArcSet.feedbackEdges.empty()) {
return feedbackArcSet.acyclicEdges;
}
// The tail registers in each bank is reserved for swapping, when required.
int localScratchI32RegCount = 0;
int localScratchRefRegCount = 0;
for (auto feedbackEdge : feedbackArcSet.feedbackEdges) {
Register scratchReg;
if (feedbackEdge.first.isRef()) {
localScratchRefRegCount += 1;
scratchReg = Register::getWithSameType(
feedbackEdge.first, maxRefRegisterOrdinal_ + localScratchRefRegCount);
} else {
localScratchI32RegCount += 1;
scratchReg = Register::getWithSameType(
feedbackEdge.first, maxI32RegisterOrdinal_ + localScratchI32RegCount);
// Integer types that use more than one register slot will be emitted
// as remapping per 4-byte word, so we have to account for the extra
// temporaries. See BytecodeEncoder:encodeBranch().
assert(scratchReg.byteWidth() >= 4 && "expected >= i32");
localScratchI32RegCount += scratchReg.byteWidth() / 4 - 1;
}
feedbackArcSet.acyclicEdges.insert(feedbackArcSet.acyclicEdges.begin(),
{feedbackEdge.first, scratchReg});
// The scratch register's use as source is its last use, so set MOVE.
// This ensures the ref in the scratch register is released after the copy,
// preventing leaks when the branch takes an alternate path (e.g., loop
// exit).
Register scratchSrc = scratchReg;
if (scratchSrc.isRef()) {
scratchSrc.setMove(true);
}
feedbackArcSet.acyclicEdges.push_back({scratchSrc, feedbackEdge.second});
}
if (localScratchI32RegCount > 0) {
scratchI32RegisterCount_ =
std::max(scratchI32RegisterCount_, localScratchI32RegCount);
assert(getMaxI32RegisterOrdinal() <= Register::kInt32RegisterCount &&
"spilling i32 regs");
if (getMaxI32RegisterOrdinal() > Register::kInt32RegisterCount &&
branchOp) {
mlir::emitError(branchOp->getLoc())
<< "spilling entire i32 register address space";
}
}
if (localScratchRefRegCount > 0) {
scratchRefRegisterCount_ =
std::max(scratchRefRegisterCount_, localScratchRefRegCount);
assert(getMaxRefRegisterOrdinal() <= Register::kRefRegisterCount &&
"spilling ref regs");
if (getMaxRefRegisterOrdinal() > Register::kRefRegisterCount && branchOp) {
mlir::emitError(branchOp->getLoc())
<< "spilling entire ref register address space";
}
}
return feedbackArcSet.acyclicEdges;
}
} // namespace mlir::iree_compiler