llvm-project/mlir/lib/Transforms/BufferOptimizations.cpp

418 lines
17 KiB
C++

//===- BufferOptimizations.cpp - pre-pass optimizations for bufferization -===//
//
// 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 three optimization passes. The first two
// passes try to move alloc nodes out of blocks to reduce the number of
// allocations and copies during buffer deallocation. The third pass tries to
// convert heap-based allocations to stack-based allocations, if possible.
#include "PassDetail.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/IR/Operation.h"
#include "mlir/Interfaces/LoopLikeInterface.h"
#include "mlir/Pass/Pass.h"
#include "mlir/Transforms/BufferUtils.h"
#include "mlir/Transforms/Passes.h"
using namespace mlir;
/// Returns true if the given operation implements a known high-level region-
/// based control-flow interface.
static bool isKnownControlFlowInterface(Operation *op) {
return isa<LoopLikeOpInterface, RegionBranchOpInterface>(op);
}
/// Check if the size of the allocation is less than the given size. The
/// transformation is only applied to small buffers since large buffers could
/// exceed the stack space.
static bool defaultIsSmallAlloc(Value alloc, unsigned maximumSizeInBytes,
unsigned bitwidthOfIndexType,
unsigned maxRankOfAllocatedMemRef) {
auto type = alloc.getType().dyn_cast<ShapedType>();
if (!type || !alloc.getDefiningOp<memref::AllocOp>())
return false;
if (!type.hasStaticShape()) {
// Check if the dynamic shape dimension of the alloc is produced by RankOp.
// If this is the case, it is likely to be small. Furthermore, the dimension
// is limited to the maximum rank of the allocated memref to avoid large
// values by multiplying several small values.
if (type.getRank() <= maxRankOfAllocatedMemRef) {
return llvm::all_of(
alloc.getDefiningOp()->getOperands(),
[&](Value operand) { return operand.getDefiningOp<RankOp>(); });
}
return false;
}
// For index types, use the provided size, as the type does not know.
unsigned int bitwidth = type.getElementType().isIndex()
? bitwidthOfIndexType
: type.getElementTypeBitWidth();
return type.getNumElements() * bitwidth <= maximumSizeInBytes * 8;
}
/// Checks whether the given aliases leave the allocation scope.
static bool
leavesAllocationScope(Region *parentRegion,
const BufferAliasAnalysis::ValueSetT &aliases) {
for (Value alias : aliases) {
for (auto *use : alias.getUsers()) {
// If there is at least one alias that leaves the parent region, we know
// that this alias escapes the whole region and hence the associated
// allocation leaves allocation scope.
if (use->hasTrait<OpTrait::ReturnLike>() &&
use->getParentRegion() == parentRegion)
return true;
}
}
return false;
}
/// Checks, if an automated allocation scope for a given alloc value exists.
static bool hasAllocationScope(Value alloc,
const BufferAliasAnalysis &aliasAnalysis) {
Region *region = alloc.getParentRegion();
do {
if (Operation *parentOp = region->getParentOp()) {
// Check if the operation is an automatic allocation scope and whether an
// alias leaves the scope. This means, an allocation yields out of
// this scope and can not be transformed in a stack-based allocation.
if (parentOp->hasTrait<OpTrait::AutomaticAllocationScope>() &&
!leavesAllocationScope(region, aliasAnalysis.resolve(alloc)))
return true;
// Check if the operation is a known control flow interface and break the
// loop to avoid transformation in loops. Furthermore skip transformation
// if the operation does not implement a RegionBeanchOpInterface.
if (BufferPlacementTransformationBase::isLoop(parentOp) ||
!isKnownControlFlowInterface(parentOp))
break;
}
} while ((region = region->getParentRegion()));
return false;
}
namespace {
//===----------------------------------------------------------------------===//
// BufferAllocationHoisting
//===----------------------------------------------------------------------===//
/// A base implementation compatible with the `BufferAllocationHoisting` class.
struct BufferAllocationHoistingStateBase {
/// A pointer to the current dominance info.
DominanceInfo *dominators;
/// The current allocation value.
Value allocValue;
/// The current placement block (if any).
Block *placementBlock;
/// Initializes the state base.
BufferAllocationHoistingStateBase(DominanceInfo *dominators, Value allocValue,
Block *placementBlock)
: dominators(dominators), allocValue(allocValue),
placementBlock(placementBlock) {}
};
/// Implements the actual hoisting logic for allocation nodes.
template <typename StateT>
class BufferAllocationHoisting : public BufferPlacementTransformationBase {
public:
BufferAllocationHoisting(Operation *op)
: BufferPlacementTransformationBase(op), dominators(op),
postDominators(op) {}
/// Moves allocations upwards.
void hoist() {
for (BufferPlacementAllocs::AllocEntry &entry : allocs) {
Value allocValue = std::get<0>(entry);
Operation *definingOp = allocValue.getDefiningOp();
assert(definingOp && "No defining op");
auto operands = definingOp->getOperands();
auto resultAliases = aliases.resolve(allocValue);
// Determine the common dominator block of all aliases.
Block *dominatorBlock =
findCommonDominator(allocValue, resultAliases, dominators);
// Init the initial hoisting state.
StateT state(&dominators, allocValue, allocValue.getParentBlock());
// Check for additional allocation dependencies to compute an upper bound
// for hoisting.
Block *dependencyBlock = nullptr;
// If this node has dependencies, check all dependent nodes. This ensures
// that all dependency values have been computed before allocating the
// buffer.
for (Value depValue : operands) {
Block *depBlock = depValue.getParentBlock();
if (!dependencyBlock || dominators.dominates(dependencyBlock, depBlock))
dependencyBlock = depBlock;
}
// Find the actual placement block and determine the start operation using
// an upper placement-block boundary. The idea is that placement block
// cannot be moved any further upwards than the given upper bound.
Block *placementBlock = findPlacementBlock(
state, state.computeUpperBound(dominatorBlock, dependencyBlock));
Operation *startOperation = BufferPlacementAllocs::getStartOperation(
allocValue, placementBlock, liveness);
// Move the alloc in front of the start operation.
Operation *allocOperation = allocValue.getDefiningOp();
allocOperation->moveBefore(startOperation);
}
}
private:
/// Finds a valid placement block by walking upwards in the CFG until we
/// either cannot continue our walk due to constraints (given by the StateT
/// implementation) or we have reached the upper-most dominator block.
Block *findPlacementBlock(StateT &state, Block *upperBound) {
Block *currentBlock = state.placementBlock;
// Walk from the innermost regions/loops to the outermost regions/loops and
// find an appropriate placement block that satisfies the constraint of the
// current StateT implementation. Walk until we reach the upperBound block
// (if any).
// If we are not able to find a valid parent operation or an associated
// parent block, break the walk loop.
Operation *parentOp;
Block *parentBlock;
while ((parentOp = currentBlock->getParentOp()) &&
(parentBlock = parentOp->getBlock()) &&
(!upperBound ||
dominators.properlyDominates(upperBound, currentBlock))) {
// Try to find an immediate dominator and check whether the parent block
// is above the immediate dominator (if any).
DominanceInfoNode *idom = dominators.getNode(currentBlock)->getIDom();
if (idom && dominators.properlyDominates(parentBlock, idom->getBlock())) {
// If the current immediate dominator is below the placement block, move
// to the immediate dominator block.
currentBlock = idom->getBlock();
state.recordMoveToDominator(currentBlock);
} else {
// We have to move to our parent block since an immediate dominator does
// either not exist or is above our parent block. If we cannot move to
// our parent operation due to constraints given by the StateT
// implementation, break the walk loop. Furthermore, we should not move
// allocations out of unknown region-based control-flow operations.
if (!isKnownControlFlowInterface(parentOp) ||
!state.isLegalPlacement(parentOp))
break;
// Move to our parent block by notifying the current StateT
// implementation.
currentBlock = parentBlock;
state.recordMoveToParent(currentBlock);
}
}
// Return the finally determined placement block.
return state.placementBlock;
}
/// The dominator info to find the appropriate start operation to move the
/// allocs.
DominanceInfo dominators;
/// The post dominator info to move the dependent allocs in the right
/// position.
PostDominanceInfo postDominators;
/// The map storing the final placement blocks of a given alloc value.
llvm::DenseMap<Value, Block *> placementBlocks;
};
/// A state implementation compatible with the `BufferAllocationHoisting` class
/// that hoists allocations into dominator blocks while keeping them inside of
/// loops.
struct BufferAllocationHoistingState : BufferAllocationHoistingStateBase {
using BufferAllocationHoistingStateBase::BufferAllocationHoistingStateBase;
/// Computes the upper bound for the placement block search.
Block *computeUpperBound(Block *dominatorBlock, Block *dependencyBlock) {
// If we do not have a dependency block, the upper bound is given by the
// dominator block.
if (!dependencyBlock)
return dominatorBlock;
// Find the "lower" block of the dominator and the dependency block to
// ensure that we do not move allocations above this block.
return dominators->properlyDominates(dominatorBlock, dependencyBlock)
? dependencyBlock
: dominatorBlock;
}
/// Returns true if the given operation does not represent a loop.
bool isLegalPlacement(Operation *op) {
return !BufferPlacementTransformationBase::isLoop(op);
}
/// Sets the current placement block to the given block.
void recordMoveToDominator(Block *block) { placementBlock = block; }
/// Sets the current placement block to the given block.
void recordMoveToParent(Block *block) { recordMoveToDominator(block); }
};
/// A state implementation compatible with the `BufferAllocationHoisting` class
/// that hoists allocations out of loops.
struct BufferAllocationLoopHoistingState : BufferAllocationHoistingStateBase {
using BufferAllocationHoistingStateBase::BufferAllocationHoistingStateBase;
/// Remembers the dominator block of all aliases.
Block *aliasDominatorBlock;
/// Computes the upper bound for the placement block search.
Block *computeUpperBound(Block *dominatorBlock, Block *dependencyBlock) {
aliasDominatorBlock = dominatorBlock;
// If there is a dependency block, we have to use this block as an upper
// bound to satisfy all allocation value dependencies.
return dependencyBlock ? dependencyBlock : nullptr;
}
/// Returns true if the given operation represents a loop and one of the
/// aliases caused the `aliasDominatorBlock` to be "above" the block of the
/// given loop operation. If this is the case, it indicates that the
/// allocation is passed via a back edge.
bool isLegalPlacement(Operation *op) {
return BufferPlacementTransformationBase::isLoop(op) &&
!dominators->dominates(aliasDominatorBlock, op->getBlock());
}
/// Does not change the internal placement block, as we want to move
/// operations out of loops only.
void recordMoveToDominator(Block *block) {}
/// Sets the current placement block to the given block.
void recordMoveToParent(Block *block) { placementBlock = block; }
};
//===----------------------------------------------------------------------===//
// BufferPlacementPromotion
//===----------------------------------------------------------------------===//
/// Promotes heap-based allocations to stack-based allocations (if possible).
class BufferPlacementPromotion : BufferPlacementTransformationBase {
public:
BufferPlacementPromotion(Operation *op)
: BufferPlacementTransformationBase(op) {}
/// Promote buffers to stack-based allocations.
void promote(function_ref<bool(Value)> isSmallAlloc) {
for (BufferPlacementAllocs::AllocEntry &entry : allocs) {
Value alloc = std::get<0>(entry);
Operation *dealloc = std::get<1>(entry);
// Checking several requirements to transform an AllocOp into an AllocaOp.
// The transformation is done if the allocation is limited to a given
// size. Furthermore, a deallocation must not be defined for this
// allocation entry and a parent allocation scope must exist.
if (!isSmallAlloc(alloc) || dealloc ||
!hasAllocationScope(alloc, aliases))
continue;
Operation *startOperation = BufferPlacementAllocs::getStartOperation(
alloc, alloc.getParentBlock(), liveness);
// Build a new alloca that is associated with its parent
// `AutomaticAllocationScope` determined during the initialization phase.
OpBuilder builder(startOperation);
Operation *allocOp = alloc.getDefiningOp();
Operation *alloca = builder.create<memref::AllocaOp>(
alloc.getLoc(), alloc.getType().cast<MemRefType>(),
allocOp->getOperands());
// Replace the original alloc by a newly created alloca.
allocOp->replaceAllUsesWith(alloca);
allocOp->erase();
}
}
};
//===----------------------------------------------------------------------===//
// BufferOptimizationPasses
//===----------------------------------------------------------------------===//
/// The buffer hoisting pass that hoists allocation nodes into dominating
/// blocks.
struct BufferHoistingPass : BufferHoistingBase<BufferHoistingPass> {
void runOnFunction() override {
// Hoist all allocations into dominator blocks.
BufferAllocationHoisting<BufferAllocationHoistingState> optimizer(
getFunction());
optimizer.hoist();
}
};
/// The buffer loop hoisting pass that hoists allocation nodes out of loops.
struct BufferLoopHoistingPass : BufferLoopHoistingBase<BufferLoopHoistingPass> {
void runOnFunction() override {
// Hoist all allocations out of loops.
BufferAllocationHoisting<BufferAllocationLoopHoistingState> optimizer(
getFunction());
optimizer.hoist();
}
};
/// The promote buffer to stack pass that tries to convert alloc nodes into
/// alloca nodes.
class PromoteBuffersToStackPass
: public PromoteBuffersToStackBase<PromoteBuffersToStackPass> {
public:
PromoteBuffersToStackPass(unsigned maxAllocSizeInBytes,
unsigned bitwidthOfIndexType,
unsigned maxRankOfAllocatedMemRef) {
this->maxAllocSizeInBytes = maxAllocSizeInBytes;
this->bitwidthOfIndexType = bitwidthOfIndexType;
this->maxRankOfAllocatedMemRef = maxRankOfAllocatedMemRef;
}
explicit PromoteBuffersToStackPass(std::function<bool(Value)> isSmallAlloc)
: isSmallAlloc(std::move(isSmallAlloc)) {}
LogicalResult initialize(MLIRContext *context) override {
if (isSmallAlloc == nullptr) {
isSmallAlloc = [=](Value alloc) {
return defaultIsSmallAlloc(alloc, maxAllocSizeInBytes,
bitwidthOfIndexType,
maxRankOfAllocatedMemRef);
};
}
return success();
}
void runOnFunction() override {
// Move all allocation nodes and convert candidates into allocas.
BufferPlacementPromotion optimizer(getFunction());
optimizer.promote(isSmallAlloc);
}
private:
std::function<bool(Value)> isSmallAlloc;
};
} // end anonymous namespace
std::unique_ptr<Pass> mlir::createBufferHoistingPass() {
return std::make_unique<BufferHoistingPass>();
}
std::unique_ptr<Pass> mlir::createBufferLoopHoistingPass() {
return std::make_unique<BufferLoopHoistingPass>();
}
std::unique_ptr<Pass>
mlir::createPromoteBuffersToStackPass(unsigned maxAllocSizeInBytes,
unsigned bitwidthOfIndexType,
unsigned maxRankOfAllocatedMemRef) {
return std::make_unique<PromoteBuffersToStackPass>(
maxAllocSizeInBytes, bitwidthOfIndexType, maxRankOfAllocatedMemRef);
}
std::unique_ptr<Pass>
mlir::createPromoteBuffersToStackPass(std::function<bool(Value)> isSmallAlloc) {
return std::make_unique<PromoteBuffersToStackPass>(std::move(isSmallAlloc));
}