33#include "llvm/ADT/STLExtras.h"
34#include "llvm/ADT/SetVector.h"
35#include "llvm/ADT/SmallBitVector.h"
36#include "llvm/ADT/TypeSwitch.h"
42#define GEN_PASS_DEF_SCFPARALLELLOOPFUSION
43#include "mlir/Dialect/SCF/Transforms/Passes.h.inc"
53 return walkResult.wasInterrupted();
58 ParallelOp secondPloop) {
59 if (firstPloop.getNumLoops() != secondPloop.getNumLoops())
65 return std::equal(
lhs.begin(),
lhs.end(),
rhs.begin());
67 return matchOperands(firstPloop.getLowerBound(),
68 secondPloop.getLowerBound()) &&
69 matchOperands(firstPloop.getUpperBound(),
70 secondPloop.getUpperBound()) &&
71 matchOperands(firstPloop.getStep(), secondPloop.getStep());
82 if (!isa<memref::StoreOp, vector::TransferWriteOp, vector::StoreOp>(op1))
84 bool opsAreIdentical =
86 .Case([&](memref::StoreOp storeOp1) {
87 auto storeOp2 = cast<memref::StoreOp>(op2);
88 return (storeOp1.getMemRef() == storeOp2.getMemRef()) &&
89 (storeOp1.getIndices() == storeOp2.getIndices());
91 .Case([&](vector::TransferWriteOp writeOp1) {
92 auto writeOp2 = cast<vector::TransferWriteOp>(op2);
93 return (writeOp1.getBase() == writeOp2.getBase()) &&
94 (writeOp1.getIndices() == writeOp2.getIndices()) &&
95 (writeOp1.getMask() == writeOp2.getMask()) &&
96 (writeOp1.getValueToStore().
getType() ==
97 writeOp2.getValueToStore().getType()) &&
98 (writeOp1.getInBounds() == writeOp2.getInBounds());
100 .Case([&](vector::StoreOp vecStoreOp1) {
101 auto vecStoreOp2 = cast<vector::StoreOp>(op2);
102 return (vecStoreOp1.getBase() == vecStoreOp2.getBase()) &&
103 (vecStoreOp1.getIndices() == vecStoreOp2.getIndices()) &&
104 (vecStoreOp1.getValueToStore().
getType() ==
105 vecStoreOp2.getValueToStore().getType()) &&
106 (vecStoreOp1.getAlignment() == vecStoreOp2.getAlignment()) &&
107 (vecStoreOp1.getNontemporal() ==
108 vecStoreOp2.getNontemporal());
110 .Default([](
Operation *) {
return false; });
111 return opsAreIdentical;
124 if (!val1DefOp || !val2DefOp)
129 val1DefOp, val2DefOp,
144 return constOp.value();
147 return constOp.value();
154 return constOp.value();
157 return constOp.value();
161 if (
auto applyOp = expr.
getDefiningOp<affine::AffineApplyOp>()) {
169 auto bin = dyn_cast<AffineBinaryOpExpr>(
result);
172 auto lhsDim = dyn_cast<AffineDimExpr>(bin.getLHS());
173 auto rhsDim = dyn_cast<AffineDimExpr>(bin.getRHS());
174 auto lhsConst = dyn_cast<AffineConstantExpr>(bin.getLHS());
175 auto rhsConst = dyn_cast<AffineConstantExpr>(bin.getRHS());
176 if (lhsConst && rhsDim)
177 return lhsConst.getValue();
178 if (rhsConst && lhsDim)
179 return rhsConst.getValue();
215 auto getConstLoopBoundsForIV =
216 [](
Value index) -> std::optional<std::tuple<int64_t, int64_t, int64_t>> {
217 auto blockArg = dyn_cast<BlockArgument>(
index);
220 auto *parentOp = blockArg.getOwner()->getParentOp();
221 auto loopLike = dyn_cast<LoopLikeOpInterface>(parentOp);
228 auto ivs = loopLike.getLoopInductionVars();
231 auto it = llvm::find(*ivs, blockArg);
232 if (it == ivs->end())
234 unsigned pos = std::distance(ivs->begin(), it);
235 if (pos >= ranges.size())
237 auto [lb,
ub, step] = ranges[pos];
238 return std::make_tuple(lb,
ub, step);
242 std::optional<int64_t> writeConst =
244 if (!writeConst && writeIndex) {
246 if (
auto bounds = getConstLoopBoundsForIV(writeIndex)) {
247 auto [lb,
ub, step] = *bounds;
248 if (step > 0 &&
ub == lb + step)
256 if (rangeExtent <= 0 || step <= 0)
260 int64_t rangeEnd = rangeStart + rangeExtent;
261 return lb >= rangeStart &&
ub <= rangeEnd;
264 if (offsetConst && writeConst) {
266 int64_t start = *offsetConst + *writeConst;
268 return (*loadConst >= start && *loadConst < start + extent);
269 if (
auto bounds = getConstLoopBoundsForIV(loadIndex)) {
270 auto [lb,
ub, step] = *bounds;
271 return loopIVWithinRange(lb,
ub, step, start, extent);
277 if (offsetConst && *offsetConst == 0 &&
280 if (
auto addConst =
getAddConstant(loadIndex, writeIndex, loopsIVsMap)) {
284 return (*addConst >= start && *addConst < start + extent);
290 if (
auto offsetVal = dyn_cast<Value>(offset)) {
303 .Case([&](memref::LoadOp
load) {
return load.getMemRef(); })
304 .Case([&](memref::StoreOp store) {
return store.getMemRef(); })
305 .Case([&](vector::TransferReadOp read) {
return read.getBase(); })
306 .Case([&](vector::TransferWriteOp write) {
return write.getBase(); })
307 .Case([&](vector::LoadOp
load) {
return load.getBase(); })
308 .Case([&](vector::StoreOp store) {
return store.getBase(); })
331 Value base = writeBase;
335 llvm::SmallBitVector droppedDims;
336 bool hasSubview =
false;
337 auto *ctx = loadOp.getContext();
338 if (
auto subView = base.
getDefiningOp<memref::SubViewOp>()) {
339 if (!subView.hasUnitStride())
341 baseMemref = cast<MemrefValue>(subView.getSource());
342 offsets = llvm::to_vector(subView.getMixedOffsets());
343 droppedDims = subView.getDroppedDims();
346 baseMemref = dyn_cast<MemrefValue>(base);
351 auto loadIndices = loadOp.getIndices();
352 unsigned baseRank = baseMemref.getType().getRank();
353 if ((loadOp.getMemref() != baseMemref) || (loadIndices.size() != baseRank))
356 unsigned writeRank = writeIndices.size();
357 if ((!hasSubview && writeRank != baseRank) ||
358 (hasSubview && offsets.size() != baseRank) ||
359 (vectorDimForWriteDim.size() != writeRank))
362 auto zeroAttr = IntegerAttr::get(IndexType::get(ctx), 0);
363 unsigned writeMemrefDim = 0;
364 for (
unsigned baseDim : llvm::seq(baseRank)) {
365 bool wasDropped = (hasSubview && droppedDims.test(baseDim));
366 int64_t vectorDim = !wasDropped ? vectorDimForWriteDim[writeMemrefDim] : -1;
368 if (vectorDim >= 0) {
369 int64_t dimSize = vecTy.getDimSize(vectorDim);
370 if (dimSize == ShapedType::kDynamic)
374 Value writeIndex = !wasDropped ? writeIndices[writeMemrefDim] :
Value();
390 vector::TransferWriteOp writeOp,
392 auto vecTy = dyn_cast<VectorType>(writeOp.getVector().getType());
396 unsigned writeRank = writeOp.getIndices().size();
404 for (
unsigned vecDim = 0; vecDim < permutationMap.
getNumResults(); ++vecDim) {
405 auto dimExpr = dyn_cast<AffineDimExpr>(permutationMap.
getResult(vecDim));
408 unsigned writeDim = dimExpr.getPosition();
409 if (writeDim >= writeRank || vectorDimForWriteDim[writeDim] != -1)
411 vectorDimForWriteDim[writeDim] = vecDim;
415 vecTy, vectorDimForWriteDim, ivsMap);
420 vector::StoreOp storeOp,
422 auto vecTy = dyn_cast<VectorType>(storeOp.getValueToStore().getType());
426 unsigned writeRank = storeOp.getIndices().size();
427 if (vecTy.getRank() > writeRank)
431 unsigned vecRank = vecTy.getRank();
432 for (
unsigned i = 0; i < vecRank; ++i) {
433 unsigned writeDim = writeRank - vecRank + i;
434 vectorDimForWriteDim[writeDim] = i;
438 vecTy, vectorDimForWriteDim, ivsMap);
448template <
typename OpTy1,
typename OpTy2>
450 OpTy1 op1, OpTy2 op2,
const IRMapping &firstToSecondPloopIVsMap,
454 if (!base1 || !base2)
457 auto accessThroughTrivialSubviewIsSame =
458 [&
b](memref::SubViewOp subView,
ValueRange subViewAccess,
461 LogicalResult resolved = resolveSourceIndicesRankReducingSubview(
462 subView.getLoc(),
b, subView, subViewAccess, resolvedSubviewAccess);
463 if (failed(resolved) ||
464 (resolvedSubviewAccess.size() != sourceAccess.size()))
466 for (
auto [dimIdx, resolvedIndex] :
467 llvm::enumerate(resolvedSubviewAccess)) {
476 if (
auto subView = base1.template getDefiningOp<memref::SubViewOp>();
479 base2, cast<MemrefValue>(subView.getSource())) &&
480 accessThroughTrivialSubviewIsSame(subView, op1.getIndices(),
482 firstToSecondPloopIVsMap))
486 if (
auto subView = base2.template getDefiningOp<memref::SubViewOp>();
489 base1, cast<MemrefValue>(subView.getSource())) &&
490 accessThroughTrivialSubviewIsSame(subView, op2.getIndices(),
492 firstToSecondPloopIVsMap))
501template <
typename OpTy1,
typename OpTy2>
504 auto indices1 = op1.getIndices();
505 auto indices2 = op2.getIndices();
506 if (indices1.size() != indices2.size())
508 for (
auto [idx1, idx2] : llvm::zip(indices1, indices2)) {
519 const IRMapping &firstToSecondPloopIVsMap,
521 if (!loadOp || !storeOp)
524 if (!isa<memref::LoadOp, vector::TransferReadOp, vector::LoadOp>(loadOp))
526 bool accessSameMemory =
528 .Case([&](memref::LoadOp memLoadOp) {
529 if (
auto memStoreOp = dyn_cast<memref::StoreOp>(storeOp))
531 firstToSecondPloopIVsMap,
b);
532 if (
auto vecWriteOp = dyn_cast<vector::TransferWriteOp>(storeOp))
534 firstToSecondPloopIVsMap);
535 if (
auto vecStoreOp = dyn_cast<vector::StoreOp>(storeOp))
537 firstToSecondPloopIVsMap);
540 .Case([&](vector::TransferReadOp vecReadOp) {
541 auto vecWriteOp = dyn_cast<vector::TransferWriteOp>(storeOp);
545 firstToSecondPloopIVsMap,
b) &&
546 (vecReadOp.getMask() == vecWriteOp.getMask()) &&
547 (vecReadOp.getInBounds() == vecWriteOp.getInBounds());
549 .Case([&](vector::LoadOp vecLoadOp) {
550 auto vecStoreOp = dyn_cast<vector::StoreOp>(storeOp);
554 firstToSecondPloopIVsMap,
b) &&
555 (vecLoadOp.getAlignment() == vecStoreOp.getAlignment());
557 .Default([](
Operation *) {
return false; });
558 return accessSameMemory;
563 .Case([&](memref::StoreOp storeOp) {
return storeOp.getMemRef(); })
564 .Case([&](vector::TransferWriteOp writeOp) {
return writeOp.getBase(); })
565 .Case([&](vector::StoreOp vecStoreOp) {
return vecStoreOp.getBase(); })
574 if (
auto transfWriteOp = dyn_cast<vector::TransferWriteOp>(storeOp);
575 transfWriteOp && isa<memref::LoadOp>(loadOp))
578 if (
auto vecStoreOp = dyn_cast<vector::StoreOp>(storeOp);
579 vecStoreOp && isa<memref::LoadOp>(loadOp))
589 ParallelOp firstPloop, ParallelOp secondPloop,
590 const IRMapping &firstToSecondPloopIndices,
595 llvm::SmallSetVector<Value, 4> buffersWrittenInFirstPloop;
597 auto collectStoreOpsInWalk = [&](
Operation *op) {
598 auto memOpInterf = dyn_cast_if_present<MemoryEffectOpInterface>(op);
611 MemrefValue storeOpBaseMemref = dyn_cast<MemrefValue>(storeOpBase);
612 if (!storeOpBaseMemref)
616 bufferStoresInFirstPloop[buffer].push_back(op);
617 buffersWrittenInFirstPloop.insert(buffer);
623 if (firstPloop.getBody()->walk(collectStoreOpsInWalk).wasInterrupted())
632 auto checkLoadInWalkHasNoIncompatibleDataDeps = [&](
Operation *loadOp) {
633 auto memOpInterf = dyn_cast_if_present<MemoryEffectOpInterface>(loadOp);
649 if (!isa<memref::LoadOp, vector::TransferReadOp, vector::LoadOp>(loadOp) ||
650 !isa<MemrefValue>(loadOp->getOperand(0)))
653 MemrefValue loadOpBase = cast<MemrefValue>(loadOp->getOperand(0));
656 for (
Value storedMem : buffersWrittenInFirstPloop)
657 if ((storedMem != loadedOrigBuf) &&
mayAlias(storedMem, loadedOrigBuf) &&
658 !llvm::all_of(bufferStoresInFirstPloop[storedMem],
661 firstToSecondPloopIndices);
666 auto writeOpsIt = bufferStoresInFirstPloop.find(loadedOrigBuf);
667 if (writeOpsIt == bufferStoresInFirstPloop.end())
673 if (writeOps.empty())
680 if (!llvm::all_of(writeOps, [&](
Operation *otherWriteOp) {
689 firstToSecondPloopIndices,
b)) {
698 return !secondPloop.getBody()
699 ->walk(checkLoadInWalkHasNoIncompatibleDataDeps)
708 const IRMapping &firstToSecondPloopIndices,
712 firstPloop, secondPloop, firstToSecondPloopIndices,
mayAlias,
b))
716 secondToFirstPloopIndices.
map(secondPloop.getBody()->getArguments(),
717 firstPloop.getBody()->getArguments());
719 secondPloop, firstPloop, secondToFirstPloopIndices,
mayAlias,
b);
726 const IRMapping &firstToSecondPloopIndices,
738static void fuseIfLegal(ParallelOp firstPloop, ParallelOp &secondPloop,
741 Block *block1 = firstPloop.getBody();
742 Block *block2 = secondPloop.getBody();
746 if (!
isFusionLegal(firstPloop, secondPloop, firstToSecondPloopIndices,
758 ValueRange inits2 = secondPloop.getInitVals();
761 newInitVars.append(inits2.begin(), inits2.end());
764 b.setInsertionPoint(secondPloop);
765 auto newSecondPloop = ParallelOp::create(
766 b, secondPloop.getLoc(), secondPloop.getLowerBound(),
767 secondPloop.getUpperBound(), secondPloop.getStep(), newInitVars);
769 Block *newBlock = newSecondPloop.getBody();
773 b.inlineBlockBefore(block2, newBlock, newBlock->
begin(),
775 b.inlineBlockBefore(block1, newBlock, newBlock->
begin(),
778 ValueRange results = newSecondPloop.getResults();
779 if (!results.empty()) {
780 b.setInsertionPointToEnd(newBlock);
785 newReduceArgs.append(reduceArgs2.begin(), reduceArgs2.end());
787 auto newReduceOp = scf::ReduceOp::create(
b, term2.getLoc(), newReduceArgs);
789 for (
auto &&[i, reg] : llvm::enumerate(llvm::concat<Region>(
790 term1.getReductions(), term2.getReductions()))) {
792 Block &newRedBlock = newReduceOp.getReductions()[i].
front();
793 b.inlineBlockBefore(&oldRedBlock, &newRedBlock, newRedBlock.
begin(),
797 firstPloop.replaceAllUsesWith(results.take_front(inits1.size()));
798 secondPloop.replaceAllUsesWith(results.take_back(inits2.size()));
804 secondPloop = newSecondPloop;
812 for (
auto &block : region) {
814 ploopChains.push_back({});
819 bool noSideEffects =
true;
820 for (
auto &op : block) {
821 if (
auto ploop = dyn_cast<ParallelOp>(op)) {
823 ploopChains.back().push_back(ploop);
825 ploopChains.push_back({ploop});
826 noSideEffects =
true;
834 for (
int i = 0, e = ploops.size(); i + 1 < e; ++i)
841struct ParallelLoopFusion
842 :
public impl::SCFParallelLoopFusionBase<ParallelLoopFusion> {
843 void runOnOperation()
override {
844 auto &aa = getAnalysis<AliasAnalysis>();
851 auto val2Def = val2.getDefiningOp();
855 val2Def ? val2Def->getParentOfType<ParallelOp>() :
nullptr;
856 if (val1Loop != val2Loop)
859 return !aa.alias(val1, val2).isNo();
862 getOperation()->walk([&](
Operation *child) {
871 return std::make_unique<ParallelLoopFusion>();
static bool mayAlias(Value first, Value second)
Returns true if two values may be referencing aliasing memory.
static bool canResolveAlias(Operation *loadOp, Operation *storeOp, const IRMapping &loopsIVsMap)
To be called when mayAlias(val1, val2) is true.
static bool equalIterationSpaces(ParallelOp firstPloop, ParallelOp secondPloop)
Verify equal iteration spaces.
static bool isLoadOnWrittenVector(memref::LoadOp loadOp, Value writeBase, ValueRange writeIndices, VectorType vecTy, ArrayRef< int64_t > vectorDimForWriteDim, const IRMapping &ivsMap)
Recognize scalar memref.load of an element produced by a vector write (vector.transfer_write or vecto...
static bool loadMatchesVectorWrite(memref::LoadOp loadOp, vector::TransferWriteOp writeOp, const IRMapping &ivsMap)
Recognize scalar memref.load of an element produced by a vector.transfer_write.
static std::optional< int64_t > getAddConstant(Value expr, Value base, const IRMapping &loopsIVsMap)
If the expr value is the result of an integer addition of base and a constant, return the constant.
static bool opsAccessSameIndices(OpTy1 op1, OpTy2 op2, const IRMapping &loopsIVsMap, OpBuilder &b)
Check if both memory read/write operations access the same indices (considering also the mapping of i...
static Value getStoreOpTargetBuffer(Operation *op)
static bool haveNoDataDependenciesExceptSameIndex(ParallelOp firstPloop, ParallelOp secondPloop, const IRMapping &firstToSecondPloopIndices, llvm::function_ref< bool(Value, Value)> mayAlias, OpBuilder &b)
Check that the parallel loops have no mixed access to the same buffers.
static Value getBaseMemref(Operation *op)
Return the base memref value used by the given memory op.
static bool loadsFromSameMemoryLocationWrittenBy(Operation *loadOp, Operation *storeOp, const IRMapping &firstToSecondPloopIVsMap, OpBuilder &b)
Check if the loadOp reads from the same memory location (same buffer, same indices and same propertie...
static bool loadIndexWithinWriteRange(Value loadIndex, OpFoldResult offset, Value writeIndex, int64_t extent, const IRMapping &loopsIVsMap)
static bool opsWriteSameMemLocation(Operation *op1, Operation *op2)
Check if both operations are the same type of memory write op and write to the same memory location (...
static bool noIncompatibleDataDependencies(ParallelOp firstPloop, ParallelOp secondPloop, const IRMapping &firstToSecondPloopIndices, llvm::function_ref< bool(Value, Value)> mayAlias, OpBuilder &b)
Check that in each loop there are no read ops on the buffers written by the other loop,...
static bool valsAreEquivalent(Value val1, Value val2, const IRMapping &loopsIVsMap)
Check if val1 (from the first parallel loop) and val2 (from the second) are equivalent,...
static bool isFusionLegal(ParallelOp firstPloop, ParallelOp secondPloop, const IRMapping &firstToSecondPloopIndices, llvm::function_ref< bool(Value, Value)> mayAlias, OpBuilder &b)
Check if fusion of the two parallel loops is legal: i.e.
static bool opsAccessSameIndicesViaRankReducingSubview(OpTy1 op1, OpTy2 op2, const IRMapping &firstToSecondPloopIVsMap, OpBuilder &b)
Check if both operations access the same positions of the same buffer, but one of the two does it thr...
static bool loadMatchesVectorStore(memref::LoadOp loadOp, vector::StoreOp storeOp, const IRMapping &ivsMap)
Recognize scalar memref.load of an element produced by a vector.store.
static bool hasNestedParallelOp(ParallelOp ploop)
Verify there are no nested ParallelOps.
static void fuseIfLegal(ParallelOp firstPloop, ParallelOp &secondPloop, OpBuilder builder, llvm::function_ref< bool(Value, Value)> mayAlias)
Prepend operations of firstPloop's body into secondPloop's body.
Base type for affine expression.
A multi-dimensional affine map Affine map's are immutable like Type's, and they are uniqued.
bool isProjectedPermutation(bool allowZeroInResults=false) const
Returns true if the AffineMap represents a subset (i.e.
unsigned getNumSymbols() const
unsigned getNumDims() const
unsigned getNumResults() const
AffineExpr getResult(unsigned idx) const
static AffineMap getPermutationMap(ArrayRef< unsigned > permutation, MLIRContext *context)
Returns an AffineMap representing a permutation.
Block represents an ordered list of Operations.
Operation * getTerminator()
Get the terminator operation of this block.
BlockArgListType getArguments()
A class for computing basic dominance information.
bool properlyDominates(Operation *a, Operation *b, bool enclosingOpOk=true) const
Return true if operation A properly dominates operation B, i.e.
This is a utility class for mapping one set of IR entities to another.
auto lookupOrDefault(T from) const
Lookup a mapped value within the map.
void map(Value from, Value to)
Inserts a new mapping for 'from' to 'to'.
This class coordinates rewriting a piece of IR outside of a pattern rewrite, providing a way to keep ...
This class helps build Operations.
This class represents a single result from folding an operation.
This trait indicates that the memory effects of an operation includes the effects of operations neste...
This class implements the operand iterators for the Operation class.
Operation is the basic unit of execution within MLIR.
OpTy getParentOfType()
Return the closest surrounding parent operation that is of type 'OpTy'.
OperationName getName()
The name of an operation is the key identifier for it.
MutableArrayRef< Region > getRegions()
Returns the regions held by this operation.
user_range getUsers()
Returns a range of all users.
This class contains a list of basic blocks and a link to the parent operation it is attached to.
This class provides an abstraction over the different types of ranges over Values.
This class represents an instance of an SSA value in the MLIR system, representing a computable value...
Operation * getDefiningOp() const
If this value is the result of an operation, return the operation that defines it.
static WalkResult advance()
static WalkResult interrupt()
MemrefValue skipFullyAliasingOperations(MemrefValue source)
Walk up the source chain until an operation that changes/defines the view of memory is found (i....
bool isSameViewOrTrivialAlias(MemrefValue a, MemrefValue b)
Checks if two (memref) values are the same or statically known to alias the same region of memory.
void naivelyFuseParallelOps(Region ®ion, llvm::function_ref< bool(Value, Value)> mayAlias)
Fuses all adjacent scf.parallel operations with identical bounds and step into one scf....
Include the generated interface declarations.
bool matchPattern(Value value, const Pattern &pattern)
Entry point for matching a pattern over a Value.
std::optional< int64_t > getConstantIntValue(OpFoldResult ofr)
If ofr is a constant integer or an IntegerAttr, return the integer.
Type getType(OpFoldResult ofr)
Returns the int type of the integer in ofr.
bool isMemoryEffectFree(Operation *op)
Returns true if the given operation is free of memory effects.
llvm::SmallVector< std::tuple< int64_t, int64_t, int64_t > > getConstLoopBounds(mlir::LoopLikeOpInterface loopOp)
Get constant loop bounds and steps for each of the induction variables of the given loop operation,...
detail::constant_int_predicate_matcher m_Zero()
Matches a constant scalar / vector splat / tensor splat integer zero.
TypedValue< BaseMemRefType > MemrefValue
A value with a memref type.
llvm::DenseMap< KeyT, ValueT, KeyInfoT, BucketT > DenseMap
std::unique_ptr< Pass > createParallelLoopFusionPass()
Creates a loop fusion pass which fuses parallel loops.
The following effect indicates that the operation frees some resource that has been allocated.
The following effect indicates that the operation reads from some resource.
The following effect indicates that the operation writes to some resource.
static bool isEquivalentTo(Operation *lhs, Operation *rhs, function_ref< LogicalResult(Value, Value)> checkEquivalent, function_ref< void(Value, Value)> markEquivalent=nullptr, Flags flags=Flags::None, function_ref< LogicalResult(ValueRange, ValueRange)> checkCommutativeEquivalent=nullptr)
Compare two operations (including their regions) and return if they are equivalent.