blob: ca3bd31835d01c8f4494efceef4da0ae4f4ed9d9 [file] [edit]
//===- LoopTripCountVersioning.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
/// Versions an innermost loop whose runtime trip count is likely to equal a
/// compile-time constant N:
///
/// if (ub == N) { copy of the loop with ub := N } else { original loop }
///
/// Both arms are the original loop with only the bound changed, so the
/// transformation is legal wherever the guard is uniform, which holds on a CPU
/// and not on a GPU: a thread-varying guard could split threads across two
/// copies of a barrier, so device code is excluded. The constant trip count
/// lets LLVM fully unroll the fast arm, or vectorize it without a remainder
/// loop.
///
/// Heuristic: the loop variable is the leading subscript of an explicit-shape
/// array whose declared leading extent N is at most max-section-extent. For
/// `a(1:m, :) = 0` with `real :: a(4, n)`, the inner loop is versioned on
/// `m == 4`. Loops are skipped when N times the number of FIR operations in
/// the body exceeds max-unrolled-ops, a proxy for whether LLVM will fully
/// unroll the fast arm. This runs on FIR, the only level where both the loop
/// and the array's declared type exist.
//
//===----------------------------------------------------------------------===//
#include "flang/Optimizer/Builder/CUFCommon.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/Transforms/Passes.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/OpenACC/OpenACC.h"
#include "mlir/Dialect/OpenMP/OpenMPDialect.h"
#include "mlir/IR/Builders.h"
#include "mlir/Interfaces/LoopLikeInterface.h"
#include "mlir/Pass/Pass.h"
#include "llvm/ADT/Statistic.h"
#include "llvm/Support/Debug.h"
namespace fir {
#define GEN_PASS_DEF_LOOPTRIPCOUNTVERSIONING
#include "flang/Optimizer/Transforms/Passes.h.inc"
} // namespace fir
#define DEBUG_TYPE "flang-loop-tripcount-versioning"
STATISTIC(NumLoopsExamined, "Number of innermost loops examined");
STATISTIC(NumLoopsTooLarge,
"Number of candidates skipped: N * body size > max-unrolled-ops");
STATISTIC(NumLoopsVersioned, "Number of loops versioned on trip count");
namespace {
/// One specialized copy of a loop: when \p condition holds at runtime, the
/// loop runs exactly \p tripCount iterations. Both values must dominate the
/// loop.
struct LoopVersion {
mlir::Value condition;
mlir::Value tripCount;
};
/// An innermost `do i = 1, ub` whose induction variable is the leading
/// subscript of an array with constant declared leading extent \p extent.
struct SectionExtentCandidate {
fir::DoLoopOp loop;
int64_t extent;
};
class LoopTripCountVersioningPass
: public fir::impl::LoopTripCountVersioningBase<
LoopTripCountVersioningPass> {
public:
LoopTripCountVersioningPass() = default;
LoopTripCountVersioningPass(
const fir::LoopTripCountVersioningOptions &options)
: Base(options) {}
void runOnOperation() override;
};
} // namespace
/// Look through fir.convert operations.
static mlir::Value stripConverts(mlir::Value val) {
while (auto cvt = val.getDefiningOp<fir::ConvertOp>())
val = cvt.getValue();
return val;
}
/// Is \p val the integer constant \p expected, looking through converts?
static bool isConstantInt(mlir::Value val, int64_t expected) {
std::optional<llvm::APInt> cst = fir::getIntIfConstant(stripConverts(val));
return cst && cst->trySExtValue() == expected;
}
/// The shape operand is optional on fir.array_coor. An absent shape or a plain
/// fir.shape carries only extents, so the lower bound is one in every
/// dimension; only a fir.shape_shift can say otherwise.
static bool hasUnitLeadingLowerBound(mlir::Value shapeLike) {
if (!shapeLike || shapeLike.getDefiningOp<fir::ShapeOp>())
return true;
auto shapeShift = shapeLike.getDefiningOp<fir::ShapeShiftOp>();
return shapeShift && isConstantInt(shapeShift.getOrigins().front(), 1);
}
/// If the leading subscript of \p coor is \p iv, and the array's leading
/// dimension has a constant declared extent onto which \p iv maps one-to-one
/// (unit lower bound and, if sliced, a triplet `(1, sectionUb, 1)`), return
/// the extent.
///
/// Only the leading dimension is considered: it becomes the innermost loop,
/// and unrolling produces contiguous memory accesses. An array section that
/// reaches the loop through a temporary box (fir.box_addr of a fir.embox) is
/// not recognized. Both are possible future extensions.
static std::optional<int64_t> getLeadingExtentIndexedBy(fir::ArrayCoorOp coor,
mlir::Value iv,
mlir::Value sectionUb) {
// unwrapRefType leaves a box type as is, so this also rejects boxed arrays.
auto seqTy = mlir::dyn_cast<fir::SequenceType>(
fir::unwrapRefType(coor.getMemref().getType()));
if (!seqTy || seqTy.getShape().empty())
return std::nullopt;
int64_t extent = seqTy.getShape().front();
if (extent == fir::SequenceType::getUnknownExtent())
return std::nullopt;
if (!hasUnitLeadingLowerBound(coor.getShape()))
return std::nullopt;
mlir::ValueRange indices = coor.getIndices();
if (indices.empty() || stripConverts(indices.front()) != iv)
return std::nullopt;
if (mlir::Value slice = coor.getSlice()) {
auto sliceOp = slice.getDefiningOp<fir::SliceOp>();
if (!sliceOp)
return std::nullopt;
mlir::ValueRange triples = sliceOp.getTriples();
if (!isConstantInt(triples[0], 1) || !isConstantInt(triples[2], 1) ||
stripConverts(triples[1]) != sectionUb)
return std::nullopt;
}
return extent;
}
/// If \p loop is an innermost `do i = 1, ub step 1` with a non-constant upper
/// bound whose induction variable is the leading subscript of at least one
/// array with a constant declared leading extent, return the smallest such
/// extent N, provided 1 < N <= \p maxExtent. The smallest extent is the value
/// ub must equal when every such array is at full extent, which is the case
/// shape conformance forces for array-section assignments.
///
/// Accesses under a fir.if count too: a WHERE mask lowers to a fir.if, and
/// `b` in `where (m(1:n)) a(1:n) = b` forces n == size(b) whether or not it
/// is touched. TODO: a user conditional like `if (c) b(i) = 0` looks the
/// same and does not bound ub; distinguishing the two needs a mark from
/// HLFIR lowering. Counting it only picks a smaller N than is likely.
///
/// Extent 1 is skipped: legacy code declares dummies as `a(1)` and indexes
/// past them, so `ub == 1` would almost never hold.
static std::optional<int64_t> getSectionExtent(fir::DoLoopOp loop,
unsigned maxExtent) {
// A constant upper bound needs no versioning.
if (!isConstantInt(loop.getLowerBound(), 1) ||
!isConstantInt(loop.getStep(), 1) ||
fir::getIntIfConstant(stripConverts(loop.getUpperBound())))
return std::nullopt;
// Array-section lowering clamps the loop bound to max(ub, 0) while the slice
// triplet carries the unclamped ub; compare against the unclamped value.
mlir::Value ub = loop.getUpperBound();
mlir::Value sectionUb = ub;
if (auto maxOp = ub.getDefiningOp<mlir::arith::MaxSIOp>()) {
if (isConstantInt(maxOp.getRhs(), 0))
sectionUb = maxOp.getLhs();
else if (isConstantInt(maxOp.getLhs(), 0))
sectionUb = maxOp.getRhs();
}
sectionUb = stripConverts(sectionUb);
mlir::Value iv = loop.getInductionVar();
std::optional<int64_t> extent;
loop.getBody()->walk([&](fir::ArrayCoorOp coor) {
if (std::optional<int64_t> coorExtent =
getLeadingExtentIndexedBy(coor, iv, sectionUb))
extent = extent ? std::min(*extent, *coorExtent) : *coorExtent;
});
if (!extent || *extent <= 1 || *extent > maxExtent)
return std::nullopt;
return extent;
}
/// Replace \p loop with a chain of guarded copies, one per entry of
/// \p versions, each with its upper bound replaced by the constant trip
/// count, and the original loop as the final else arm:
///
/// if (c0) { loop with ub := tc0 }
/// else if (c1) { loop with ub := tc1 }
/// else { original loop }
///
/// The conditions and trip counts must dominate \p loop. Loop results are
/// forwarded through fir.result.
static void versionLoop(mlir::OpBuilder &builder, fir::DoLoopOp loop,
llvm::ArrayRef<LoopVersion> versions) {
assert(!versions.empty() && "nothing to version on");
mlir::Location loc = loop.getLoc();
mlir::TypeRange resultTypes = loop->getResultTypes();
bool hasResults = !resultTypes.empty();
builder.setInsertionPoint(loop);
fir::IfOp outerIf;
for (const LoopVersion &version : versions) {
auto ifOp = fir::IfOp::create(builder, loc, resultTypes, version.condition,
/*withElse=*/true);
if (!outerIf) {
outerIf = ifOp;
} else if (hasResults) {
// Terminate the enclosing else arm with this nested if's results.
fir::ResultOp::create(builder, loc, ifOp.getResults());
}
builder.setInsertionPointToStart(&ifOp.getThenRegion().front());
auto fastLoop = mlir::cast<fir::DoLoopOp>(builder.clone(*loop));
fastLoop.getUpperBoundMutable().assign(version.tripCount);
if (hasResults)
fir::ResultOp::create(builder, loc, fastLoop.getResults());
builder.setInsertionPointToStart(&ifOp.getElseRegion().front());
}
// The insertion point is now at the start of the innermost else arm. Move
// the original loop there.
loop->replaceAllUsesWith(outerIf);
loop->remove();
builder.insert(loop);
if (hasResults)
fir::ResultOp::create(builder, loc, loop.getResults());
}
/// Device code may contain convergent operations which must not be duplicated
/// under a condition that can differ between threads. Three things make a loop
/// device code: a device ancestor (cuf.kernel, gpu.launch, acc compute region,
/// omp.target), a device function (cuf.proc_attr, acc routine, omp
/// declare_target nohost), or a device module (omp.is_target_device, where
/// every remaining function is compiled for the target). attributes(host,
/// device) and unspecialized acc routines count: their device copy is split
/// off after this pass runs, so the one func.func here would be versioned for
/// both.
static bool isNoHostDeclareTarget(mlir::func::FuncOp func) {
auto iface = mlir::dyn_cast<mlir::omp::DeclareTargetInterface>(*func);
if (!iface)
return false;
mlir::omp::DeclareTargetAttr attr = iface.getDeclareTarget();
return attr &&
attr.getDeviceType() == mlir::omp::DeclareTargetDeviceType::nohost;
}
static bool runsOnDevice(fir::DoLoopOp loop) {
if (cuf::isExecutingOnDevice(loop) ||
loop->getParentOfType<mlir::omp::TargetOp>())
return true;
auto func = loop->getParentOfType<mlir::func::FuncOp>();
if (func && (cuf::isDeviceProcedure(func) || mlir::acc::isAccRoutine(func) ||
isNoHostDeclareTarget(func)))
return true;
auto offloadMod = loop->getParentOfType<mlir::omp::OffloadModuleInterface>();
return offloadMod && offloadMod.getIsTargetDevice();
}
/// Innermost means no nested loop of any kind. Loops containing OpenMP or
/// OpenACC operations are also excluded.
static bool isVersionableInnermostLoop(fir::DoLoopOp loop) {
return !loop.getBody()
->walk([](mlir::Operation *op) {
if (mlir::isa<mlir::LoopLikeOpInterface>(op) ||
mlir::isa_and_present<mlir::omp::OpenMPDialect,
mlir::acc::OpenACCDialect>(
op->getDialect()))
return mlir::WalkResult::interrupt();
return mlir::WalkResult::advance();
})
.wasInterrupted();
}
void LoopTripCountVersioningPass::runOnOperation() {
if (maxSectionExtent == 0)
return;
LLVM_DEBUG(llvm::dbgs() << "=== Begin " DEBUG_TYPE " ===\n");
mlir::func::FuncOp func = getOperation();
LLVM_DEBUG(llvm::dbgs() << "Func-name:" << func.getSymName() << "\n");
// Collect candidates before transforming anything. Candidates are innermost
// loops, so no candidate contains another and the transformations are
// independent.
llvm::SmallVector<SectionExtentCandidate> candidates;
func.walk([&](fir::DoLoopOp loop) {
if (runsOnDevice(loop) || !isVersionableInnermostLoop(loop))
return;
++NumLoopsExamined;
std::optional<int64_t> extent = getSectionExtent(loop, maxSectionExtent);
if (!extent)
return;
unsigned bodyOps = 0;
loop.getBody()->walk([&](mlir::Operation *) { ++bodyOps; });
uint64_t unrolledOps = static_cast<uint64_t>(*extent) * bodyOps;
bool tooLarge = maxUnrolledOps != 0 && unrolledOps > maxUnrolledOps;
LLVM_DEBUG({
// One write per line so parallel builds do not interleave mid-line.
std::string msg;
llvm::raw_string_ostream os(msg);
os << "Section extent candidate (N=" << *extent
<< ", body ops=" << bodyOps << ") at " << loop.getLoc()
<< (tooLarge ? " skipped: too large to unroll\n" : "\n");
llvm::dbgs() << msg;
});
if (tooLarge) {
++NumLoopsTooLarge;
return;
}
candidates.push_back({loop, *extent});
});
if (candidates.empty()) {
LLVM_DEBUG(llvm::dbgs()
<< "No loops to transform.\n=== End " DEBUG_TYPE " ===\n");
return;
}
mlir::OpBuilder builder(func.getContext());
for (const SectionExtentCandidate &candidate : candidates) {
fir::DoLoopOp loop = candidate.loop;
mlir::Location loc = loop.getLoc();
mlir::Value ub = loop.getUpperBound();
// Define the guard values before the loop so they dominate the fir.if.
builder.setInsertionPoint(loop);
mlir::Value extentCst = mlir::arith::ConstantOp::create(
builder, loc, ub.getType(),
builder.getIntegerAttr(ub.getType(), candidate.extent));
mlir::Value isDeclaredExtent = mlir::arith::CmpIOp::create(
builder, loc, mlir::arith::CmpIPredicate::eq, ub, extentCst);
versionLoop(builder, loop, {{isDeclaredExtent, extentCst}});
++NumLoopsVersioned;
}
LLVM_DEBUG(llvm::dbgs() << "Func after transformation:\n");
LLVM_DEBUG(func->dump());
LLVM_DEBUG(llvm::dbgs() << "=== End " DEBUG_TYPE " ===\n");
}