blob: c8ad34f7f60ad8b88eeda860702e0beee9405838 [file] [edit]
//===- 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));
}