Add a helper function to compute a mapping of successor operands to successor inputs. This mapping is computed in various places. Also add a helper function to gather all region branch points. This commit is in preparation of a bug fix / partial redesign of `-remove-dead-values`. This commit also removes some duplicate code in various places.
309 lines
12 KiB
C++
309 lines
12 KiB
C++
//======- BufferViewFlowAnalysis.cpp - Buffer alias analysis -*- C++ -*-======//
|
|
//
|
|
// 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "mlir/Dialect/Bufferization/Transforms/BufferViewFlowAnalysis.h"
|
|
|
|
#include "mlir/Dialect/Bufferization/IR/BufferViewFlowOpInterface.h"
|
|
#include "mlir/Interfaces/CallInterfaces.h"
|
|
#include "mlir/Interfaces/ControlFlowInterfaces.h"
|
|
#include "mlir/Interfaces/FunctionInterfaces.h"
|
|
#include "mlir/Interfaces/ViewLikeInterface.h"
|
|
#include "llvm/ADT/SetOperations.h"
|
|
|
|
using namespace mlir;
|
|
using namespace mlir::bufferization;
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// BufferViewFlowAnalysis
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
/// Constructs a new alias analysis using the op provided.
|
|
BufferViewFlowAnalysis::BufferViewFlowAnalysis(Operation *op) { build(op); }
|
|
|
|
static BufferViewFlowAnalysis::ValueSetT
|
|
resolveValues(const BufferViewFlowAnalysis::ValueMapT &map, Value value) {
|
|
BufferViewFlowAnalysis::ValueSetT result;
|
|
SmallVector<Value, 8> queue;
|
|
queue.push_back(value);
|
|
while (!queue.empty()) {
|
|
Value currentValue = queue.pop_back_val();
|
|
if (result.insert(currentValue).second) {
|
|
auto it = map.find(currentValue);
|
|
if (it != map.end()) {
|
|
for (Value aliasValue : it->second)
|
|
queue.push_back(aliasValue);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Find all immediate and indirect dependent buffers this value could
|
|
/// potentially have. Note that the resulting set will also contain the value
|
|
/// provided as it is a dependent alias of itself.
|
|
BufferViewFlowAnalysis::ValueSetT
|
|
BufferViewFlowAnalysis::resolve(Value rootValue) const {
|
|
return resolveValues(dependencies, rootValue);
|
|
}
|
|
|
|
BufferViewFlowAnalysis::ValueSetT
|
|
BufferViewFlowAnalysis::resolveReverse(Value rootValue) const {
|
|
return resolveValues(reverseDependencies, rootValue);
|
|
}
|
|
|
|
/// Removes the given values from all alias sets.
|
|
void BufferViewFlowAnalysis::remove(const SetVector<Value> &aliasValues) {
|
|
for (auto &entry : dependencies)
|
|
llvm::set_subtract(entry.second, aliasValues);
|
|
}
|
|
|
|
void BufferViewFlowAnalysis::rename(Value from, Value to) {
|
|
dependencies[to] = dependencies[from];
|
|
dependencies.erase(from);
|
|
|
|
for (auto &[_, value] : dependencies) {
|
|
if (value.contains(from)) {
|
|
value.insert(to);
|
|
value.erase(from);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This function constructs a mapping from values to its immediate
|
|
/// dependencies. 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. Furthermore, it wires
|
|
/// successor regions and branch-like return operations from nested regions.
|
|
void BufferViewFlowAnalysis::build(Operation *op) {
|
|
// Registers all dependencies of the given values.
|
|
auto registerDependencies = [&](ValueRange values, ValueRange dependencies) {
|
|
for (auto [value, dep] : llvm::zip_equal(values, dependencies)) {
|
|
this->dependencies[value].insert(dep);
|
|
this->reverseDependencies[dep].insert(value);
|
|
}
|
|
};
|
|
|
|
// Mark all buffer results and buffer region entry block arguments of the
|
|
// given op as terminals.
|
|
auto populateTerminalValues = [&](Operation *op) {
|
|
for (Value v : op->getResults())
|
|
if (isa<BaseMemRefType>(v.getType()))
|
|
this->terminals.insert(v);
|
|
for (Region &r : op->getRegions())
|
|
for (BlockArgument v : r.getArguments())
|
|
if (isa<BaseMemRefType>(v.getType()))
|
|
this->terminals.insert(v);
|
|
};
|
|
|
|
op->walk([&](Operation *op) {
|
|
// Query BufferViewFlowOpInterface. If the op does not implement that
|
|
// interface, try to infer the dependencies from other interfaces that the
|
|
// op may implement.
|
|
if (auto bufferViewFlowOp = dyn_cast<BufferViewFlowOpInterface>(op)) {
|
|
bufferViewFlowOp.populateDependencies(registerDependencies);
|
|
for (Value v : op->getResults())
|
|
if (isa<BaseMemRefType>(v.getType()) &&
|
|
bufferViewFlowOp.mayBeTerminalBuffer(v))
|
|
this->terminals.insert(v);
|
|
for (Region &r : op->getRegions())
|
|
for (BlockArgument v : r.getArguments())
|
|
if (isa<BaseMemRefType>(v.getType()) &&
|
|
bufferViewFlowOp.mayBeTerminalBuffer(v))
|
|
this->terminals.insert(v);
|
|
return WalkResult::advance();
|
|
}
|
|
|
|
// Add additional dependencies created by view changes to the alias list.
|
|
if (auto viewInterface = dyn_cast<ViewLikeOpInterface>(op)) {
|
|
registerDependencies(viewInterface.getViewSource(),
|
|
viewInterface.getViewDest());
|
|
return WalkResult::advance();
|
|
}
|
|
|
|
if (auto branchInterface = dyn_cast<BranchOpInterface>(op)) {
|
|
// Query all branch interfaces to link block argument dependencies.
|
|
Block *parentBlock = branchInterface->getBlock();
|
|
for (auto it = parentBlock->succ_begin(), e = parentBlock->succ_end();
|
|
it != e; ++it) {
|
|
// Query the branch op interface to get the successor operands.
|
|
auto successorOperands =
|
|
branchInterface.getSuccessorOperands(it.getIndex());
|
|
// Build the actual mapping of values to their immediate dependencies.
|
|
registerDependencies(successorOperands.getForwardedOperands(),
|
|
(*it)->getArguments().drop_front(
|
|
successorOperands.getProducedOperandCount()));
|
|
}
|
|
return WalkResult::advance();
|
|
}
|
|
|
|
if (auto regionInterface = dyn_cast<RegionBranchOpInterface>(op)) {
|
|
// Wire the successor operands with the successor inputs.
|
|
DenseMap<OpOperand *, SmallVector<Value>> mapping;
|
|
regionInterface.getSuccessorOperandInputMapping(mapping);
|
|
for (const auto &[operand, inputs] : mapping)
|
|
for (Value input : inputs)
|
|
registerDependencies({operand->get()}, {input});
|
|
return WalkResult::advance();
|
|
}
|
|
|
|
// Region terminators are handled together with RegionBranchOpInterface.
|
|
if (isa<RegionBranchTerminatorOpInterface>(op))
|
|
return WalkResult::advance();
|
|
|
|
if (isa<CallOpInterface>(op)) {
|
|
// This is an intra-function analysis. We have no information about other
|
|
// functions. Conservatively assume that each operand may alias with each
|
|
// result. Also mark the results are terminals because the function could
|
|
// return newly allocated buffers.
|
|
populateTerminalValues(op);
|
|
for (Value operand : op->getOperands())
|
|
for (Value result : op->getResults())
|
|
registerDependencies({operand}, {result});
|
|
return WalkResult::advance();
|
|
}
|
|
|
|
// We have no information about unknown ops.
|
|
populateTerminalValues(op);
|
|
|
|
return WalkResult::advance();
|
|
});
|
|
}
|
|
|
|
bool BufferViewFlowAnalysis::mayBeTerminalBuffer(Value value) const {
|
|
assert(isa<BaseMemRefType>(value.getType()) && "expected memref");
|
|
return terminals.contains(value);
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// BufferOriginAnalysis
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
/// Return "true" if the given value is the result of a memory allocation.
|
|
static bool hasAllocateSideEffect(Value v) {
|
|
Operation *op = v.getDefiningOp();
|
|
if (!op)
|
|
return false;
|
|
return hasEffect<MemoryEffects::Allocate>(op, v);
|
|
}
|
|
|
|
/// Return "true" if the given value is a function block argument.
|
|
static bool isFunctionArgument(Value v) {
|
|
auto bbArg = dyn_cast<BlockArgument>(v);
|
|
if (!bbArg)
|
|
return false;
|
|
Block *b = bbArg.getOwner();
|
|
auto funcOp = dyn_cast<FunctionOpInterface>(b->getParentOp());
|
|
if (!funcOp)
|
|
return false;
|
|
return bbArg.getOwner() == &funcOp.getFunctionBody().front();
|
|
}
|
|
|
|
/// Given a memref value, return the "base" value by skipping over all
|
|
/// ViewLikeOpInterface ops (if any) in the reverse use-def chain.
|
|
static Value getViewBase(Value value) {
|
|
while (auto viewLikeOp = value.getDefiningOp<ViewLikeOpInterface>()) {
|
|
if (value != viewLikeOp.getViewDest()) {
|
|
break;
|
|
}
|
|
value = viewLikeOp.getViewSource();
|
|
}
|
|
return value;
|
|
}
|
|
|
|
BufferOriginAnalysis::BufferOriginAnalysis(Operation *op) : analysis(op) {}
|
|
|
|
std::optional<bool> BufferOriginAnalysis::isSameAllocation(Value v1, Value v2) {
|
|
assert(isa<BaseMemRefType>(v1.getType()) && "expected buffer");
|
|
assert(isa<BaseMemRefType>(v2.getType()) && "expected buffer");
|
|
|
|
// Skip over all view-like ops.
|
|
v1 = getViewBase(v1);
|
|
v2 = getViewBase(v2);
|
|
|
|
// Fast path: If both buffers are the same SSA value, we can be sure that
|
|
// they originate from the same allocation.
|
|
if (v1 == v2)
|
|
return true;
|
|
|
|
// Compute the SSA values from which the buffers `v1` and `v2` originate.
|
|
SmallPtrSet<Value, 16> origin1 = analysis.resolveReverse(v1);
|
|
SmallPtrSet<Value, 16> origin2 = analysis.resolveReverse(v2);
|
|
|
|
// Originating buffers are "terminal" if they could not be traced back any
|
|
// further by the `BufferViewFlowAnalysis`. Examples of terminal buffers:
|
|
// - function block arguments
|
|
// - values defined by allocation ops such as "memref.alloc"
|
|
// - values defined by ops that are unknown to the buffer view flow analysis
|
|
// - values that are marked as "terminal" in the `BufferViewFlowOpInterface`
|
|
SmallPtrSet<Value, 16> terminal1, terminal2;
|
|
|
|
// While gathering terminal buffers, keep track of whether all terminal
|
|
// buffers are newly allocated buffer or function entry arguments.
|
|
bool allAllocs1 = true, allAllocs2 = true;
|
|
bool allAllocsOrFuncEntryArgs1 = true, allAllocsOrFuncEntryArgs2 = true;
|
|
|
|
// Helper function that gathers terminal buffers among `origin`.
|
|
auto gatherTerminalBuffers = [this](const SmallPtrSet<Value, 16> &origin,
|
|
SmallPtrSet<Value, 16> &terminal,
|
|
bool &allAllocs,
|
|
bool &allAllocsOrFuncEntryArgs) {
|
|
for (Value v : origin) {
|
|
if (isa<BaseMemRefType>(v.getType()) && analysis.mayBeTerminalBuffer(v)) {
|
|
terminal.insert(v);
|
|
allAllocs &= hasAllocateSideEffect(v);
|
|
allAllocsOrFuncEntryArgs &=
|
|
isFunctionArgument(v) || hasAllocateSideEffect(v);
|
|
}
|
|
}
|
|
assert(!terminal.empty() && "expected non-empty terminal set");
|
|
};
|
|
|
|
// Gather terminal buffers for `v1` and `v2`.
|
|
gatherTerminalBuffers(origin1, terminal1, allAllocs1,
|
|
allAllocsOrFuncEntryArgs1);
|
|
gatherTerminalBuffers(origin2, terminal2, allAllocs2,
|
|
allAllocsOrFuncEntryArgs2);
|
|
|
|
// If both `v1` and `v2` have a single matching terminal buffer, they are
|
|
// guaranteed to originate from the same buffer allocation.
|
|
if (llvm::hasSingleElement(terminal1) && llvm::hasSingleElement(terminal2) &&
|
|
*terminal1.begin() == *terminal2.begin())
|
|
return true;
|
|
|
|
// At least one of the two values has multiple terminals.
|
|
|
|
// Check if there is overlap between the terminal buffers of `v1` and `v2`.
|
|
bool distinctTerminalSets = true;
|
|
for (Value v : terminal1)
|
|
distinctTerminalSets &= !terminal2.contains(v);
|
|
// If there is overlap between the terminal buffers of `v1` and `v2`, we
|
|
// cannot make an accurate decision without further analysis.
|
|
if (!distinctTerminalSets)
|
|
return std::nullopt;
|
|
|
|
// If `v1` originates from only allocs, and `v2` is guaranteed to originate
|
|
// from different allocations (that is guaranteed if `v2` originates from
|
|
// only distinct allocs or function entry arguments), we can be sure that
|
|
// `v1` and `v2` originate from different allocations. The same argument can
|
|
// be made when swapping `v1` and `v2`.
|
|
bool isolatedAlloc1 = allAllocs1 && (allAllocs2 || allAllocsOrFuncEntryArgs2);
|
|
bool isolatedAlloc2 = (allAllocs1 || allAllocsOrFuncEntryArgs1) && allAllocs2;
|
|
if (isolatedAlloc1 || isolatedAlloc2)
|
|
return false;
|
|
|
|
// Otherwise: We do not know whether `v1` and `v2` originate from the same
|
|
// allocation or not.
|
|
// TODO: Function arguments are currently handled conservatively. We assume
|
|
// that they could be the same allocation.
|
|
// TODO: Terminals other than allocations and function arguments are
|
|
// currently handled conservatively. We assume that they could be the same
|
|
// allocation. E.g., we currently return "nullopt" for values that originate
|
|
// from different "memref.get_global" ops (with different symbols).
|
|
return std::nullopt;
|
|
}
|