1 //===- AffineScalarReplacement.cpp - Affine scalar replacement pass -------===// 2 // 3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 4 // See https://llvm.org/LICENSE.txt for license information. 5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 6 // 7 //===----------------------------------------------------------------------===// 8 // 9 // This file implements a pass to forward affine memref stores to loads, thereby 10 // potentially getting rid of intermediate memrefs entirely. It also removes 11 // redundant loads. 12 // TODO: In the future, similar techniques could be used to eliminate 13 // dead memref store's and perform more complex forwarding when support for 14 // SSA scalars live out of 'affine.for'/'affine.if' statements is available. 15 //===----------------------------------------------------------------------===// 16 17 #include "PassDetail.h" 18 #include "mlir/Analysis/AffineAnalysis.h" 19 #include "mlir/Analysis/Utils.h" 20 #include "mlir/Dialect/Affine/IR/AffineOps.h" 21 #include "mlir/Dialect/Affine/Passes.h" 22 #include "mlir/Dialect/MemRef/IR/MemRef.h" 23 #include "mlir/Dialect/StandardOps/IR/Ops.h" 24 #include "mlir/IR/Dominance.h" 25 #include "mlir/Support/LogicalResult.h" 26 #include "llvm/ADT/SmallPtrSet.h" 27 #include <algorithm> 28 29 #define DEBUG_TYPE "memref-dataflow-opt" 30 31 using namespace mlir; 32 33 namespace { 34 // The store to load forwarding and load CSE rely on three conditions: 35 // 36 // 1) store/load providing a replacement value and load being replaced need to 37 // have mathematically equivalent affine access functions (checked after full 38 // composition of load/store operands); this implies that they access the same 39 // single memref element for all iterations of the common surrounding loop, 40 // 41 // 2) the store/load op should dominate the load op, 42 // 43 // 3) no operation that may write to memory read by the load being replaced can 44 // occur after executing the instruction (load or store) providing the 45 // replacement value and before the load being replaced (thus potentially 46 // allowing overwriting the memory read by the load). 47 // 48 // The above conditions are simple to check, sufficient, and powerful for most 49 // cases in practice - they are sufficient, but not necessary --- since they 50 // don't reason about loops that are guaranteed to execute at least once or 51 // multiple sources to forward from. 52 // 53 // TODO: more forwarding can be done when support for 54 // loop/conditional live-out SSA values is available. 55 // TODO: do general dead store elimination for memref's. This pass 56 // currently only eliminates the stores only if no other loads/uses (other 57 // than dealloc) remain. 58 // 59 struct AffineScalarReplacement 60 : public AffineScalarReplacementBase<AffineScalarReplacement> { 61 void runOnFunction() override; 62 63 LogicalResult forwardStoreToLoad(AffineReadOpInterface loadOp, 64 SmallVectorImpl<Operation *> &loadOpsToErase, 65 SmallPtrSetImpl<Value> &memrefsToErase, 66 DominanceInfo &domInfo); 67 68 void loadCSE(AffineReadOpInterface loadOp, 69 SmallVectorImpl<Operation *> &loadOpsToErase, 70 DominanceInfo &domInfo); 71 72 void findUnusedStore(AffineWriteOpInterface storeOp, 73 SmallVectorImpl<Operation *> &storeOpsToErase, 74 SmallPtrSetImpl<Value> &memrefsToErase, 75 PostDominanceInfo &postDominanceInfo); 76 }; 77 78 } // end anonymous namespace 79 80 /// Creates a pass to perform optimizations relying on memref dataflow such as 81 /// store to load forwarding, elimination of dead stores, and dead allocs. 82 std::unique_ptr<OperationPass<FuncOp>> 83 mlir::createAffineScalarReplacementPass() { 84 return std::make_unique<AffineScalarReplacement>(); 85 } 86 87 /// Ensure that all operations that could be executed after `start` 88 /// (noninclusive) and prior to `memOp` (e.g. on a control flow/op path 89 /// between the operations) do not have the potential memory effect 90 /// `EffectType` on `memOp`. `memOp` is an operation that reads or writes to 91 /// a memref. For example, if `EffectType` is MemoryEffects::Write, this method 92 /// will check if there is no write to the memory between `start` and `memOp` 93 /// that would change the read within `memOp`. 94 template <typename EffectType, typename T> 95 bool hasNoInterveningEffect(Operation *start, T memOp) { 96 97 Value memref = memOp.getMemRef(); 98 bool isOriginalAllocation = memref.getDefiningOp<memref::AllocaOp>() || 99 memref.getDefiningOp<memref::AllocOp>(); 100 101 // A boolean representing whether an intervening operation could have impacted 102 // memOp. 103 bool hasSideEffect = false; 104 105 // Check whether the effect on memOp can be caused by a given operation op. 106 std::function<void(Operation *)> checkOperation = [&](Operation *op) { 107 // If the effect has alreay been found, early exit, 108 if (hasSideEffect) 109 return; 110 111 if (auto memEffect = dyn_cast<MemoryEffectOpInterface>(op)) { 112 SmallVector<MemoryEffects::EffectInstance, 1> effects; 113 memEffect.getEffects(effects); 114 115 bool opMayHaveEffect = false; 116 for (auto effect : effects) { 117 // If op causes EffectType on a potentially aliasing location for 118 // memOp, mark as having the effect. 119 if (isa<EffectType>(effect.getEffect())) { 120 if (isOriginalAllocation && effect.getValue() && 121 (effect.getValue().getDefiningOp<memref::AllocaOp>() || 122 effect.getValue().getDefiningOp<memref::AllocOp>())) { 123 if (effect.getValue() != memref) 124 continue; 125 } 126 opMayHaveEffect = true; 127 break; 128 } 129 } 130 131 if (!opMayHaveEffect) 132 return; 133 134 // If the side effect comes from an affine read or write, try to 135 // prove the side effecting `op` cannot reach `memOp`. 136 if (isa<AffineReadOpInterface, AffineWriteOpInterface>(op)) { 137 MemRefAccess srcAccess(op); 138 MemRefAccess destAccess(memOp); 139 // Dependence analysis is only correct if both ops operate on the same 140 // memref. 141 if (srcAccess.memref == destAccess.memref) { 142 FlatAffineValueConstraints dependenceConstraints; 143 144 // Number of loops containing the start op and the ending operation. 145 unsigned minSurroundingLoops = 146 getNumCommonSurroundingLoops(*start, *memOp); 147 148 // Number of loops containing the operation `op` which has the 149 // potential memory side effect and can occur on a path between 150 // `start` and `memOp`. 151 unsigned nsLoops = getNumCommonSurroundingLoops(*op, *memOp); 152 153 // For ease, let's consider the case that `op` is a store and we're 154 // looking for other potential stores (e.g `op`) that overwrite memory 155 // after `start`, and before being read in `memOp`. In this case, we 156 // only need to consider other potential stores with depth > 157 // minSurrounding loops since `start` would overwrite any store with a 158 // smaller number of surrounding loops before. 159 unsigned d; 160 for (d = nsLoops + 1; d > minSurroundingLoops; d--) { 161 DependenceResult result = checkMemrefAccessDependence( 162 srcAccess, destAccess, d, &dependenceConstraints, 163 /*dependenceComponents=*/nullptr); 164 if (hasDependence(result)) { 165 hasSideEffect = true; 166 return; 167 } 168 } 169 170 // No side effect was seen, simply return. 171 return; 172 } 173 } 174 hasSideEffect = true; 175 return; 176 } 177 178 if (op->hasTrait<OpTrait::HasRecursiveSideEffects>()) { 179 // Recurse into the regions for this op and check whether the internal 180 // operations may have the side effect `EffectType` on memOp. 181 for (Region ®ion : op->getRegions()) 182 for (Block &block : region) 183 for (Operation &op : block) 184 checkOperation(&op); 185 return; 186 } 187 188 // Otherwise, conservatively assume generic operations have the effect 189 // on the operation 190 hasSideEffect = true; 191 return; 192 }; 193 194 // Check all paths from ancestor op `parent` to the operation `to` for the 195 // effect. It is known that `to` must be contained within `parent`. 196 auto until = [&](Operation *parent, Operation *to) { 197 // TODO check only the paths from `parent` to `to`. 198 // Currently we fallback and check the entire parent op, rather than 199 // just the paths from the parent path, stopping after reaching `to`. 200 // This is conservatively correct, but could be made more aggressive. 201 assert(parent->isAncestor(to)); 202 checkOperation(parent); 203 }; 204 205 // Check for all paths from operation `from` to operation `untilOp` for the 206 // given memory effect. 207 std::function<void(Operation *, Operation *)> recur = 208 [&](Operation *from, Operation *untilOp) { 209 assert( 210 from->getParentRegion()->isAncestor(untilOp->getParentRegion()) && 211 "Checking for side effect between two operations without a common " 212 "ancestor"); 213 214 // If the operations are in different regions, recursively consider all 215 // path from `from` to the parent of `to` and all paths from the parent 216 // of `to` to `to`. 217 if (from->getParentRegion() != untilOp->getParentRegion()) { 218 recur(from, untilOp->getParentOp()); 219 until(untilOp->getParentOp(), untilOp); 220 return; 221 } 222 223 // Now, assuming that `from` and `to` exist in the same region, perform 224 // a CFG traversal to check all the relevant operations. 225 226 // Additional blocks to consider. 227 SmallVector<Block *, 2> todoBlocks; 228 { 229 // First consider the parent block of `from` an check all operations 230 // after `from`. 231 for (auto iter = ++from->getIterator(), end = from->getBlock()->end(); 232 iter != end && &*iter != untilOp; ++iter) { 233 checkOperation(&*iter); 234 } 235 236 // If the parent of `from` doesn't contain `to`, add the successors 237 // to the list of blocks to check. 238 if (untilOp->getBlock() != from->getBlock()) 239 for (Block *succ : from->getBlock()->getSuccessors()) 240 todoBlocks.push_back(succ); 241 } 242 243 SmallPtrSet<Block *, 4> done; 244 // Traverse the CFG until hitting `to`. 245 while (todoBlocks.size()) { 246 Block *blk = todoBlocks.pop_back_val(); 247 if (done.count(blk)) 248 continue; 249 done.insert(blk); 250 for (auto &op : *blk) { 251 if (&op == untilOp) 252 break; 253 checkOperation(&op); 254 if (&op == blk->getTerminator()) 255 for (Block *succ : blk->getSuccessors()) 256 todoBlocks.push_back(succ); 257 } 258 } 259 }; 260 recur(start, memOp); 261 return !hasSideEffect; 262 } 263 264 // This attempts to find stores which have no impact on the final result. 265 // A writing op writeA will be eliminated if there exists an op writeB if 266 // 1) writeA and writeB have mathematically equivalent affine access functions. 267 // 2) writeB postdominates writeA. 268 // 3) There is no potential read between writeA and writeB. 269 void AffineScalarReplacement::findUnusedStore( 270 AffineWriteOpInterface writeA, SmallVectorImpl<Operation *> &opsToErase, 271 SmallPtrSetImpl<Value> &memrefsToErase, 272 PostDominanceInfo &postDominanceInfo) { 273 274 for (Operation *user : writeA.getMemRef().getUsers()) { 275 // Only consider writing operations. 276 auto writeB = dyn_cast<AffineWriteOpInterface>(user); 277 if (!writeB) 278 continue; 279 280 // The operations must be distinct. 281 if (writeB == writeA) 282 continue; 283 284 // Both operations must lie in the same region. 285 if (writeB->getParentRegion() != writeA->getParentRegion()) 286 continue; 287 288 // Both operations must write to the same memory. 289 MemRefAccess srcAccess(writeB); 290 MemRefAccess destAccess(writeA); 291 292 if (srcAccess != destAccess) 293 continue; 294 295 // writeB must postdominate writeA. 296 if (!postDominanceInfo.postDominates(writeB, writeA)) 297 continue; 298 299 // There cannot be an operation which reads from memory between 300 // the two writes. 301 if (!hasNoInterveningEffect<MemoryEffects::Read>(writeA, writeB)) 302 continue; 303 304 opsToErase.push_back(writeA); 305 break; 306 } 307 } 308 309 /// Attempt to eliminate loadOp by replacing it with a value stored into memory 310 /// which the load is guaranteed to retrieve. This check involves three 311 /// components: 1) The store and load must be on the same location 2) The store 312 /// must dominate (and therefore must always occur prior to) the load 3) No 313 /// other operations will overwrite the memory loaded between the given load 314 /// and store. If such a value exists, the replaced `loadOp` will be added to 315 /// `loadOpsToErase` and its memref will be added to `memrefsToErase`. 316 LogicalResult AffineScalarReplacement::forwardStoreToLoad( 317 AffineReadOpInterface loadOp, SmallVectorImpl<Operation *> &loadOpsToErase, 318 SmallPtrSetImpl<Value> &memrefsToErase, DominanceInfo &domInfo) { 319 320 // The store op candidate for forwarding that satisfies all conditions 321 // to replace the load, if any. 322 Operation *lastWriteStoreOp = nullptr; 323 324 for (auto *user : loadOp.getMemRef().getUsers()) { 325 auto storeOp = dyn_cast<AffineWriteOpInterface>(user); 326 if (!storeOp) 327 continue; 328 MemRefAccess srcAccess(storeOp); 329 MemRefAccess destAccess(loadOp); 330 331 // 1. Check if the store and the load have mathematically equivalent 332 // affine access functions; this implies that they statically refer to the 333 // same single memref element. As an example this filters out cases like: 334 // store %A[%i0 + 1] 335 // load %A[%i0] 336 // store %A[%M] 337 // load %A[%N] 338 // Use the AffineValueMap difference based memref access equality checking. 339 if (srcAccess != destAccess) 340 continue; 341 342 // 2. The store has to dominate the load op to be candidate. 343 if (!domInfo.dominates(storeOp, loadOp)) 344 continue; 345 346 // 3. Ensure there is no intermediate operation which could replace the 347 // value in memory. 348 if (!hasNoInterveningEffect<MemoryEffects::Write>(storeOp, loadOp)) 349 continue; 350 351 // We now have a candidate for forwarding. 352 assert(lastWriteStoreOp == nullptr && 353 "multiple simulataneous replacement stores"); 354 lastWriteStoreOp = storeOp; 355 } 356 357 if (!lastWriteStoreOp) 358 return failure(); 359 360 // Perform the actual store to load forwarding. 361 Value storeVal = 362 cast<AffineWriteOpInterface>(lastWriteStoreOp).getValueToStore(); 363 // Check if 2 values have the same shape. This is needed for affine vector 364 // loads and stores. 365 if (storeVal.getType() != loadOp.getValue().getType()) 366 return failure(); 367 loadOp.getValue().replaceAllUsesWith(storeVal); 368 // Record the memref for a later sweep to optimize away. 369 memrefsToErase.insert(loadOp.getMemRef()); 370 // Record this to erase later. 371 loadOpsToErase.push_back(loadOp); 372 return success(); 373 } 374 375 // The load to load forwarding / redundant load elimination is similar to the 376 // store to load forwarding. 377 // loadA will be be replaced with loadB if: 378 // 1) loadA and loadB have mathematically equivalent affine access functions. 379 // 2) loadB dominates loadA. 380 // 3) There is no write between loadA and loadB. 381 void AffineScalarReplacement::loadCSE( 382 AffineReadOpInterface loadA, SmallVectorImpl<Operation *> &loadOpsToErase, 383 DominanceInfo &domInfo) { 384 SmallVector<AffineReadOpInterface, 4> loadCandidates; 385 for (auto *user : loadA.getMemRef().getUsers()) { 386 auto loadB = dyn_cast<AffineReadOpInterface>(user); 387 if (!loadB || loadB == loadA) 388 continue; 389 390 MemRefAccess srcAccess(loadB); 391 MemRefAccess destAccess(loadA); 392 393 // 1. The accesses have to be to the same location. 394 if (srcAccess != destAccess) { 395 continue; 396 } 397 398 // 2. The store has to dominate the load op to be candidate. 399 if (!domInfo.dominates(loadB, loadA)) 400 continue; 401 402 // 3. There is no write between loadA and loadB. 403 if (!hasNoInterveningEffect<MemoryEffects::Write>(loadB.getOperation(), 404 loadA)) 405 continue; 406 407 // Check if two values have the same shape. This is needed for affine vector 408 // loads. 409 if (loadB.getValue().getType() != loadA.getValue().getType()) 410 continue; 411 412 loadCandidates.push_back(loadB); 413 } 414 415 // Of the legal load candidates, use the one that dominates all others 416 // to minimize the subsequent need to loadCSE 417 Value loadB; 418 for (AffineReadOpInterface option : loadCandidates) { 419 if (llvm::all_of(loadCandidates, [&](AffineReadOpInterface depStore) { 420 return depStore == option || 421 domInfo.dominates(option.getOperation(), 422 depStore.getOperation()); 423 })) { 424 loadB = option.getValue(); 425 break; 426 } 427 } 428 429 if (loadB) { 430 loadA.getValue().replaceAllUsesWith(loadB); 431 // Record this to erase later. 432 loadOpsToErase.push_back(loadA); 433 } 434 } 435 436 void AffineScalarReplacement::runOnFunction() { 437 // Only supports single block functions at the moment. 438 FuncOp f = getFunction(); 439 440 // Load op's whose results were replaced by those forwarded from stores. 441 SmallVector<Operation *, 8> opsToErase; 442 443 // A list of memref's that are potentially dead / could be eliminated. 444 SmallPtrSet<Value, 4> memrefsToErase; 445 446 auto &domInfo = getAnalysis<DominanceInfo>(); 447 auto &postDomInfo = getAnalysis<PostDominanceInfo>(); 448 449 // Walk all load's and perform store to load forwarding. 450 f.walk([&](AffineReadOpInterface loadOp) { 451 if (failed( 452 forwardStoreToLoad(loadOp, opsToErase, memrefsToErase, domInfo))) { 453 loadCSE(loadOp, opsToErase, domInfo); 454 } 455 }); 456 457 // Erase all load op's whose results were replaced with store fwd'ed ones. 458 for (auto *op : opsToErase) 459 op->erase(); 460 opsToErase.clear(); 461 462 // Walk all store's and perform unused store elimination 463 f.walk([&](AffineWriteOpInterface storeOp) { 464 findUnusedStore(storeOp, opsToErase, memrefsToErase, postDomInfo); 465 }); 466 // Erase all store op's which don't impact the program 467 for (auto *op : opsToErase) 468 op->erase(); 469 470 // Check if the store fwd'ed memrefs are now left with only stores and can 471 // thus be completely deleted. Note: the canonicalize pass should be able 472 // to do this as well, but we'll do it here since we collected these anyway. 473 for (auto memref : memrefsToErase) { 474 // If the memref hasn't been alloc'ed in this function, skip. 475 Operation *defOp = memref.getDefiningOp(); 476 if (!defOp || !isa<memref::AllocOp>(defOp)) 477 // TODO: if the memref was returned by a 'call' operation, we 478 // could still erase it if the call had no side-effects. 479 continue; 480 if (llvm::any_of(memref.getUsers(), [&](Operation *ownerOp) { 481 return !isa<AffineWriteOpInterface, memref::DeallocOp>(ownerOp); 482 })) 483 continue; 484 485 // Erase all stores, the dealloc, and the alloc on the memref. 486 for (auto *user : llvm::make_early_inc_range(memref.getUsers())) 487 user->erase(); 488 defOp->erase(); 489 } 490 } 491