//===- 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/Analysis/DataFlow/SparseAnalysis.h" #include "mlir/Analysis/DataFlowFramework.h" #include "mlir/IR/Attributes.h" #include "mlir/IR/Block.h" #include "mlir/IR/Diagnostics.h" #include "mlir/IR/Location.h" #include "mlir/IR/Operation.h" #include "mlir/IR/SymbolTable.h" #include "mlir/IR/Value.h" #include "mlir/IR/ValueRange.h" #include "mlir/Interfaces/CallInterfaces.h" #include "mlir/Interfaces/ControlFlowInterfaces.h" #include "mlir/Support/LLVM.h" #include "llvm/Support/Casting.h" #include #include 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 { AnalysisState::onUpdate(solver); if (ProgramPoint *pp = llvm::dyn_cast_if_present(anchor)) { if (pp->isBlockStart()) { // Re-invoke the analyses on the block itself. for (DataFlowAnalysis *analysis : subscribers) solver->enqueue({pp, analysis}); // Re-invoke the analyses on all operations in the block. for (DataFlowAnalysis *analysis : subscribers) for (Operation &op : *pp->getBlock()) solver->enqueue({solver->getProgramPointAfter(&op), analysis}); } } else if (auto *latticeAnchor = llvm::dyn_cast_if_present(anchor)) { // Re-invoke the analysis on the successor block. if (auto *edge = dyn_cast(latticeAnchor)) { for (DataFlowAnalysis *analysis : subscribers) solver->enqueue( {solver->getProgramPointBefore(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) { registerAnchorKind(); } LogicalResult DeadCodeAnalysis::initialize(Operation *top) { // Mark the top-level blocks as executable. for (Region ®ion : top->getRegions()) { if (region.empty()) continue; auto *state = getOrCreate(getProgramPointBefore(®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()) { Region *callableRegion = callable.getCallableRegion(); if (!callableRegion) continue; auto symbol = dyn_cast(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(getProgramPointAfter(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. std::optional 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(getProgramPointAfter(callable)); propagateIfChanged(state, state->setHasUnknownPredecessors()); }); } for (const SymbolTable::SymbolUse &use : *uses) { if (isa(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()); if (!symbol) continue; auto *state = getOrCreate(getProgramPointAfter(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->getBlock() != nullptr && !op->getNumSuccessors() && isa(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(op)) { // When the liveness of the parent block changes, make sure to re-invoke the // analysis on the op. if (op->getBlock()) getOrCreate(getProgramPointBefore(op->getBlock())) ->blockContentSubscribe(this); // Visit the op. if (failed(visit(getProgramPointAfter(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(getProgramPointBefore(to)); propagateIfChanged(state, state->setToLive()); auto *edgeState = getOrCreate(getLatticeAnchor(from, to)); propagateIfChanged(edgeState, edgeState->setToLive()); } void DeadCodeAnalysis::markEntryBlocksLive(Operation *op) { for (Region ®ion : op->getRegions()) { if (region.empty()) continue; auto *state = getOrCreate(getProgramPointBefore(®ion.front())); propagateIfChanged(state, state->setToLive()); } } LogicalResult DeadCodeAnalysis::visit(ProgramPoint *point) { if (point->isBlockStart()) return success(); Operation *op = point->getPrevOp(); // If the parent block is not executable, there is nothing to do. if (op->getBlock() != nullptr && !getOrCreate(getProgramPointBefore(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(op)) visitCallOperation(call); // Visit the regions. if (op->getNumRegions()) { // Check if we can reason about the region control-flow. if (auto branch = dyn_cast(op)) { visitRegionBranchOperation(branch); // Check if this is a callable operation. } else if (auto callable = dyn_cast(op)) { const auto *callsites = getOrCreateFor( getProgramPointAfter(op), getProgramPointAfter(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(op->getParentOp())) { // Visit the exiting terminator of a region. visitRegionTerminator(op, branch); } else if (auto callable = dyn_cast(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(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.resolveCallableInTable(&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(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(callableOp) && !isExternalCallable(callableOp)) { // Add the live callsite. auto *callsites = getOrCreate(getProgramPointAfter(callableOp)); propagateIfChanged(callsites, callsites->join(call)); } else { // Mark this call op's predecessors as overdefined. auto *predecessors = getOrCreate(getProgramPointAfter(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 std::nullopt to indicate /// the analysis should bail out. static std::optional> getOperandValuesImpl( Operation *op, function_ref *(Value)> getLattice) { SmallVector operands; operands.reserve(op->getNumOperands()); for (Value operand : op->getOperands()) { const Lattice *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; } std::optional> DeadCodeAnalysis::getOperandValues(Operation *op) { return getOperandValuesImpl(op, [&](Value value) { auto *lattice = getOrCreate>(value); lattice->useDefSubscribe(this); return lattice; }); } void DeadCodeAnalysis::visitBranchOperation(BranchOpInterface branch) { // Try to deduce a single successor for the branch. std::optional> 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. std::optional> operands = getOperandValues(branch); if (!operands) return; SmallVector successors; branch.getEntrySuccessorRegions(*operands, successors); for (const RegionSuccessor &successor : successors) { // The successor can be either an entry block or the parent operation. ProgramPoint *point = successor.getSuccessor() ? getProgramPointBefore(&successor.getSuccessor()->front()) : getProgramPointAfter(branch); // Mark the entry block as executable. auto *state = getOrCreate(point); propagateIfChanged(state, state->setToLive()); // Add the parent op as a predecessor. auto *predecessors = getOrCreate(point); propagateIfChanged( predecessors, predecessors->join(branch, successor.getSuccessorInputs())); } } void DeadCodeAnalysis::visitRegionTerminator(Operation *op, RegionBranchOpInterface branch) { std::optional> operands = getOperandValues(op); if (!operands) return; SmallVector successors; if (auto terminator = dyn_cast(op)) terminator.getSuccessorRegions(*operands, successors); else branch.getSuccessorRegions(op->getParentRegion(), 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(getProgramPointBefore(®ion->front())); propagateIfChanged(state, state->setToLive()); predecessors = getOrCreate( getProgramPointBefore(®ion->front())); } else { // Add this terminator as a predecessor to the parent op. predecessors = getOrCreate(getProgramPointAfter(branch)); } propagateIfChanged(predecessors, predecessors->join(op, successor.getSuccessorInputs())); } } void DeadCodeAnalysis::visitCallableTerminator(Operation *op, CallableOpInterface callable) { // Add as predecessors to all callsites this return op. auto *callsites = getOrCreateFor( getProgramPointAfter(op), getProgramPointAfter(callable)); bool canResolve = op->hasTrait(); for (Operation *predecessor : callsites->getKnownPredecessors()) { assert(isa(predecessor)); auto *predecessors = getOrCreate(getProgramPointAfter(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()); } } }