blob: 60f49d4e305c44b80beb830418b5df0e9a459da0 [file] [log] [blame]
//===- BufferPlacement.cpp - the impl for buffer placement ---------------===//
//
// Part of the LLVM Project, 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
//
//===----------------------------------------------------------------------===//
//
// This file implements logic for computing correct alloc and dealloc positions.
// The main class is the BufferPlacementPass class that implements the
// underlying algorithm. In order to put allocations and deallocations at safe
// positions, it is significantly important to put them into the correct blocks.
// However, the liveness analysis does not pay attention to aliases, which can
// occur due to branches (and their associated block arguments) in general. For
// this purpose, BufferPlacement firstly finds all possible aliases for a single
// value (using the BufferPlacementAliasAnalysis class). Consider the following
// example:
//
// ^bb0(%arg0):
// cond_br %cond, ^bb1, ^bb2
// ^bb1:
// br ^exit(%arg0)
// ^bb2:
// %new_value = ...
// br ^exit(%new_value)
// ^exit(%arg1):
// return %arg1;
//
// Using liveness information on its own would cause us to place the allocs and
// deallocs in the wrong block. This is due to the fact that %new_value will not
// be liveOut of its block. Instead, we have to place the alloc for %new_value
// in bb0 and its associated dealloc in exit. Using the class
// BufferPlacementAliasAnalysis, we will find out that %new_value has a
// potential alias %arg1. In order to find the dealloc position we have to find
// all potential aliases, iterate over their uses and find the common
// post-dominator block. In this block we can safely be sure that %new_value
// will die and can use liveness information to determine the exact operation
// after which we have to insert the dealloc. Finding the alloc position is
// highly similar and non- obvious. Again, we have to consider all potential
// aliases and find the common dominator block to place the alloc.
//
// TODO:
// The current implementation does not support loops and the resulting code will
// be invalid with respect to program semantics. The only thing that is
// currently missing is a high-level loop analysis that allows us to move allocs
// and deallocs outside of the loop blocks. Furthermore, it doesn't also accept
// functions which return buffers already.
//
//===----------------------------------------------------------------------===//
#include "mlir/Transforms/BufferPlacement.h"
#include "mlir/IR/Function.h"
#include "mlir/IR/Operation.h"
#include "mlir/Pass/Pass.h"
#include "mlir/Transforms/Passes.h"
using namespace mlir;
namespace {
//===----------------------------------------------------------------------===//
// BufferPlacementAliasAnalysis
//===----------------------------------------------------------------------===//
/// A straight-forward alias analysis which ensures that all aliases of all
/// values will be determined. This is a requirement for the BufferPlacement
/// class since you need to determine safe positions to place alloc and
/// deallocs.
class BufferPlacementAliasAnalysis {
public:
using ValueSetT = SmallPtrSet<Value, 16>;
public:
/// Constructs a new alias analysis using the op provided.
BufferPlacementAliasAnalysis(Operation *op) { build(op->getRegions()); }
/// Finds all immediate and indirect aliases this value could potentially
/// have. Note that the resulting set will also contain the value provided as
/// it is an alias of itself.
ValueSetT resolve(Value value) const {
ValueSetT result;
resolveRecursive(value, result);
return result;
}
private:
/// Recursively determines alias information for the given value. It stores
/// all newly found potential aliases in the given result set.
void resolveRecursive(Value value, ValueSetT &result) const {
if (!result.insert(value).second)
return;
auto it = aliases.find(value);
if (it == aliases.end())
return;
for (Value alias : it->second)
resolveRecursive(alias, result);
}
/// This function constructs a mapping from values to its immediate aliases.
/// It iterates over all blocks, gets their predecessors, determines the
/// values that will be passed to the corresponding block arguments and
/// inserts them into the underlying map.
void build(MutableArrayRef<Region> regions) {
for (Region &region : regions) {
for (Block &block : region) {
// Iterate over all predecessor and get the mapped values to their
// corresponding block arguments values.
for (auto it = block.pred_begin(), e = block.pred_end(); it != e;
++it) {
unsigned successorIndex = it.getSuccessorIndex();
// Get the terminator and the values that will be passed to our block.
auto branchInterface =
dyn_cast<BranchOpInterface>((*it)->getTerminator());
if (!branchInterface)
continue;
// Query the branch op interace to get the successor operands.
auto successorOperands =
branchInterface.getSuccessorOperands(successorIndex);
if (successorOperands.hasValue()) {
// Build the actual mapping of values to their immediate aliases.
for (auto argPair : llvm::zip(block.getArguments(),
successorOperands.getValue())) {
aliases[std::get<1>(argPair)].insert(std::get<0>(argPair));
}
}
}
}
}
}
/// Maps values to all immediate aliases this value can have.
llvm::DenseMap<Value, ValueSetT> aliases;
};
//===----------------------------------------------------------------------===//
// BufferPlacementPositions
//===----------------------------------------------------------------------===//
/// Stores correct alloc and dealloc positions to place dialect-specific alloc
/// and dealloc operations.
struct BufferPlacementPositions {
public:
BufferPlacementPositions()
: allocPosition(nullptr), deallocPosition(nullptr) {}
/// Creates a new positions tuple including alloc and dealloc positions.
BufferPlacementPositions(Operation *allocPosition, Operation *deallocPosition)
: allocPosition(allocPosition), deallocPosition(deallocPosition) {}
/// Returns the alloc position before which the alloc operation has to be
/// inserted.
Operation *getAllocPosition() const { return allocPosition; }
/// Returns the dealloc position after which the dealloc operation has to be
/// inserted.
Operation *getDeallocPosition() const { return deallocPosition; }
private:
Operation *allocPosition;
Operation *deallocPosition;
};
//===----------------------------------------------------------------------===//
// BufferPlacementAnalysis
//===----------------------------------------------------------------------===//
// The main buffer placement analysis used to place allocs and deallocs.
class BufferPlacementAnalysis {
public:
using DeallocSetT = SmallPtrSet<Operation *, 2>;
public:
BufferPlacementAnalysis(Operation *op)
: operation(op), liveness(op), dominators(op), postDominators(op),
aliases(op) {}
/// Computes the actual positions to place allocs and deallocs for the given
/// value.
BufferPlacementPositions
computeAllocAndDeallocPositions(OpResult result) const {
if (result.use_empty())
return BufferPlacementPositions(result.getOwner(), result.getOwner());
// Get all possible aliases.
auto possibleValues = aliases.resolve(result);
return BufferPlacementPositions(getAllocPosition(result, possibleValues),
getDeallocPosition(result, possibleValues));
}
/// Finds all associated dealloc nodes for the alloc nodes using alias
/// information.
DeallocSetT findAssociatedDeallocs(OpResult allocResult) const {
DeallocSetT result;
auto possibleValues = aliases.resolve(allocResult);
for (Value alias : possibleValues)
for (Operation *op : alias.getUsers()) {
// Check for an existing memory effect interface.
auto effectInstance = dyn_cast<MemoryEffectOpInterface>(op);
if (!effectInstance)
continue;
// Check whether the associated value will be freed using the current
// operation.
SmallVector<MemoryEffects::EffectInstance, 2> effects;
effectInstance.getEffectsOnValue(alias, effects);
if (llvm::any_of(effects, [=](MemoryEffects::EffectInstance &it) {
return isa<MemoryEffects::Free>(it.getEffect());
}))
result.insert(op);
}
return result;
}
/// Dumps the buffer placement information to the given stream.
void print(raw_ostream &os) const {
os << "// ---- Buffer Placement -----\n";
for (Region &region : operation->getRegions())
for (Block &block : region)
for (Operation &operation : block)
for (OpResult result : operation.getResults()) {
BufferPlacementPositions positions =
computeAllocAndDeallocPositions(result);
os << "Positions for ";
result.print(os);
os << "\n Alloc: ";
positions.getAllocPosition()->print(os);
os << "\n Dealloc: ";
positions.getDeallocPosition()->print(os);
os << "\n";
}
}
private:
/// Finds a correct placement block to store alloc/dealloc node according to
/// the algorithm described at the top of the file. It supports dominator and
/// post-dominator analyses via template arguments.
template <typename DominatorT>
Block *
findPlacementBlock(OpResult result,
const BufferPlacementAliasAnalysis::ValueSetT &aliases,
const DominatorT &doms) const {
// Start with the current block the value is defined in.
Block *dom = result.getOwner()->getBlock();
// Iterate over all aliases and their uses to find a safe placement block
// according to the given dominator information.
for (Value alias : aliases)
for (Operation *user : alias.getUsers()) {
// Move upwards in the dominator tree to find an appropriate
// dominator block that takes the current use into account.
dom = doms.findNearestCommonDominator(dom, user->getBlock());
}
return dom;
}
/// Finds a correct alloc position according to the algorithm described at
/// the top of the file.
Operation *getAllocPosition(
OpResult result,
const BufferPlacementAliasAnalysis::ValueSetT &aliases) const {
// Determine the actual block to place the alloc and get liveness
// information.
Block *placementBlock = findPlacementBlock(result, aliases, dominators);
const LivenessBlockInfo *livenessInfo =
liveness.getLiveness(placementBlock);
// We have to ensure that the alloc will be before the first use of all
// aliases of the given value. We first assume that there are no uses in the
// placementBlock and that we can safely place the alloc before the
// terminator at the end of the block.
Operation *startOperation = placementBlock->getTerminator();
// Iterate over all aliases and ensure that the startOperation will point to
// the first operation of all potential aliases in the placementBlock.
for (Value alias : aliases) {
Operation *aliasStartOperation = livenessInfo->getStartOperation(alias);
// Check whether the aliasStartOperation lies in the desired block and
// whether it is before the current startOperation. If yes, this will be
// the new startOperation.
if (aliasStartOperation->getBlock() == placementBlock &&
aliasStartOperation->isBeforeInBlock(startOperation))
startOperation = aliasStartOperation;
}
// startOperation is the first operation before which we can safely store
// the alloc taking all potential aliases into account.
return startOperation;
}
/// Finds a correct dealloc position according to the algorithm described at
/// the top of the file.
Operation *getDeallocPosition(
OpResult result,
const BufferPlacementAliasAnalysis::ValueSetT &aliases) const {
// Determine the actual block to place the dealloc and get liveness
// information.
Block *placementBlock = findPlacementBlock(result, aliases, postDominators);
const LivenessBlockInfo *livenessInfo =
liveness.getLiveness(placementBlock);
// We have to ensure that the dealloc will be after the last use of all
// aliases of the given value. We first assume that there are no uses in the
// placementBlock and that we can safely place the dealloc at the beginning.
Operation *endOperation = &placementBlock->front();
// Iterate over all aliases and ensure that the endOperation will point to
// the last operation of all potential aliases in the placementBlock.
for (Value alias : aliases) {
Operation *aliasEndOperation =
livenessInfo->getEndOperation(alias, endOperation);
// Check whether the aliasEndOperation lies in the desired block and
// whether it is behind the current endOperation. If yes, this will be the
// new endOperation.
if (aliasEndOperation->getBlock() == placementBlock &&
endOperation->isBeforeInBlock(aliasEndOperation))
endOperation = aliasEndOperation;
}
// endOperation is the last operation behind which we can safely store the
// dealloc taking all potential aliases into account.
return endOperation;
}
/// The operation this transformation was constructed from.
Operation *operation;
/// The underlying liveness analysis to compute fine grained information about
/// alloc and dealloc positions.
Liveness liveness;
/// The dominator analysis to place allocs in the appropriate blocks.
DominanceInfo dominators;
/// The post dominator analysis to place deallocs in the appropriate blocks.
PostDominanceInfo postDominators;
/// The internal alias analysis to ensure that allocs and deallocs take all
/// their potential aliases into account.
BufferPlacementAliasAnalysis aliases;
};
//===----------------------------------------------------------------------===//
// BufferPlacementPass
//===----------------------------------------------------------------------===//
/// The actual buffer placement pass that moves alloc and dealloc nodes into
/// the right positions. It uses the algorithm described at the top of the file.
struct BufferPlacementPass
: mlir::PassWrapper<BufferPlacementPass, FunctionPass> {
void runOnFunction() override {
// Get required analysis information first.
auto &analysis = getAnalysis<BufferPlacementAnalysis>();
// Compute an initial placement of all nodes.
llvm::SmallVector<std::pair<OpResult, BufferPlacementPositions>, 16>
placements;
getFunction().walk([&](MemoryEffectOpInterface op) {
// Try to find a single allocation result.
SmallVector<MemoryEffects::EffectInstance, 2> effects;
op.getEffects(effects);
SmallVector<MemoryEffects::EffectInstance, 2> allocateResultEffects;
llvm::copy_if(effects, std::back_inserter(allocateResultEffects),
[=](MemoryEffects::EffectInstance &it) {
Value value = it.getValue();
return isa<MemoryEffects::Allocate>(it.getEffect()) &&
value && value.isa<OpResult>();
});
// If there is one result only, we will be able to move the allocation and
// (possibly existing) deallocation ops.
if (allocateResultEffects.size() == 1) {
// Insert allocation result.
auto allocResult = allocateResultEffects[0].getValue().cast<OpResult>();
placements.emplace_back(
allocResult, analysis.computeAllocAndDeallocPositions(allocResult));
}
});
// Move alloc (and dealloc - if any) nodes into the right places and insert
// dealloc nodes if necessary.
for (auto &entry : placements) {
// Find already associated dealloc nodes.
OpResult alloc = entry.first;
auto deallocs = analysis.findAssociatedDeallocs(alloc);
if (deallocs.size() > 1) {
emitError(alloc.getLoc(),
"not supported number of associated dealloc operations");
return;
}
// Move alloc node to the right place.
BufferPlacementPositions &positions = entry.second;
Operation *allocOperation = alloc.getOwner();
allocOperation->moveBefore(positions.getAllocPosition());
// If there is an existing dealloc, move it to the right place.
Operation *nextOp = positions.getDeallocPosition()->getNextNode();
// If the Dealloc position is at the terminator operation of the block,
// then the value should escape from a deallocation.
if (!nextOp) {
assert(deallocs.size() == 0 &&
"There should be no dealloc for the returned buffer");
continue;
}
if (deallocs.size()) {
(*deallocs.begin())->moveBefore(nextOp);
} else {
// If there is no dealloc node, insert one in the right place.
OpBuilder builder(nextOp);
builder.create<DeallocOp>(allocOperation->getLoc(), alloc);
}
}
};
};
} // end anonymous namespace
//===----------------------------------------------------------------------===//
// BufferAssignmentPlacer
//===----------------------------------------------------------------------===//
/// Creates a new assignment placer.
BufferAssignmentPlacer::BufferAssignmentPlacer(Operation *op) : operation(op) {}
/// Computes the actual position to place allocs for the given value.
OpBuilder::InsertPoint
BufferAssignmentPlacer::computeAllocPosition(OpResult result) {
Operation *owner = result.getOwner();
return OpBuilder::InsertPoint(owner->getBlock(), Block::iterator(owner));
}
//===----------------------------------------------------------------------===//
// FunctionAndBlockSignatureConverter
//===----------------------------------------------------------------------===//
// Performs the actual signature rewriting step.
LogicalResult FunctionAndBlockSignatureConverter::matchAndRewrite(
FuncOp funcOp, ArrayRef<Value> operands,
ConversionPatternRewriter &rewriter) const {
if (!converter) {
funcOp.emitError("The type converter has not been defined for "
"FunctionAndBlockSignatureConverter");
return failure();
}
auto funcType = funcOp.getType();
// Convert function arguments using the provided TypeConverter.
TypeConverter::SignatureConversion conversion(funcType.getNumInputs());
for (auto argType : llvm::enumerate(funcType.getInputs()))
conversion.addInputs(argType.index(),
converter->convertType(argType.value()));
// If a function result type is not a memref but it would be a memref after
// type conversion, a new argument should be appended to the function
// arguments list for this result. Otherwise, it remains unchanged as a
// function result.
SmallVector<Type, 2> newResultTypes;
newResultTypes.reserve(funcOp.getNumResults());
for (Type resType : funcType.getResults()) {
Type convertedType = converter->convertType(resType);
if (BufferAssignmentTypeConverter::isConvertedMemref(convertedType,
resType))
conversion.addInputs(convertedType);
else
newResultTypes.push_back(convertedType);
}
// Update the signature of the function.
rewriter.updateRootInPlace(funcOp, [&] {
funcOp.setType(rewriter.getFunctionType(conversion.getConvertedTypes(),
newResultTypes));
rewriter.applySignatureConversion(&funcOp.getBody(), conversion);
});
return success();
}
//===----------------------------------------------------------------------===//
// BufferAssignmentTypeConverter
//===----------------------------------------------------------------------===//
/// Registers conversions into BufferAssignmentTypeConverter
BufferAssignmentTypeConverter::BufferAssignmentTypeConverter() {
// Keep all types unchanged.
addConversion([](Type type) { return type; });
// A type conversion that converts ranked-tensor type to memref type.
addConversion([](RankedTensorType type) {
return (Type)MemRefType::get(type.getShape(), type.getElementType());
});
}
/// Checks if `type` has been converted from non-memref type to memref.
bool BufferAssignmentTypeConverter::isConvertedMemref(Type type, Type before) {
return type.isa<MemRefType>() && !before.isa<MemRefType>();
}
//===----------------------------------------------------------------------===//
// BufferPlacementPass construction
//===----------------------------------------------------------------------===//
std::unique_ptr<Pass> mlir::createBufferPlacementPass() {
return std::make_unique<BufferPlacementPass>();
}