426 lines
16 KiB
C++
426 lines
16 KiB
C++
//===- DeadCodeAnalysis.cpp - Dead code analysis --------------------------===//
|
|
//
|
|
// 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/Analysis/DataFlow/DeadCodeAnalysis.h"
|
|
#include "mlir/Analysis/DataFlow/ConstantPropagationAnalysis.h"
|
|
#include "mlir/Interfaces/CallInterfaces.h"
|
|
#include "mlir/Interfaces/ControlFlowInterfaces.h"
|
|
|
|
using namespace mlir;
|
|
using namespace mlir::dataflow;
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// Executable
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
ChangeResult Executable::setToLive() {
|
|
if (live)
|
|
return ChangeResult::NoChange;
|
|
live = true;
|
|
return ChangeResult::Change;
|
|
}
|
|
|
|
void Executable::print(raw_ostream &os) const {
|
|
os << (live ? "live" : "dead");
|
|
}
|
|
|
|
void Executable::onUpdate(DataFlowSolver *solver) const {
|
|
if (auto *block = point.dyn_cast<Block *>()) {
|
|
// Re-invoke the analyses on the block itself.
|
|
for (DataFlowAnalysis *analysis : subscribers)
|
|
solver->enqueue({block, analysis});
|
|
// Re-invoke the analyses on all operations in the block.
|
|
for (DataFlowAnalysis *analysis : subscribers)
|
|
for (Operation &op : *block)
|
|
solver->enqueue({&op, analysis});
|
|
} else if (auto *programPoint = point.dyn_cast<GenericProgramPoint *>()) {
|
|
// Re-invoke the analysis on the successor block.
|
|
if (auto *edge = dyn_cast<CFGEdge>(programPoint)) {
|
|
for (DataFlowAnalysis *analysis : subscribers)
|
|
solver->enqueue({edge->getTo(), analysis});
|
|
}
|
|
}
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// PredecessorState
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
void PredecessorState::print(raw_ostream &os) const {
|
|
if (allPredecessorsKnown())
|
|
os << "(all) ";
|
|
os << "predecessors:\n";
|
|
for (Operation *op : getKnownPredecessors())
|
|
os << " " << *op << "\n";
|
|
}
|
|
|
|
ChangeResult PredecessorState::join(Operation *predecessor) {
|
|
return knownPredecessors.insert(predecessor) ? ChangeResult::Change
|
|
: ChangeResult::NoChange;
|
|
}
|
|
|
|
ChangeResult PredecessorState::join(Operation *predecessor, ValueRange inputs) {
|
|
ChangeResult result = join(predecessor);
|
|
if (!inputs.empty()) {
|
|
ValueRange &curInputs = successorInputs[predecessor];
|
|
if (curInputs != inputs) {
|
|
curInputs = inputs;
|
|
result |= ChangeResult::Change;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// CFGEdge
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
Location CFGEdge::getLoc() const {
|
|
return FusedLoc::get(
|
|
getFrom()->getParent()->getContext(),
|
|
{getFrom()->getParent()->getLoc(), getTo()->getParent()->getLoc()});
|
|
}
|
|
|
|
void CFGEdge::print(raw_ostream &os) const {
|
|
getFrom()->print(os);
|
|
os << "\n -> \n";
|
|
getTo()->print(os);
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// DeadCodeAnalysis
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
DeadCodeAnalysis::DeadCodeAnalysis(DataFlowSolver &solver)
|
|
: DataFlowAnalysis(solver) {
|
|
registerPointKind<CFGEdge>();
|
|
}
|
|
|
|
LogicalResult DeadCodeAnalysis::initialize(Operation *top) {
|
|
// Mark the top-level blocks as executable.
|
|
for (Region ®ion : top->getRegions()) {
|
|
if (region.empty())
|
|
continue;
|
|
auto *state = getOrCreate<Executable>(®ion.front());
|
|
propagateIfChanged(state, state->setToLive());
|
|
}
|
|
|
|
// Mark as overdefined the predecessors of symbol callables with potentially
|
|
// unknown predecessors.
|
|
initializeSymbolCallables(top);
|
|
|
|
return initializeRecursively(top);
|
|
}
|
|
|
|
void DeadCodeAnalysis::initializeSymbolCallables(Operation *top) {
|
|
analysisScope = top;
|
|
auto walkFn = [&](Operation *symTable, bool allUsesVisible) {
|
|
Region &symbolTableRegion = symTable->getRegion(0);
|
|
Block *symbolTableBlock = &symbolTableRegion.front();
|
|
|
|
bool foundSymbolCallable = false;
|
|
for (auto callable : symbolTableBlock->getOps<CallableOpInterface>()) {
|
|
Region *callableRegion = callable.getCallableRegion();
|
|
if (!callableRegion)
|
|
continue;
|
|
auto symbol = dyn_cast<SymbolOpInterface>(callable.getOperation());
|
|
if (!symbol)
|
|
continue;
|
|
|
|
// Public symbol callables or those for which we can't see all uses have
|
|
// potentially unknown callsites.
|
|
if (symbol.isPublic() || (!allUsesVisible && symbol.isNested())) {
|
|
auto *state = getOrCreate<PredecessorState>(callable);
|
|
propagateIfChanged(state, state->setHasUnknownPredecessors());
|
|
}
|
|
foundSymbolCallable = true;
|
|
}
|
|
|
|
// Exit early if no eligible symbol callables were found in the table.
|
|
if (!foundSymbolCallable)
|
|
return;
|
|
|
|
// Walk the symbol table to check for non-call uses of symbols.
|
|
Optional<SymbolTable::UseRange> uses =
|
|
SymbolTable::getSymbolUses(&symbolTableRegion);
|
|
if (!uses) {
|
|
// If we couldn't gather the symbol uses, conservatively assume that
|
|
// we can't track information for any nested symbols.
|
|
return top->walk([&](CallableOpInterface callable) {
|
|
auto *state = getOrCreate<PredecessorState>(callable);
|
|
propagateIfChanged(state, state->setHasUnknownPredecessors());
|
|
});
|
|
}
|
|
|
|
for (const SymbolTable::SymbolUse &use : *uses) {
|
|
if (isa<CallOpInterface>(use.getUser()))
|
|
continue;
|
|
// If a callable symbol has a non-call use, then we can't be guaranteed to
|
|
// know all callsites.
|
|
Operation *symbol = symbolTable.lookupSymbolIn(top, use.getSymbolRef());
|
|
auto *state = getOrCreate<PredecessorState>(symbol);
|
|
propagateIfChanged(state, state->setHasUnknownPredecessors());
|
|
}
|
|
};
|
|
SymbolTable::walkSymbolTables(top, /*allSymUsesVisible=*/!top->getBlock(),
|
|
walkFn);
|
|
}
|
|
|
|
/// Returns true if the operation is a returning terminator in region
|
|
/// control-flow or the terminator of a callable region.
|
|
static bool isRegionOrCallableReturn(Operation *op) {
|
|
return !op->getNumSuccessors() &&
|
|
isa<RegionBranchOpInterface, CallableOpInterface>(op->getParentOp()) &&
|
|
op->getBlock()->getTerminator() == op;
|
|
}
|
|
|
|
LogicalResult DeadCodeAnalysis::initializeRecursively(Operation *op) {
|
|
// Initialize the analysis by visiting every op with control-flow semantics.
|
|
if (op->getNumRegions() || op->getNumSuccessors() ||
|
|
isRegionOrCallableReturn(op) || isa<CallOpInterface>(op)) {
|
|
// When the liveness of the parent block changes, make sure to re-invoke the
|
|
// analysis on the op.
|
|
if (op->getBlock())
|
|
getOrCreate<Executable>(op->getBlock())->blockContentSubscribe(this);
|
|
// Visit the op.
|
|
if (failed(visit(op)))
|
|
return failure();
|
|
}
|
|
// Recurse on nested operations.
|
|
for (Region ®ion : op->getRegions())
|
|
for (Operation &op : region.getOps())
|
|
if (failed(initializeRecursively(&op)))
|
|
return failure();
|
|
return success();
|
|
}
|
|
|
|
void DeadCodeAnalysis::markEdgeLive(Block *from, Block *to) {
|
|
auto *state = getOrCreate<Executable>(to);
|
|
propagateIfChanged(state, state->setToLive());
|
|
auto *edgeState = getOrCreate<Executable>(getProgramPoint<CFGEdge>(from, to));
|
|
propagateIfChanged(edgeState, edgeState->setToLive());
|
|
}
|
|
|
|
void DeadCodeAnalysis::markEntryBlocksLive(Operation *op) {
|
|
for (Region ®ion : op->getRegions()) {
|
|
if (region.empty())
|
|
continue;
|
|
auto *state = getOrCreate<Executable>(®ion.front());
|
|
propagateIfChanged(state, state->setToLive());
|
|
}
|
|
}
|
|
|
|
LogicalResult DeadCodeAnalysis::visit(ProgramPoint point) {
|
|
if (point.is<Block *>())
|
|
return success();
|
|
auto *op = point.dyn_cast<Operation *>();
|
|
if (!op)
|
|
return emitError(point.getLoc(), "unknown program point kind");
|
|
|
|
// If the parent block is not executable, there is nothing to do.
|
|
if (!getOrCreate<Executable>(op->getBlock())->isLive())
|
|
return success();
|
|
|
|
// We have a live call op. Add this as a live predecessor of the callee.
|
|
if (auto call = dyn_cast<CallOpInterface>(op))
|
|
visitCallOperation(call);
|
|
|
|
// Visit the regions.
|
|
if (op->getNumRegions()) {
|
|
// Check if we can reason about the region control-flow.
|
|
if (auto branch = dyn_cast<RegionBranchOpInterface>(op)) {
|
|
visitRegionBranchOperation(branch);
|
|
|
|
// Check if this is a callable operation.
|
|
} else if (auto callable = dyn_cast<CallableOpInterface>(op)) {
|
|
const auto *callsites = getOrCreateFor<PredecessorState>(op, callable);
|
|
|
|
// If the callsites could not be resolved or are known to be non-empty,
|
|
// mark the callable as executable.
|
|
if (!callsites->allPredecessorsKnown() ||
|
|
!callsites->getKnownPredecessors().empty())
|
|
markEntryBlocksLive(callable);
|
|
|
|
// Otherwise, conservatively mark all entry blocks as executable.
|
|
} else {
|
|
markEntryBlocksLive(op);
|
|
}
|
|
}
|
|
|
|
if (isRegionOrCallableReturn(op)) {
|
|
if (auto branch = dyn_cast<RegionBranchOpInterface>(op->getParentOp())) {
|
|
// Visit the exiting terminator of a region.
|
|
visitRegionTerminator(op, branch);
|
|
} else if (auto callable =
|
|
dyn_cast<CallableOpInterface>(op->getParentOp())) {
|
|
// Visit the exiting terminator of a callable.
|
|
visitCallableTerminator(op, callable);
|
|
}
|
|
}
|
|
// Visit the successors.
|
|
if (op->getNumSuccessors()) {
|
|
// Check if we can reason about the control-flow.
|
|
if (auto branch = dyn_cast<BranchOpInterface>(op)) {
|
|
visitBranchOperation(branch);
|
|
|
|
// Otherwise, conservatively mark all successors as exectuable.
|
|
} else {
|
|
for (Block *successor : op->getSuccessors())
|
|
markEdgeLive(op->getBlock(), successor);
|
|
}
|
|
}
|
|
|
|
return success();
|
|
}
|
|
|
|
void DeadCodeAnalysis::visitCallOperation(CallOpInterface call) {
|
|
Operation *callableOp = call.resolveCallable(&symbolTable);
|
|
|
|
// A call to a externally-defined callable has unknown predecessors.
|
|
const auto isExternalCallable = [this](Operation *op) {
|
|
// A callable outside the analysis scope is an external callable.
|
|
if (!analysisScope->isAncestor(op))
|
|
return true;
|
|
// Otherwise, check if the callable region is defined.
|
|
if (auto callable = dyn_cast<CallableOpInterface>(op))
|
|
return !callable.getCallableRegion();
|
|
return false;
|
|
};
|
|
|
|
// TODO: Add support for non-symbol callables when necessary. If the
|
|
// callable has non-call uses we would mark as having reached pessimistic
|
|
// fixpoint, otherwise allow for propagating the return values out.
|
|
if (isa_and_nonnull<SymbolOpInterface>(callableOp) &&
|
|
!isExternalCallable(callableOp)) {
|
|
// Add the live callsite.
|
|
auto *callsites = getOrCreate<PredecessorState>(callableOp);
|
|
propagateIfChanged(callsites, callsites->join(call));
|
|
} else {
|
|
// Mark this call op's predecessors as overdefined.
|
|
auto *predecessors = getOrCreate<PredecessorState>(call);
|
|
propagateIfChanged(predecessors, predecessors->setHasUnknownPredecessors());
|
|
}
|
|
}
|
|
|
|
/// Get the constant values of the operands of an operation. If any of the
|
|
/// constant value lattices are uninitialized, return none to indicate the
|
|
/// analysis should bail out.
|
|
static Optional<SmallVector<Attribute>> getOperandValuesImpl(
|
|
Operation *op,
|
|
function_ref<const Lattice<ConstantValue> *(Value)> getLattice) {
|
|
SmallVector<Attribute> operands;
|
|
operands.reserve(op->getNumOperands());
|
|
for (Value operand : op->getOperands()) {
|
|
const Lattice<ConstantValue> *cv = getLattice(operand);
|
|
// If any of the operands' values are uninitialized, bail out.
|
|
if (cv->getValue().isUninitialized())
|
|
return {};
|
|
operands.push_back(cv->getValue().getConstantValue());
|
|
}
|
|
return operands;
|
|
}
|
|
|
|
Optional<SmallVector<Attribute>>
|
|
DeadCodeAnalysis::getOperandValues(Operation *op) {
|
|
return getOperandValuesImpl(op, [&](Value value) {
|
|
auto *lattice = getOrCreate<Lattice<ConstantValue>>(value);
|
|
lattice->useDefSubscribe(this);
|
|
return lattice;
|
|
});
|
|
}
|
|
|
|
void DeadCodeAnalysis::visitBranchOperation(BranchOpInterface branch) {
|
|
// Try to deduce a single successor for the branch.
|
|
Optional<SmallVector<Attribute>> operands = getOperandValues(branch);
|
|
if (!operands)
|
|
return;
|
|
|
|
if (Block *successor = branch.getSuccessorForOperands(*operands)) {
|
|
markEdgeLive(branch->getBlock(), successor);
|
|
} else {
|
|
// Otherwise, mark all successors as executable and outgoing edges.
|
|
for (Block *successor : branch->getSuccessors())
|
|
markEdgeLive(branch->getBlock(), successor);
|
|
}
|
|
}
|
|
|
|
void DeadCodeAnalysis::visitRegionBranchOperation(
|
|
RegionBranchOpInterface branch) {
|
|
// Try to deduce which regions are executable.
|
|
Optional<SmallVector<Attribute>> operands = getOperandValues(branch);
|
|
if (!operands)
|
|
return;
|
|
|
|
SmallVector<RegionSuccessor> successors;
|
|
branch.getSuccessorRegions(/*index=*/{}, *operands, successors);
|
|
for (const RegionSuccessor &successor : successors) {
|
|
// The successor can be either an entry block or the parent operation.
|
|
ProgramPoint point = successor.getSuccessor()
|
|
? &successor.getSuccessor()->front()
|
|
: ProgramPoint(branch);
|
|
// Mark the entry block as executable.
|
|
auto *state = getOrCreate<Executable>(point);
|
|
propagateIfChanged(state, state->setToLive());
|
|
// Add the parent op as a predecessor.
|
|
auto *predecessors = getOrCreate<PredecessorState>(point);
|
|
propagateIfChanged(
|
|
predecessors,
|
|
predecessors->join(branch, successor.getSuccessorInputs()));
|
|
}
|
|
}
|
|
|
|
void DeadCodeAnalysis::visitRegionTerminator(Operation *op,
|
|
RegionBranchOpInterface branch) {
|
|
Optional<SmallVector<Attribute>> operands = getOperandValues(op);
|
|
if (!operands)
|
|
return;
|
|
|
|
SmallVector<RegionSuccessor> successors;
|
|
branch.getSuccessorRegions(op->getParentRegion()->getRegionNumber(),
|
|
*operands, successors);
|
|
|
|
// Mark successor region entry blocks as executable and add this op to the
|
|
// list of predecessors.
|
|
for (const RegionSuccessor &successor : successors) {
|
|
PredecessorState *predecessors;
|
|
if (Region *region = successor.getSuccessor()) {
|
|
auto *state = getOrCreate<Executable>(®ion->front());
|
|
propagateIfChanged(state, state->setToLive());
|
|
predecessors = getOrCreate<PredecessorState>(®ion->front());
|
|
} else {
|
|
// Add this terminator as a predecessor to the parent op.
|
|
predecessors = getOrCreate<PredecessorState>(branch);
|
|
}
|
|
propagateIfChanged(predecessors,
|
|
predecessors->join(op, successor.getSuccessorInputs()));
|
|
}
|
|
}
|
|
|
|
void DeadCodeAnalysis::visitCallableTerminator(Operation *op,
|
|
CallableOpInterface callable) {
|
|
// If there are no exiting values, we have nothing to do.
|
|
if (op->getNumOperands() == 0)
|
|
return;
|
|
|
|
// Add as predecessors to all callsites this return op.
|
|
auto *callsites = getOrCreateFor<PredecessorState>(op, callable);
|
|
bool canResolve = op->hasTrait<OpTrait::ReturnLike>();
|
|
for (Operation *predecessor : callsites->getKnownPredecessors()) {
|
|
assert(isa<CallOpInterface>(predecessor));
|
|
auto *predecessors = getOrCreate<PredecessorState>(predecessor);
|
|
if (canResolve) {
|
|
propagateIfChanged(predecessors, predecessors->join(op));
|
|
} else {
|
|
// If the terminator is not a return-like, then conservatively assume we
|
|
// can't resolve the predecessor.
|
|
propagateIfChanged(predecessors,
|
|
predecessors->setHasUnknownPredecessors());
|
|
}
|
|
}
|
|
}
|