| //===- AllocationPlacement.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 |
| // |
| //===----------------------------------------------------------------------===// |
| // |
| // This pass decides, for each array allocation in a function, whether it should |
| // live on the stack (fir.alloca) or on the heap (fir.allocmem), and rewrites it |
| // accordingly. The decision is delegated to the policy in |
| // AllocationPlacementPolicy.h. Two rewrite engines are reused: |
| // - stack-to-heap uses fir::replaceAllocas (MemoryUtils); |
| // - heap-to-stack reuses the StackArrays analysis and rewrite, which only |
| // stackifies fir.allocmem that are provably freed on all paths. |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "StackArrays.h" |
| #include "flang/Optimizer/Dialect/FIRAttr.h" |
| #include "flang/Optimizer/Dialect/FIRDialect.h" |
| #include "flang/Optimizer/Dialect/FIROps.h" |
| #include "flang/Optimizer/Dialect/FIROpsSupport.h" |
| #include "flang/Optimizer/Dialect/FIRType.h" |
| #include "flang/Optimizer/Dialect/Support/FIRContext.h" |
| #include "flang/Optimizer/Support/DataLayout.h" |
| #include "flang/Optimizer/Transforms/AllocationPlacementPolicy.h" |
| #include "flang/Optimizer/Transforms/MemoryUtils.h" |
| #include "flang/Optimizer/Transforms/Passes.h" |
| #include "mlir/Dialect/Arith/IR/Arith.h" |
| #include "mlir/Dialect/DLTI/DLTI.h" |
| #include "mlir/Dialect/Func/IR/FuncOps.h" |
| #include "mlir/Dialect/LLVMIR/LLVMDialect.h" |
| #include "mlir/IR/Diagnostics.h" |
| #include "mlir/Pass/Pass.h" |
| #include "mlir/Transforms/GreedyPatternRewriteDriver.h" |
| #include "llvm/ADT/DenseSet.h" |
| #include "llvm/ADT/SmallVector.h" |
| #include "llvm/Support/Debug.h" |
| #include <optional> |
| |
| namespace fir { |
| #define GEN_PASS_DEF_ALLOCATIONPLACEMENT |
| #include "flang/Optimizer/Transforms/Passes.h.inc" |
| } // namespace fir |
| |
| #define DEBUG_TYPE "allocation-placement" |
| |
| //===----------------------------------------------------------------------===// |
| // Default placement policy |
| //===----------------------------------------------------------------------===// |
| |
| fir::AllocationPlacement |
| fir::decideAllocationPlacement(const AllocationInfo &info, |
| const AllocationPlacementThresholds &thresholds, |
| std::size_t stackBytesUsed) { |
| using P = fir::AllocationPlacement; |
| |
| // Translate a "should this be on the stack" decision into a placement, |
| // accounting for where the allocation currently lives. |
| auto place = [&](bool wantStack) -> P { |
| if (wantStack) |
| return info.isCurrentlyOnStack ? P::Leave : P::Stack; |
| return info.isCurrentlyOnStack ? P::Heap : P::Leave; |
| }; |
| |
| // -fstack-arrays: put everything on the stack (best effort). The |
| // heap-to-stack conversion still only happens where it is provably safe. |
| if (thresholds.stackArrays) |
| return place(/*wantStack=*/true); |
| |
| // Runtime-sized arrays (automatic arrays, dynamic temporaries) go on the |
| // heap. |
| if (info.isDynamic) |
| return place(/*wantStack=*/false); |
| |
| // Without a known constant size we cannot reason about thresholds. |
| if (!info.byteSize) |
| return P::Leave; |
| |
| // Constant-size user variables always go on the stack. |
| if (!info.isTemporary) |
| return place(/*wantStack=*/true); |
| |
| auto size = static_cast<std::size_t>(*info.byteSize); |
| if (size <= thresholds.smallArrayThresholdBytes) |
| // Small arrays go on the stack while the per-function budget allows it. |
| return place(/*wantStack=*/stackBytesUsed + size <= |
| thresholds.totalStackLimitBytes); |
| |
| // Big array temporaries go on the heap. |
| return place(/*wantStack=*/false); |
| } |
| |
| namespace { |
| |
| /// Return true if the allocation is a compiler temporary, i.e. it has no |
| /// uniqued name (user variables always carry one). |
| static bool isTemporaryAllocation(mlir::Operation *op) { |
| std::optional<llvm::StringRef> uniqName; |
| if (auto alloca = mlir::dyn_cast<fir::AllocaOp>(op)) |
| uniqName = alloca.getUniqName(); |
| else if (auto allocmem = mlir::dyn_cast<fir::AllocMemOp>(op)) |
| uniqName = allocmem.getUniqName(); |
| return !uniqName || uniqName->empty(); |
| } |
| |
| /// Return the constant byte size of an array allocation, or std::nullopt if it |
| /// is dynamic or cannot be determined. |
| static std::optional<std::int64_t> |
| getConstantByteSize(mlir::Operation *op, |
| const std::optional<mlir::DataLayout> &dl, |
| const std::optional<fir::KindMapping> &kindMap) { |
| if (!dl || !kindMap) |
| return std::nullopt; |
| if (auto alloca = mlir::dyn_cast<fir::AllocaOp>(op)) |
| return fir::getAllocaByteSize(alloca, *dl, *kindMap); |
| if (auto allocmem = mlir::dyn_cast<fir::AllocMemOp>(op)) { |
| if (allocmem.hasLenParams() || allocmem.hasShapeOperands()) |
| return std::nullopt; |
| if (auto sizeAndAlignment = fir::getTypeSizeAndAlignment( |
| op->getLoc(), allocmem.getAllocatedType(), *dl, *kindMap)) |
| return static_cast<std::int64_t>(sizeAndAlignment->first); |
| } |
| return std::nullopt; |
| } |
| |
| /// Replacement generator used for stack-to-heap conversions (fir.alloca -> |
| /// fir.allocmem). Mirrors the MemoryAllocation pass. |
| static mlir::Value genAllocmem(mlir::OpBuilder &builder, fir::AllocaOp alloca, |
| bool /*deallocPointsDominateAlloc*/) { |
| mlir::Type varTy = alloca.getInType(); |
| auto unpackName = [](std::optional<llvm::StringRef> opt) -> llvm::StringRef { |
| if (opt) |
| return *opt; |
| return {}; |
| }; |
| llvm::StringRef uniqName = unpackName(alloca.getUniqName()); |
| llvm::StringRef bindcName = unpackName(alloca.getBindcName()); |
| auto heap = fir::AllocMemOp::create(builder, alloca.getLoc(), varTy, uniqName, |
| bindcName, alloca.getTypeparams(), |
| alloca.getShape()); |
| LLVM_DEBUG(llvm::dbgs() << "allocation placement: replaced " << alloca |
| << " with " << heap << '\n'); |
| return heap; |
| } |
| |
| static void genFreemem(mlir::Location loc, mlir::OpBuilder &builder, |
| mlir::Value allocmem) { |
| [[maybe_unused]] auto free = fir::FreeMemOp::create(builder, loc, allocmem); |
| LLVM_DEBUG(llvm::dbgs() << "allocation placement: add free " << free |
| << " for " << allocmem << '\n'); |
| } |
| |
| [[maybe_unused]] static llvm::StringRef |
| placementName(fir::AllocationPlacement placement) { |
| switch (placement) { |
| case fir::AllocationPlacement::Stack: |
| return "stack"; |
| case fir::AllocationPlacement::Heap: |
| return "heap"; |
| case fir::AllocationPlacement::Leave: |
| return "leave"; |
| } |
| return "?"; |
| } |
| |
| /// Return true if \p placement leaves the allocation on the stack. |
| static bool endsUpOnStack(fir::AllocationPlacement placement, |
| bool isCurrentlyOnStack) { |
| return placement == fir::AllocationPlacement::Stack || |
| (placement == fir::AllocationPlacement::Leave && isCurrentlyOnStack); |
| } |
| |
| class AllocationPlacementPass |
| : public fir::impl::AllocationPlacementBase<AllocationPlacementPass> { |
| public: |
| AllocationPlacementPass() = default; |
| AllocationPlacementPass(const AllocationPlacementPass &pass) |
| : fir::impl::AllocationPlacementBase<AllocationPlacementPass>(pass), |
| placementHook(pass.placementHook) {} |
| AllocationPlacementPass(fir::AllocationPlacementOptions options) |
| : fir::impl::AllocationPlacementBase<AllocationPlacementPass>( |
| std::move(options)) {} |
| AllocationPlacementPass(fir::AllocationPlacementOptions options, |
| fir::AllocationPlacementHook hook) |
| : fir::impl::AllocationPlacementBase<AllocationPlacementPass>( |
| std::move(options)), |
| placementHook(std::move(hook)) {} |
| |
| void runOnOperation() override; |
| |
| private: |
| fir::AllocationPlacementHook placementHook; |
| }; |
| |
| void AllocationPlacementPass::runOnOperation() { |
| mlir::func::FuncOp func = getOperation(); |
| if (func.empty()) |
| return; |
| |
| fir::AllocationPlacementThresholds baseThresholds; |
| baseThresholds.stackArrays = stackArrays; |
| baseThresholds.smallArrayThresholdBytes = smallArrayThresholdBytes; |
| baseThresholds.totalStackLimitBytes = totalStackLimitBytes; |
| |
| auto module = func->getParentOfType<mlir::ModuleOp>(); |
| std::optional<mlir::DataLayout> dl = |
| module ? fir::support::getOrSetMLIRDataLayout( |
| module, /*allowDefaultLayout=*/false) |
| : std::nullopt; |
| std::optional<fir::KindMapping> kindMap; |
| if (module) |
| kindMap = fir::getKindMapping(module); |
| |
| // Analysis of which fir.allocmem can be safely moved to the stack, and where. |
| auto &analysis = getAnalysis<fir::StackArraysAnalysisWrapper>(); |
| const fir::StackArraysAnalysisWrapper::AllocMemMap *candidateOps = |
| analysis.getCandidateOps(func); |
| if (!candidateOps) { |
| signalPassFailure(); |
| return; |
| } |
| |
| // Walk allocations in deterministic program order, maintaining the running |
| // per-function stack budget while collecting the conversions to perform. |
| std::size_t stackBytesUsed = 0; |
| llvm::DenseSet<mlir::Operation *> allocasToHeap; |
| llvm::SmallVector<mlir::Operation *> allocmemsToStack; |
| |
| func.walk([&](mlir::Operation *op) { |
| auto alloca = mlir::dyn_cast<fir::AllocaOp>(op); |
| auto allocmem = mlir::dyn_cast<fir::AllocMemOp>(op); |
| if (!alloca && !allocmem) |
| return; |
| |
| if (alloca) { |
| // Do not touch to fir.alloca that must stay fir.alloca (e.g. |
| // the temporary alloca for array function results that will |
| // later be promoted to hidden arguments). |
| auto attr = alloca->getAttrOfType<fir::MustBeStackAttr>( |
| fir::MustBeStackAttr::getAttrName()); |
| if (attr && attr.getValue()) |
| return; |
| } |
| |
| // Only array allocations are considered. |
| mlir::Type inTy = alloca ? alloca.getInType() : allocmem.getAllocatedType(); |
| if (!mlir::isa<fir::SequenceType>(inTy)) |
| return; |
| |
| fir::AllocationInfo info; |
| info.op = op; |
| info.isCurrentlyOnStack = static_cast<bool>(alloca); |
| info.isTemporary = isTemporaryAllocation(op); |
| info.isDynamic = |
| alloca ? alloca.isDynamic() |
| : (allocmem.hasLenParams() || allocmem.hasShapeOperands()); |
| info.byteSize = getConstantByteSize(op, dl, kindMap); |
| |
| // A hook, if provided, fully overrides the default policy; it may delegate |
| // back to decideAllocationPlacement after adjusting the thresholds. |
| fir::AllocationPlacement placement = |
| placementHook ? placementHook(info, baseThresholds, stackBytesUsed) |
| : fir::decideAllocationPlacement(info, baseThresholds, |
| stackBytesUsed); |
| |
| // Account for the decision in the running stack budget. |
| if (endsUpOnStack(placement, info.isCurrentlyOnStack) && info.byteSize) |
| stackBytesUsed += static_cast<std::size_t>(*info.byteSize); |
| |
| LLVM_DEBUG({ |
| llvm::dbgs() << "allocation-placement: " |
| << (info.isCurrentlyOnStack ? "alloca" : "allocmem") << " " |
| << (info.isTemporary ? "temp" : "user") << " "; |
| if (info.isDynamic) |
| llvm::dbgs() << "dynamic"; |
| else if (info.byteSize) |
| llvm::dbgs() << *info.byteSize << "B"; |
| else |
| llvm::dbgs() << "unknown-size"; |
| llvm::dbgs() << " -> " << placementName(placement) << "\n"; |
| }); |
| |
| if (alloca && placement == fir::AllocationPlacement::Heap) |
| allocasToHeap.insert(op); |
| else if (allocmem && placement == fir::AllocationPlacement::Stack && |
| candidateOps->contains(op)) |
| allocmemsToStack.push_back(op); |
| }); |
| |
| LLVM_DEBUG(llvm::dbgs() << "allocation-placement: " << func.getSymName() |
| << ": " << allocasToHeap.size() << " stack->heap, " |
| << allocmemsToStack.size() << " heap->stack, " |
| << stackBytesUsed << " stack bytes used\n"); |
| |
| // Heap-to-stack: only provably-safe candidates are converted. |
| if (!allocmemsToStack.empty()) { |
| mlir::MLIRContext &context = getContext(); |
| mlir::RewritePatternSet patterns(&context); |
| mlir::GreedyRewriteConfig config; |
| // Prevent the pattern driver from merging blocks; otherwise use the default |
| // configuration (folding on) as the legacy stack-arrays pass did. |
| config.setRegionSimplificationLevel( |
| mlir::GreedySimplifyRegionLevel::Disabled); |
| patterns.insert<fir::AllocMemConversion>(&context, *candidateOps, dl, |
| kindMap); |
| if (mlir::failed(mlir::applyOpPatternsGreedily( |
| allocmemsToStack, std::move(patterns), config))) { |
| mlir::emitError(func->getLoc(), |
| "error in allocation placement (heap to stack)\n"); |
| signalPassFailure(); |
| return; |
| } |
| } |
| |
| // Stack-to-heap. |
| if (!allocasToHeap.empty()) { |
| mlir::IRRewriter rewriter(&getContext()); |
| auto mustReplace = [&](fir::AllocaOp alloca) { |
| return allocasToHeap.contains(alloca.getOperation()); |
| }; |
| fir::replaceAllocas(rewriter, func.getOperation(), mustReplace, genAllocmem, |
| genFreemem); |
| } |
| } |
| |
| } // namespace |
| |
| std::unique_ptr<mlir::Pass> |
| fir::createAllocationPlacement(const fir::AllocationPlacementOptions &options, |
| fir::AllocationPlacementHook placementHook) { |
| return std::make_unique<AllocationPlacementPass>(options, |
| std::move(placementHook)); |
| } |