This is still somehow a WIP, we have some issues with this interface that are not trivial to solve. This patch tries to make the concepts of RegionBranchPoint and RegionSuccessor more robust and aligned with their definition: - A `RegionBranchPoint` is either the parent (`RegionBranchOpInterface`) op or a `RegionBranchTerminatorOpInterface` operation in a nested region. - A `RegionSuccessor` is either one of the nested region or the parent `RegionBranchOpInterface` Some new methods with reasonnable default implementation are added to help resolving the flow of values across the RegionBranchOpInterface. It is still not trivial in the current state to walk the def-use chain backward with this interface. For example when you have the 3rd block argument in the entry block of a for-loop, finding the matching operands requires to know about the hidden loop iterator block argument and where the iterargs start. The API is designed around forward-tracking of the chain unfortunately. Try to reland #161575 ; I suspect a buildbot incremental build issue.
394 lines
16 KiB
C++
394 lines
16 KiB
C++
//===- TestDenseBackwardDataFlowAnalysis.cpp - Test pass ------------------===//
|
|
//
|
|
// 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
//
|
|
// Test pass for backward dense dataflow analysis.
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "TestDenseDataFlowAnalysis.h"
|
|
#include "TestOps.h"
|
|
#include "mlir/Analysis/DataFlow/DenseAnalysis.h"
|
|
#include "mlir/Analysis/DataFlow/Utils.h"
|
|
#include "mlir/Analysis/DataFlowFramework.h"
|
|
#include "mlir/IR/Builders.h"
|
|
#include "mlir/IR/SymbolTable.h"
|
|
#include "mlir/Interfaces/CallInterfaces.h"
|
|
#include "mlir/Interfaces/ControlFlowInterfaces.h"
|
|
#include "mlir/Interfaces/SideEffectInterfaces.h"
|
|
#include "mlir/Pass/Pass.h"
|
|
#include "mlir/Support/TypeID.h"
|
|
#include "llvm/Support/DebugLog.h"
|
|
#include "llvm/Support/raw_ostream.h"
|
|
|
|
using namespace mlir;
|
|
using namespace mlir::dataflow;
|
|
using namespace mlir::dataflow::test;
|
|
|
|
#define DEBUG_TYPE "test-next-access"
|
|
|
|
namespace {
|
|
|
|
class NextAccess : public AbstractDenseLattice, public AccessLatticeBase {
|
|
public:
|
|
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(NextAccess)
|
|
|
|
using dataflow::AbstractDenseLattice::AbstractDenseLattice;
|
|
|
|
ChangeResult meet(const AbstractDenseLattice &lattice) override {
|
|
return AccessLatticeBase::merge(static_cast<AccessLatticeBase>(
|
|
static_cast<const NextAccess &>(lattice)));
|
|
}
|
|
|
|
void print(raw_ostream &os) const override {
|
|
return AccessLatticeBase::print(os);
|
|
}
|
|
};
|
|
|
|
class NextAccessAnalysis : public DenseBackwardDataFlowAnalysis<NextAccess> {
|
|
public:
|
|
NextAccessAnalysis(DataFlowSolver &solver, SymbolTableCollection &symbolTable,
|
|
bool assumeFuncReads = false)
|
|
: DenseBackwardDataFlowAnalysis(solver, symbolTable),
|
|
assumeFuncReads(assumeFuncReads) {}
|
|
|
|
LogicalResult visitOperation(Operation *op, const NextAccess &after,
|
|
NextAccess *before) override;
|
|
|
|
void visitCallControlFlowTransfer(CallOpInterface call,
|
|
CallControlFlowAction action,
|
|
const NextAccess &after,
|
|
NextAccess *before) override;
|
|
|
|
void visitRegionBranchControlFlowTransfer(RegionBranchOpInterface branch,
|
|
RegionBranchPoint regionFrom,
|
|
RegionSuccessor regionTo,
|
|
const NextAccess &after,
|
|
NextAccess *before) override;
|
|
|
|
// TODO: this isn't ideal for the analysis. When there is no next access, it
|
|
// means "we don't know what the next access is" rather than "there is no next
|
|
// access". But it's unclear how to differentiate the two cases...
|
|
void setToExitState(NextAccess *lattice) override {
|
|
LDBG() << "setToExitState: setting lattice to unknown state";
|
|
propagateIfChanged(lattice, lattice->setKnownToUnknown());
|
|
}
|
|
|
|
/// Visit an operation. If this analysis can confirm that lattice content
|
|
/// of lattice anchors around operation are necessarily identical, join
|
|
/// them into the same equivalent class.
|
|
void buildOperationEquivalentLatticeAnchor(Operation *op) override;
|
|
|
|
const bool assumeFuncReads;
|
|
};
|
|
} // namespace
|
|
|
|
LogicalResult NextAccessAnalysis::visitOperation(Operation *op,
|
|
const NextAccess &after,
|
|
NextAccess *before) {
|
|
LDBG() << "visitOperation: "
|
|
<< OpWithFlags(op, OpPrintingFlags().skipRegions());
|
|
LDBG() << " after state: " << after;
|
|
LDBG() << " before state: " << *before;
|
|
|
|
auto memory = dyn_cast<MemoryEffectOpInterface>(op);
|
|
// If we can't reason about the memory effects, conservatively assume we can't
|
|
// say anything about the next access.
|
|
if (!memory) {
|
|
LDBG() << " No memory effect interface, setting to exit state";
|
|
setToExitState(before);
|
|
return success();
|
|
}
|
|
|
|
SmallVector<MemoryEffects::EffectInstance> effects;
|
|
memory.getEffects(effects);
|
|
LDBG() << " Found " << effects.size() << " memory effects";
|
|
|
|
// First, check if all underlying values are already known. Otherwise, avoid
|
|
// propagating and stay in the "undefined" state to avoid incorrectly
|
|
// propagating values that may be overwritten later on as that could be
|
|
// problematic for convergence based on monotonicity of lattice updates.
|
|
SmallVector<Value> underlyingValues;
|
|
underlyingValues.reserve(effects.size());
|
|
for (const MemoryEffects::EffectInstance &effect : effects) {
|
|
Value value = effect.getValue();
|
|
|
|
// Effects with unspecified value are treated conservatively and we cannot
|
|
// assume anything about the next access.
|
|
if (!value) {
|
|
LDBG() << " Effect has unspecified value, setting to exit state";
|
|
setToExitState(before);
|
|
return success();
|
|
}
|
|
|
|
// If cannot find the most underlying value, we cannot assume anything about
|
|
// the next accesses.
|
|
std::optional<Value> underlyingValue =
|
|
UnderlyingValueAnalysis::getMostUnderlyingValue(
|
|
value, [&](Value value) {
|
|
return getOrCreateFor<UnderlyingValueLattice>(
|
|
getProgramPointBefore(op), value);
|
|
});
|
|
|
|
// If the underlying value is not known yet, don't propagate.
|
|
if (!underlyingValue) {
|
|
LDBG() << " Underlying value not known for " << value
|
|
<< ", skipping propagation";
|
|
return success();
|
|
}
|
|
|
|
LDBG() << " Found underlying value " << *underlyingValue << " for "
|
|
<< value;
|
|
underlyingValues.push_back(*underlyingValue);
|
|
}
|
|
|
|
// Update the state if all underlying values are known.
|
|
LDBG() << " All underlying values known, updating state";
|
|
ChangeResult result = before->meet(after);
|
|
for (const auto &[effect, value] : llvm::zip(effects, underlyingValues)) {
|
|
// If the underlying value is known to be unknown, set to fixpoint.
|
|
if (!value) {
|
|
LDBG() << " Underlying value is unknown, setting to exit state";
|
|
setToExitState(before);
|
|
return success();
|
|
}
|
|
|
|
LDBG() << " Setting next access for value " << value << " to operation "
|
|
<< OpWithFlags(op, OpPrintingFlags().skipRegions());
|
|
result |= before->set(value, op);
|
|
}
|
|
LDBG() << " Final result: "
|
|
<< (result == ChangeResult::Change ? "changed" : "no change");
|
|
propagateIfChanged(before, result);
|
|
return success();
|
|
}
|
|
|
|
void NextAccessAnalysis::buildOperationEquivalentLatticeAnchor(Operation *op) {
|
|
LDBG() << "buildOperationEquivalentLatticeAnchor: "
|
|
<< OpWithFlags(op, OpPrintingFlags().skipRegions());
|
|
if (isMemoryEffectFree(op)) {
|
|
LDBG() << " Operation is memory effect free, unioning lattice anchors";
|
|
unionLatticeAnchors<NextAccess>(getProgramPointBefore(op),
|
|
getProgramPointAfter(op));
|
|
} else {
|
|
LDBG() << " Operation has memory effects, not unioning lattice anchors";
|
|
}
|
|
}
|
|
|
|
void NextAccessAnalysis::visitCallControlFlowTransfer(
|
|
CallOpInterface call, CallControlFlowAction action, const NextAccess &after,
|
|
NextAccess *before) {
|
|
LDBG() << "visitCallControlFlowTransfer: "
|
|
<< OpWithFlags(call.getOperation(), OpPrintingFlags().skipRegions());
|
|
LDBG() << " action: "
|
|
<< (action == CallControlFlowAction::ExternalCallee ? "ExternalCallee"
|
|
: action == CallControlFlowAction::EnterCallee ? "EnterCallee"
|
|
: "ExitCallee");
|
|
LDBG() << " assumeFuncReads: " << assumeFuncReads;
|
|
|
|
if (action == CallControlFlowAction::ExternalCallee && assumeFuncReads) {
|
|
LDBG() << " Handling external callee with assumed function reads";
|
|
SmallVector<Value> underlyingValues;
|
|
underlyingValues.reserve(call->getNumOperands());
|
|
for (Value operand : call.getArgOperands()) {
|
|
std::optional<Value> underlyingValue =
|
|
UnderlyingValueAnalysis::getMostUnderlyingValue(
|
|
operand, [&](Value value) {
|
|
return getOrCreateFor<UnderlyingValueLattice>(
|
|
getProgramPointBefore(call.getOperation()), value);
|
|
});
|
|
if (!underlyingValue) {
|
|
LDBG() << " Underlying value not known for operand " << operand
|
|
<< ", returning";
|
|
return;
|
|
}
|
|
LDBG() << " Found underlying value " << *underlyingValue
|
|
<< " for operand " << operand;
|
|
underlyingValues.push_back(*underlyingValue);
|
|
}
|
|
|
|
LDBG() << " Setting next access for " << underlyingValues.size()
|
|
<< " operands";
|
|
ChangeResult result = before->meet(after);
|
|
for (Value operand : underlyingValues) {
|
|
LDBG() << " Setting next access for operand " << operand << " to call "
|
|
<< call;
|
|
result |= before->set(operand, call);
|
|
}
|
|
LDBG() << " Call control flow result: "
|
|
<< (result == ChangeResult::Change ? "changed" : "no change");
|
|
return propagateIfChanged(before, result);
|
|
}
|
|
auto testCallAndStore =
|
|
dyn_cast<::test::TestCallAndStoreOp>(call.getOperation());
|
|
if (testCallAndStore && ((action == CallControlFlowAction::EnterCallee &&
|
|
testCallAndStore.getStoreBeforeCall()) ||
|
|
(action == CallControlFlowAction::ExitCallee &&
|
|
!testCallAndStore.getStoreBeforeCall()))) {
|
|
LDBG() << " Handling TestCallAndStoreOp with special logic";
|
|
(void)visitOperation(call, after, before);
|
|
} else {
|
|
LDBG() << " Using default call control flow transfer logic";
|
|
AbstractDenseBackwardDataFlowAnalysis::visitCallControlFlowTransfer(
|
|
call, action, after, before);
|
|
}
|
|
}
|
|
|
|
void NextAccessAnalysis::visitRegionBranchControlFlowTransfer(
|
|
RegionBranchOpInterface branch, RegionBranchPoint regionFrom,
|
|
RegionSuccessor regionTo, const NextAccess &after, NextAccess *before) {
|
|
LDBG() << "visitRegionBranchControlFlowTransfer: "
|
|
<< OpWithFlags(branch.getOperation(), OpPrintingFlags().skipRegions());
|
|
LDBG() << " regionFrom: " << (regionFrom.isParent() ? "parent" : "region");
|
|
LDBG() << " regionTo: " << (regionTo.isParent() ? "parent" : "region");
|
|
|
|
auto testStoreWithARegion =
|
|
dyn_cast<::test::TestStoreWithARegion>(branch.getOperation());
|
|
|
|
if (testStoreWithARegion &&
|
|
((regionTo.isParent() && !testStoreWithARegion.getStoreBeforeRegion()) ||
|
|
(regionFrom.isParent() &&
|
|
testStoreWithARegion.getStoreBeforeRegion()))) {
|
|
LDBG() << " Handling TestStoreWithARegion with special logic";
|
|
(void)visitOperation(branch, static_cast<const NextAccess &>(after),
|
|
static_cast<NextAccess *>(before));
|
|
} else {
|
|
LDBG() << " Using default region branch control flow transfer logic";
|
|
propagateIfChanged(before, before->meet(after));
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
struct TestNextAccessPass
|
|
: public PassWrapper<TestNextAccessPass, OperationPass<>> {
|
|
TestNextAccessPass() = default;
|
|
TestNextAccessPass(const TestNextAccessPass &other) : PassWrapper(other) {
|
|
interprocedural = other.interprocedural;
|
|
assumeFuncReads = other.assumeFuncReads;
|
|
}
|
|
|
|
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestNextAccessPass)
|
|
|
|
StringRef getArgument() const override { return "test-next-access"; }
|
|
|
|
Option<bool> interprocedural{
|
|
*this, "interprocedural", llvm::cl::init(true),
|
|
llvm::cl::desc("perform interprocedural analysis")};
|
|
Option<bool> assumeFuncReads{
|
|
*this, "assume-func-reads", llvm::cl::init(false),
|
|
llvm::cl::desc(
|
|
"assume external functions have read effect on all arguments")};
|
|
|
|
static constexpr llvm::StringLiteral kTagAttrName = "name";
|
|
static constexpr llvm::StringLiteral kNextAccessAttrName = "next_access";
|
|
static constexpr llvm::StringLiteral kAtEntryPointAttrName =
|
|
"next_at_entry_point";
|
|
|
|
static Attribute makeNextAccessAttribute(Operation *op,
|
|
const DataFlowSolver &solver,
|
|
const NextAccess *nextAccess) {
|
|
if (!nextAccess)
|
|
return StringAttr::get(op->getContext(), "not computed");
|
|
|
|
// Note that if the underlying value could not be computed or is unknown, we
|
|
// conservatively treat the result also unknown.
|
|
SmallVector<Attribute> attrs;
|
|
for (Value operand : op->getOperands()) {
|
|
std::optional<Value> underlyingValue =
|
|
UnderlyingValueAnalysis::getMostUnderlyingValue(
|
|
operand, [&](Value value) {
|
|
return solver.lookupState<UnderlyingValueLattice>(value);
|
|
});
|
|
if (!underlyingValue) {
|
|
attrs.push_back(StringAttr::get(op->getContext(), "unknown"));
|
|
continue;
|
|
}
|
|
Value value = *underlyingValue;
|
|
const AdjacentAccess *nextAcc = nextAccess->getAdjacentAccess(value);
|
|
if (!nextAcc || !nextAcc->isKnown()) {
|
|
attrs.push_back(StringAttr::get(op->getContext(), "unknown"));
|
|
continue;
|
|
}
|
|
|
|
SmallVector<Attribute> innerAttrs;
|
|
innerAttrs.reserve(nextAcc->get().size());
|
|
for (Operation *nextAccOp : nextAcc->get()) {
|
|
if (auto nextAccTag =
|
|
nextAccOp->getAttrOfType<StringAttr>(kTagAttrName)) {
|
|
innerAttrs.push_back(nextAccTag);
|
|
continue;
|
|
}
|
|
std::string repr;
|
|
llvm::raw_string_ostream os(repr);
|
|
nextAccOp->print(os);
|
|
innerAttrs.push_back(StringAttr::get(op->getContext(), os.str()));
|
|
}
|
|
attrs.push_back(ArrayAttr::get(op->getContext(), innerAttrs));
|
|
}
|
|
return ArrayAttr::get(op->getContext(), attrs);
|
|
}
|
|
|
|
void runOnOperation() override {
|
|
Operation *op = getOperation();
|
|
LDBG() << "runOnOperation: Starting test-next-access pass on "
|
|
<< OpWithFlags(op, OpPrintingFlags().skipRegions());
|
|
LDBG() << " interprocedural: " << interprocedural;
|
|
LDBG() << " assumeFuncReads: " << assumeFuncReads;
|
|
|
|
SymbolTableCollection symbolTable;
|
|
|
|
auto config = DataFlowConfig().setInterprocedural(interprocedural);
|
|
DataFlowSolver solver(config);
|
|
loadBaselineAnalyses(solver);
|
|
solver.load<NextAccessAnalysis>(symbolTable, assumeFuncReads);
|
|
solver.load<UnderlyingValueAnalysis>();
|
|
LDBG() << " Initializing and running dataflow solver";
|
|
if (failed(solver.initializeAndRun(op))) {
|
|
emitError(op->getLoc(), "dataflow solver failed");
|
|
return signalPassFailure();
|
|
}
|
|
LDBG() << " Dataflow solver completed successfully";
|
|
LDBG() << " Walking operations to set next access attributes";
|
|
op->walk([&](Operation *op) {
|
|
auto tag = op->getAttrOfType<StringAttr>(kTagAttrName);
|
|
if (!tag)
|
|
return;
|
|
|
|
LDBG() << " Processing tagged operation: "
|
|
<< OpWithFlags(op, OpPrintingFlags().skipRegions());
|
|
const NextAccess *nextAccess =
|
|
solver.lookupState<NextAccess>(solver.getProgramPointAfter(op));
|
|
op->setAttr(kNextAccessAttrName,
|
|
makeNextAccessAttribute(op, solver, nextAccess));
|
|
|
|
auto iface = dyn_cast<RegionBranchOpInterface>(op);
|
|
if (!iface)
|
|
return;
|
|
|
|
SmallVector<Attribute> entryPointNextAccess;
|
|
SmallVector<RegionSuccessor> regionSuccessors;
|
|
iface.getSuccessorRegions(RegionBranchPoint::parent(), regionSuccessors);
|
|
for (const RegionSuccessor &successor : regionSuccessors) {
|
|
if (!successor.getSuccessor() || successor.getSuccessor()->empty())
|
|
continue;
|
|
Block &successorBlock = successor.getSuccessor()->front();
|
|
ProgramPoint *successorPoint =
|
|
solver.getProgramPointBefore(&successorBlock);
|
|
entryPointNextAccess.push_back(makeNextAccessAttribute(
|
|
op, solver, solver.lookupState<NextAccess>(successorPoint)));
|
|
}
|
|
op->setAttr(kAtEntryPointAttrName,
|
|
ArrayAttr::get(op->getContext(), entryPointNextAccess));
|
|
});
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
namespace mlir::test {
|
|
void registerTestNextAccessPass() { PassRegistration<TestNextAccessPass>(); }
|
|
} // namespace mlir::test
|