| //===- LoopInvariantCodeMotion.cpp ----------------------------------------===// |
| // |
| // 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 |
| // |
| //===----------------------------------------------------------------------===// |
| /// \file |
| /// FIR-specific Loop Invariant Code Motion pass. |
| /// The pass relies on FIR types and interfaces to prove the safety |
| /// of hoisting invariant operations out of loop-like operations. |
| /// It may be run on both HLFIR and FIR representations. |
| //===----------------------------------------------------------------------===// |
| |
| #include "flang/Optimizer/Analysis/AliasAnalysis.h" |
| #include "flang/Optimizer/Dialect/CUF/CUFOps.h" |
| #include "flang/Optimizer/Dialect/FIROperationMoveOpInterface.h" |
| #include "flang/Optimizer/Dialect/FIROpsSupport.h" |
| #include "flang/Optimizer/Dialect/FortranVariableInterface.h" |
| #include "flang/Optimizer/HLFIR/HLFIROps.h" |
| #include "flang/Optimizer/Support/Utils.h" |
| #include "flang/Optimizer/Transforms/Passes.h" |
| #include "mlir/Interfaces/LoopLikeInterface.h" |
| #include "mlir/Pass/Pass.h" |
| #include "mlir/Transforms/LoopInvariantCodeMotionUtils.h" |
| #include "llvm/ADT/TypeSwitch.h" |
| #include "llvm/Support/DebugLog.h" |
| |
| namespace fir { |
| #define GEN_PASS_DEF_LOOPINVARIANTCODEMOTION |
| #include "flang/Optimizer/Transforms/Passes.h.inc" |
| } // namespace fir |
| |
| #define DEBUG_TYPE "flang-licm" |
| |
| // Temporary engineering option for triaging LICM. |
| static llvm::cl::opt<bool> disableFlangLICM( |
| "disable-flang-licm", llvm::cl::init(false), llvm::cl::Hidden, |
| llvm::cl::desc("Disable Flang's loop invariant code motion")); |
| |
| namespace { |
| |
| using namespace mlir; |
| |
| /// The pass tries to hoist loop invariant operations with only |
| /// MemoryEffects::Read effects (MemoryEffects::Write support |
| /// may be added later). |
| /// The safety of hoisting is proven by: |
| /// * Proving that the loop runs at least one iteration. |
| /// * Proving that is is always safe to load from this location |
| /// (see isSafeToHoistLoad() comments below). |
| struct LoopInvariantCodeMotion |
| : fir::impl::LoopInvariantCodeMotionBase<LoopInvariantCodeMotion> { |
| using LoopInvariantCodeMotionBase::LoopInvariantCodeMotionBase; |
| void runOnOperation() override; |
| }; |
| |
| } // namespace |
| |
| /// 'location' is a memory reference used by a memory access. |
| /// The type of 'location' defines the data type of the access |
| /// (e.g. it is considered to be invalid to access 'i64' |
| /// data using '!fir.ref<i32>`). |
| /// For the given location, this function returns true iff |
| /// the Fortran object being accessed is a scalar that |
| /// may not be OPTIONAL. |
| /// |
| /// Note that the '!fir.ref<!fir.box<>>' accesses are considered |
| /// to be scalar, even if the underlying data is an array. |
| /// |
| /// Note that an access of '!fir.ref<scalar>' may access |
| /// an array object. For example: |
| /// real :: x(:) |
| /// do i=... |
| /// = x(10) |
| /// 'x(10)' accesses array 'x', and it may be unsafe to hoist |
| /// it without proving that '10' is a valid index for the array. |
| /// The fact that 'x' is not OPTIONAL does not allow hoisting |
| /// on its own. |
| static bool isNonOptionalScalar(Value location) { |
| while (true) { |
| LDBG() << "Checking location:\n" << location; |
| Type dataType = fir::unwrapRefType(location.getType()); |
| if (!isa<fir::BaseBoxType>(location.getType()) && |
| (!dataType || |
| (!isa<fir::BaseBoxType>(dataType) && !fir::isa_trivial(dataType) && |
| !fir::isa_derived(dataType)))) { |
| LDBG() << "Failure: data access is not scalar"; |
| return false; |
| } |
| Operation *defOp = location.getDefiningOp(); |
| if (!defOp) { |
| // If this is a function argument |
| auto blockArg = cast<BlockArgument>(location); |
| Block *block = blockArg.getOwner(); |
| if (block && block->isEntryBlock()) |
| if (auto funcOp = |
| dyn_cast_if_present<FunctionOpInterface>(block->getParentOp())) |
| if (!funcOp.getArgAttrOfType<UnitAttr>(blockArg.getArgNumber(), |
| fir::getOptionalAttrName())) { |
| LDBG() << "Success: is non optional scalar dummy"; |
| return true; |
| } |
| |
| LDBG() << "Failure: no defining operation"; |
| return false; |
| } |
| |
| // Scalars "defined" by fir.address_of or that are new |
| // allocations (e.g. fir.alloca, cuf.alloc, etc.) are present. |
| if (isa<fir::AddrOfOp>(defOp) || |
| fir::isNewAllocationResult(cast<OpResult>(location)).value_or(false)) { |
| LDBG() << "Success: is non optional scalar"; |
| return true; |
| } |
| |
| if (auto varIface = dyn_cast<fir::FortranVariableOpInterface>(defOp)) { |
| if (varIface.isOptional()) { |
| // The variable is optional, so do not look further. |
| // Note that it is possible to deduce that the optional |
| // is actually present, but we are not doing it now. |
| LDBG() << "Failure: is optional"; |
| return false; |
| } |
| |
| // In case of MLIR inlining and ASSOCIATE an [hl]fir.declare |
| // may declare a scalar variable that is actually a "view" |
| // of an array element. Originally, such [hl]fir.declare |
| // would be located inside the loop preventing the hoisting. |
| // But if we decide to hoist such [hl]fir.declare in future, |
| // we cannot rely on their attributes/types. |
| // Use reliable checks based on the variable storage. |
| |
| // If the variable has storage specifier (e.g. it is a member |
| // of COMMON, etc.), we can rely that the storage is present, |
| // and we can also rely on its FortranVariableOpInterface |
| // definition type (which is a scalar due to previous checks). |
| if (auto storageIface = |
| dyn_cast<fir::FortranVariableStorageOpInterface>(defOp)) |
| if (Value storage = storageIface.getStorage()) { |
| LDBG() << "Success: is scalar with existing storage"; |
| return true; |
| } |
| |
| // TODO: we can probably use FIR AliasAnalysis' getSource() |
| // method to identify the storage in more cases. |
| location = llvm::TypeSwitch<Operation *, Value>(defOp) |
| .Case<fir::DeclareOp, hlfir::DeclareOp>( |
| [](auto op) { return op.getMemref(); }) |
| .Default([](auto) { return nullptr; }); |
| |
| if (location) |
| continue; |
| |
| LDBG() << "Failure: cannot reason about variable storage"; |
| return false; |
| } |
| if (auto viewIface = dyn_cast<fir::FortranObjectViewOpInterface>(defOp)) { |
| location = viewIface.getViewSource(cast<OpResult>(location)); |
| } else { |
| LDBG() << "Failure: unknown operation:\n" << *defOp; |
| return false; |
| } |
| } |
| } |
| |
| /// Returns true iff it is safe to hoist the given load-like operation 'op', |
| /// which access given memory 'locations', out of the operation 'loopLike'. |
| /// The current safety conditions are: |
| /// * The load is known to be unconditionally executed in the loop and the |
| /// loop runs at least one iteration, OR |
| /// * all the accessed locations are inside scalar non-OPTIONAL |
| /// Fortran objects (Fortran descriptors are considered to be scalars). |
| /// |
| /// When \p maybeConditionallyExecuted is true, the load may be inside a |
| /// conditional region (e.g. scf.if) within the loop, so the trip count |
| /// shortcut cannot be used: even if the loop runs, the condition might never |
| /// be true and the load might access an invalid location. |
| /// TODO: analyze the parent operation to determine whether it truly |
| /// conditionally executes its body (e.g. scf.execute_region always does). |
| static bool isSafeToHoistLoad(Operation *op, ArrayRef<Value> locations, |
| LoopLikeOpInterface loopLike, |
| AliasAnalysis &aliasAnalysis, |
| bool maybeConditionallyExecuted) { |
| for (Value location : locations) |
| if (aliasAnalysis.getModRef(loopLike.getOperation(), location).isMod()) { |
| LDBG() << "Failure: reads location:\n" |
| << location << "\nwhich is modified inside the loop"; |
| return false; |
| } |
| |
| // Check that it is safe to read from all the locations before the loop. |
| if (!maybeConditionallyExecuted) { |
| std::optional<llvm::APInt> tripCount = loopLike.getStaticTripCount(); |
| if (tripCount && !tripCount->isZero()) { |
| // Loop executes at least one iteration and the load is unconditionally |
| // executed in the loop body, so it is safe to hoist. |
| LDBG() << "Success: loop has non-zero iterations"; |
| return true; |
| } |
| } |
| |
| // Check whether the access must always be valid. |
| return llvm::all_of( |
| locations, [&](Value location) { return isNonOptionalScalar(location); }); |
| // TODO: consider hoisting under condition of the loop's trip count |
| // being non-zero. |
| } |
| |
| /// Returns true iff the given 'op' is a load-like operation, |
| /// and it can be hoisted out of 'loopLike' operation. |
| /// See isSafeToHoistLoad for the meaning of \p maybeConditionallyExecuted. |
| static bool canHoistLoad(Operation *op, LoopLikeOpInterface loopLike, |
| AliasAnalysis &aliasAnalysis, |
| bool maybeConditionallyExecuted) { |
| LDBG() << "Checking operation:\n" << *op; |
| if (auto effectInterface = dyn_cast<MemoryEffectOpInterface>(op)) { |
| SmallVector<MemoryEffects::EffectInstance> effects; |
| effectInterface.getEffects(effects); |
| if (effects.empty()) { |
| LDBG() << "Failure: not a load"; |
| return false; |
| } |
| llvm::SetVector<Value> locations; |
| for (const MemoryEffects::EffectInstance &effect : effects) { |
| Value location = effect.getValue(); |
| if (!isa<MemoryEffects::Read>(effect.getEffect())) { |
| LDBG() << "Failure: has unsupported effects"; |
| return false; |
| } else if (!location) { |
| LDBG() << "Failure: reads from unknown location"; |
| return false; |
| } |
| locations.insert(location); |
| } |
| return isSafeToHoistLoad(op, locations.getArrayRef(), loopLike, |
| aliasAnalysis, maybeConditionallyExecuted); |
| } |
| LDBG() << "Failure: has unknown effects"; |
| return false; |
| } |
| |
| /// Recursively collect regions from operations inside \p region, skipping |
| /// IsolatedFromAbove operations (whose regions form a separate scope) and |
| /// LoopLikeOpInterface operations (which have their own LICM invocation). |
| static void collectNestedRegions(Region ®ion, |
| SmallVectorImpl<Region *> &result) { |
| for (Operation &op : region.getOps()) { |
| if (op.hasTrait<OpTrait::IsIsolatedFromAbove>()) |
| continue; |
| if (isa<LoopLikeOpInterface>(&op)) |
| continue; |
| for (Region &nested : op.getRegions()) { |
| result.push_back(&nested); |
| collectNestedRegions(nested, result); |
| } |
| } |
| } |
| |
| void LoopInvariantCodeMotion::runOnOperation() { |
| if (disableFlangLICM) { |
| LDBG() << "Skipping [HL]FIR LoopInvariantCodeMotion()"; |
| return; |
| } |
| |
| LDBG() << "Enter [HL]FIR LoopInvariantCodeMotion()"; |
| |
| auto &aliasAnalysis = getAnalysis<AliasAnalysis>(); |
| aliasAnalysis.addAnalysisImplementation(fir::AliasAnalysis{}); |
| |
| std::function<bool(Operation *, LoopLikeOpInterface, bool)> |
| shouldMoveOutOfLoop = [&](Operation *op, LoopLikeOpInterface loopLike, |
| bool maybeConditionallyExecuted) { |
| if (isPure(op)) { |
| LDBG() << "Pure operation: " << *op; |
| return true; |
| } |
| |
| // Handle RecursivelySpeculatable operations that have |
| // RecursiveMemoryEffects by checking if all their |
| // nested operations can be hoisted. |
| auto iface = dyn_cast<ConditionallySpeculatable>(op); |
| if (iface && iface.getSpeculatability() == |
| Speculation::RecursivelySpeculatable) { |
| if (op->hasTrait<OpTrait::HasRecursiveMemoryEffects>()) { |
| LDBG() << "Checking recursive operation:\n" << *op; |
| llvm::SmallVector<Operation *> nestedOps; |
| for (Region ®ion : op->getRegions()) |
| for (Block &block : region) |
| for (Operation &nestedOp : block) |
| nestedOps.push_back(&nestedOp); |
| |
| bool result = llvm::all_of(nestedOps, [&](Operation *nestedOp) { |
| return shouldMoveOutOfLoop(nestedOp, loopLike, |
| maybeConditionallyExecuted); |
| }); |
| LDBG() << "Recursive operation can" << (result ? "" : "not") |
| << " be hoisted"; |
| |
| // If nested operations cannot be hoisted, there is nothing |
| // else to check. Also if the operation itself does not have |
| // any memory effects, we can return the result now. |
| // Otherwise, we have to check the operation itself below. |
| if (!result || !isa<MemoryEffectOpInterface>(op)) |
| return result; |
| } |
| } |
| return canHoistLoad(op, loopLike, aliasAnalysis, |
| maybeConditionallyExecuted); |
| }; |
| |
| getOperation()->walk([&](LoopLikeOpInterface loopLike) { |
| if (!fir::canMoveOutOf(loopLike, nullptr)) { |
| LDBG() << "Cannot hoist anything out of loop operation: "; |
| LDBG_OS([&](llvm::raw_ostream &os) { |
| loopLike->print(os, OpPrintingFlags().skipRegions()); |
| }); |
| return; |
| } |
| // We always hoist operations to the parent operation of the loopLike. |
| // Check that the parent operation allows the hoisting, e.g. |
| // omp::LoopWrapperInterface operations assume tight nesting |
| // of the inner maybe loop-like operations, so hoisting |
| // to such a parent would be invalid. We rely on |
| // fir::canMoveFromDescendant() to identify whether the hoisting |
| // is allowed. |
| Operation *parentOp = loopLike->getParentOp(); |
| if (!parentOp) { |
| LDBG() << "Skipping top-level loop-like operation?"; |
| return; |
| } else if (!fir::canMoveFromDescendant(parentOp, loopLike, nullptr)) { |
| LDBG() << "Cannot hoist anything into operation: "; |
| LDBG_OS([&](llvm::raw_ostream &os) { |
| parentOp->print(os, OpPrintingFlags().skipRegions()); |
| }); |
| return; |
| } |
| auto isDefinedOutsideRegion = [&](Value value, Region *) { |
| return loopLike.isDefinedOutsideOfLoop(value); |
| }; |
| // Check canMoveOutOf for the candidate and all its nested operations. |
| // Moving an operation with regions also moves its contents, so |
| // restrictions like cuf.kernel blocking !fir.ref operands must be |
| // checked transitively. |
| auto canMoveOutOfOp = [&](Operation *regionOwner, Operation *candidate) { |
| if (!fir::canMoveOutOf(regionOwner, candidate)) |
| return false; |
| bool blocked = false; |
| candidate->walk([&](Operation *nested) { |
| if (nested != candidate && !fir::canMoveOutOf(regionOwner, nested)) |
| blocked = true; |
| return blocked ? WalkResult::interrupt() : WalkResult::advance(); |
| }); |
| return !blocked; |
| }; |
| auto canMoveOutOfLoop = [&](Operation *op) { |
| if (!canMoveOutOfOp(loopLike, op)) { |
| LDBG() << "Cannot hoist " << *op << " out of the loop"; |
| return false; |
| } |
| if (!fir::canMoveFromDescendant(parentOp, loopLike, op)) { |
| LDBG() << "Cannot hoist " << *op << " into the parent of the loop"; |
| return false; |
| } |
| return true; |
| }; |
| auto moveOutOfRegion = [&](Operation *op, Region *) { |
| loopLike.moveOutOfLoop(op); |
| }; |
| |
| moveLoopInvariantCode( |
| loopLike.getLoopRegions(), isDefinedOutsideRegion, |
| /*shouldMoveOutOfRegion=*/ |
| [&](Operation *op, Region *) { |
| return canMoveOutOfLoop(op) && |
| shouldMoveOutOfLoop(op, loopLike, |
| /*maybeConditionallyExecuted=*/false); |
| }, |
| moveOutOfRegion); |
| |
| if (hoistFromNestedRegions == fir::LICMNestedHoistingMode::None) |
| return; |
| |
| // Hoist loop-invariant ops from nested regions (e.g., fir.convert |
| // inside scf.if) out of the loop. This enables CSE to deduplicate |
| // converted memrefs, which improves alias analysis for parallelization. |
| // The callbacks close over loopLike (ignoring the Region* parameter), |
| // so invariance and movement are evaluated against the loop, not the |
| // nested region. |
| // Loads hoisted from nested regions are treated as maybe-conditionally |
| // executed: we do not know whether the parent operation always executes |
| // its body (e.g. scf.execute_region does, scf.if might not), so the |
| // trip count shortcut cannot prove safety. |
| // TODO: analyze the parent operation to determine whether it truly |
| // conditionally executes its body. |
| SmallVector<Region *> nestedRegions; |
| for (Region *loopRegion : loopLike.getLoopRegions()) |
| collectNestedRegions(*loopRegion, nestedRegions); |
| |
| if (nestedRegions.empty()) |
| return; |
| |
| auto shouldMoveFromNestedRegion = [&](Operation *op, Region *) { |
| // Check that all intermediate operations between op and the loop |
| // allow the candidate to be moved out. For example, cuf.kernel |
| // restricts hoisting of operations with !fir.ref operands. |
| for (Operation *ancestor = op->getParentOp(); |
| ancestor != loopLike.getOperation(); |
| ancestor = ancestor->getParentOp()) { |
| if (!ancestor) { |
| // The operation is no longer nested inside the loop (a parent |
| // operation was already hoisted out). Nothing to do. |
| return false; |
| } |
| if (!canMoveOutOfOp(ancestor, op)) { |
| LDBG() << "Cannot hoist " << *op |
| << " out of intermediate operation: "; |
| LDBG_OS([&](llvm::raw_ostream &os) { |
| ancestor->print(os, OpPrintingFlags().skipRegions()); |
| }); |
| return false; |
| } |
| } |
| return canMoveOutOfLoop(op) && |
| shouldMoveOutOfLoop(op, loopLike, |
| /*maybeConditionallyExecuted=*/true); |
| }; |
| if (hoistFromNestedRegions == fir::LICMNestedHoistingMode::Aggressive) { |
| moveLoopInvariantCode(nestedRegions, isDefinedOutsideRegion, |
| shouldMoveFromNestedRegion, moveOutOfRegion); |
| } else { |
| // "cheap" mode: only hoist fir.convert. |
| // TODO: refine the cost model for "cheap" hoisting to include |
| // other inexpensive operations. |
| moveLoopInvariantCode( |
| nestedRegions, isDefinedOutsideRegion, |
| /*shouldMoveOutOfRegion=*/ |
| [&](Operation *op, Region *region) { |
| return isa<fir::ConvertOp>(op) && |
| shouldMoveFromNestedRegion(op, region); |
| }, |
| moveOutOfRegion); |
| } |
| }); |
| |
| LDBG() << "Exit [HL]FIR LoopInvariantCodeMotion()"; |
| } |