blob: e1df83d9994b02c65a23441e552ab3705ebbf896 [file] [edit]
//===-- NVPTXPromoteParamAlign.cpp - Promote .param alignment ------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// Increase the .param-space alignment of NVPTX arguments and return values so
// their loads and stores can be vectorized. On every defined function:
//
// 1. Give each byval param an explicit ABI `align` for its pointee type
// (capped at the PTX max). byval already implies this, but the alignment
// is otherwise invisible to IR alignment analyses.
// 2. For a local function whose every use is a type-compatible direct call,
// we control all call sites and raise aggregate/byval param and return
// alignment to at least 16 (for 128-bit vectorization). This is recorded
// as `stackalign` and mirrored onto the calls.
// 3. Propagate the result onto byval loads at a known offset, since `align`
// and `stackalign` aren't both picked up by IR alignment analyses.
//
// (2) runs before (1) so byval `align` still matches between caller and callee
// while stackalign is mirrored onto the calls.
//
//===----------------------------------------------------------------------===//
#include "NVPTX.h"
#include "NVPTXUtilities.h"
#include "llvm/ADT/Sequence.h"
#include "llvm/IR/Attributes.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/Module.h"
#include "llvm/InitializePasses.h"
#include "llvm/Pass.h"
#include "llvm/Support/Debug.h"
#include <optional>
#include <queue>
#define DEBUG_TYPE "nvptx-promote-param-align"
using namespace llvm;
namespace {
class NVPTXPromoteParamAlignLegacyPass : public ModulePass {
bool runOnModule(Module &M) override;
public:
static char ID;
NVPTXPromoteParamAlignLegacyPass() : ModulePass(ID) {}
StringRef getPassName() const override {
return "Promote alignment of parameters and return values (NVPTX)";
}
};
} // namespace
char NVPTXPromoteParamAlignLegacyPass::ID = 0;
INITIALIZE_PASS(NVPTXPromoteParamAlignLegacyPass, "nvptx-promote-param-align",
"Promote alignment of parameters and return values (NVPTX)",
false, false)
// Return true if the attributes that determine an NVPTX .param slot's layout
// match.
static bool layoutAttrsMatch(AttributeSet CalleeAttrs, AttributeSet CallAttrs) {
if (CalleeAttrs.getByValType() != CallAttrs.getByValType() ||
CalleeAttrs.getStackAlignment() != CallAttrs.getStackAlignment())
return false;
// `align` only affects the layout for byval parameters.
return !CalleeAttrs.getByValType() ||
CalleeAttrs.getAlignment() == CallAttrs.getAlignment();
}
static bool callSiteMatchesCalleeABI(const CallBase &CB, const Function &F) {
const AttributeList CalleeAttrs = F.getAttributes();
const AttributeList CallAttrs = CB.getAttributes();
if (!layoutAttrsMatch(CalleeAttrs.getRetAttrs(), CallAttrs.getRetAttrs()))
return false;
return all_of(seq(F.arg_size()), [&](size_t I) {
return layoutAttrsMatch(CalleeAttrs.getParamAttrs(I),
CallAttrs.getParamAttrs(I));
});
}
// Promotable if the function is local and every use is an ABI-compatible direct
// call, so we control every call site and can raise alignment on both sides.
static bool canPromoteParamAlign(Function &F) {
if (F.isDeclaration() || !F.hasLocalLinkage())
return false;
if (F.hasAddressTaken(/*Users=*/nullptr, /*IgnoreCallbackUses=*/false,
/*IgnoreAssumeLikeCalls=*/true,
/*IgnoreLLVMUsed=*/true))
return false;
return all_of(F.users(), [&](const User *U) {
const auto *CB = dyn_cast<CallBase>(U);
if (!CB || CB->getCalledOperand() != &F)
return true;
return CB->getFunctionType() == F.getFunctionType() &&
callSiteMatchesCalleeABI(*CB, F);
});
}
// Raise the alignment of every load reachable from a byval pointer at a known
// constant offset. Must stay in sync with the param load/store in LowerCall.
static bool propagateAlignmentToLoads(Value *Val, Align NewAlign,
const DataLayout &DL) {
struct Load {
LoadInst *Inst;
uint64_t Offset;
};
struct LoadContext {
Value *InitialVal;
uint64_t Offset;
};
SmallVector<Load> Loads;
std::queue<LoadContext> Worklist;
Worklist.push({Val, 0});
while (!Worklist.empty()) {
LoadContext Ctx = Worklist.front();
Worklist.pop();
for (User *CurUser : Ctx.InitialVal->users()) {
if (auto *I = dyn_cast<LoadInst>(CurUser))
Loads.push_back({I, Ctx.Offset});
else if (isa<BitCastInst>(CurUser) || isa<AddrSpaceCastInst>(CurUser))
Worklist.push({cast<Instruction>(CurUser), Ctx.Offset});
else if (auto *I = dyn_cast<GetElementPtrInst>(CurUser)) {
APInt OffsetAccumulated =
APInt::getZero(DL.getIndexTypeSizeInBits(I->getType()));
if (!I->accumulateConstantOffset(DL, OffsetAccumulated))
continue;
uint64_t OffsetLimit = -1;
uint64_t Offset = OffsetAccumulated.getLimitedValue(OffsetLimit);
assert(Offset != OffsetLimit && "Expect Offset less than UINT64_MAX");
Worklist.push({I, Ctx.Offset + Offset});
}
}
}
bool Changed = false;
for (Load &CurLoad : Loads) {
Align NewLoadAlign = commonAlignment(NewAlign, CurLoad.Offset);
if (NewLoadAlign > CurLoad.Inst->getAlign()) {
CurLoad.Inst->setAlignment(NewLoadAlign);
Changed = true;
}
}
return Changed;
}
// Bump an alignment up to at least 16 (for 128-bit vectorization), or nullopt
// if it's already large enough.
static MaybeAlign getPromotedParamAlign(Align CurrentAlign) {
const Align PromotedAlign = std::max(CurrentAlign, Align(16));
if (PromotedAlign > CurrentAlign)
return PromotedAlign;
return std::nullopt;
}
static bool promoteParamAlign(Function &F) {
if (!canPromoteParamAlign(F))
return false;
LLVMContext &Ctx = F.getContext();
const DataLayout &DL = F.getDataLayout();
// Promoted (arg index, new alignment) pairs, to mirror onto call sites.
SmallVector<std::pair<unsigned, Align>, 8> PromotedParams;
MaybeAlign PromotedRet;
// Promote aggregate and byval parameters.
for (Argument &Arg : F.args()) {
const bool IsByVal = Arg.hasByValAttr();
Type *ArgTy = IsByVal ? Arg.getParamByValType() : Arg.getType();
if (ArgTy->isEmptyTy() || (!IsByVal && !shouldPassAsArray(ArgTy)))
continue;
// An explicit stackalign already wins at emission time, nothing to promote.
if (Arg.getParamStackAlign())
continue;
const unsigned ArgNo = Arg.getArgNo();
// `align` only applies to byval (pointer) args, not by-value aggregates.
Align CurrentAlign = getPTXParamTypeAlign(ArgTy, DL);
if (IsByVal)
CurrentAlign = std::max(CurrentAlign, Arg.getParamAlign().valueOrOne());
const MaybeAlign PromotedAlign = getPromotedParamAlign(CurrentAlign);
if (!PromotedAlign)
continue;
LLVM_DEBUG(dbgs() << "Promoting alignment of " << Arg << " to "
<< PromotedAlign->value() << '\n');
Arg.addAttr(Attribute::getWithStackAlignment(Ctx, *PromotedAlign));
PromotedParams.emplace_back(ArgNo, *PromotedAlign);
}
// Promote an aggregate return value.
Type *RetTy = F.getReturnType();
if (shouldPassAsArray(RetTy) && !RetTy->isEmptyTy() &&
!F.getAttributes().getRetStackAlignment()) {
const MaybeAlign PromotedAlign =
getPromotedParamAlign(getPTXParamTypeAlign(RetTy, DL));
if (PromotedAlign) {
F.addRetAttr(Attribute::getWithStackAlignment(Ctx, *PromotedAlign));
PromotedRet = *PromotedAlign;
}
}
if (PromotedParams.empty() && !PromotedRet)
return false;
// Mirror the promotion onto every direct call site so both sides agree on the
// .param layout. canPromoteParamAlign already verified they're
// ABI-compatible.
for (User *U : F.users()) {
auto *CB = dyn_cast<CallBase>(U);
if (!CB || CB->getCalledOperand() != &F)
continue;
for (const auto &[ArgNo, PromotedAlign] : PromotedParams)
CB->addParamAttr(ArgNo,
Attribute::getWithStackAlignment(Ctx, PromotedAlign));
if (PromotedRet)
CB->addRetAttr(Attribute::getWithStackAlignment(Ctx, *PromotedRet));
assert(callSiteMatchesCalleeABI(*CB, F) &&
"mirroring must preserve call-site/callee ABI compatibility");
}
return true;
}
// Spell out each byval parameter's ABI alignment as an explicit `align` (step 1
// above). Runs after promoteParamAlign, which needs byval `align` to still
// match between callers and callees.
static bool setByValParamABIAlign(Function &F) {
if (F.isDeclaration())
return false;
LLVMContext &Ctx = F.getContext();
const DataLayout &DL = F.getDataLayout();
bool Changed = false;
for (Argument &Arg : F.args()) {
if (!Arg.hasByValAttr())
continue;
Type *ETy = Arg.getParamByValType();
if (ETy->isEmptyTy())
continue;
const Align ABIAlign = getPTXParamTypeAlign(ETy, DL);
if (Arg.getParamAlign().valueOrOne() >= ABIAlign)
continue;
Arg.removeAttr(Attribute::Alignment);
Arg.addAttr(Attribute::getWithAlignment(Ctx, ABIAlign));
Changed = true;
}
return Changed;
}
// Propagate each byval parameter's .param alignment onto its constant-offset
// loads (step 3 above). Runs after promoteParamAlign so the promoted
// `stackalign` is included, and on every function so kernels and external
// functions benefit too.
static bool propagateByValParamLoadAlign(Function &F) {
if (F.isDeclaration())
return false;
const DataLayout &DL = F.getDataLayout();
bool Changed = false;
for (Argument &Arg : F.args()) {
if (!Arg.hasByValAttr())
continue;
Type *ETy = Arg.getParamByValType();
if (ETy->isEmptyTy())
continue;
const unsigned ParamIdx = Arg.getArgNo() + AttributeList::FirstArgIndex;
const Align ParamAlign = getDeviceByValParamAlign(&F, ETy, ParamIdx, DL);
Changed |= propagateAlignmentToLoads(&Arg, ParamAlign, DL);
}
return Changed;
}
static bool promoteParamAlignModule(Module &M) {
bool Changed = false;
for (Function &F : M) {
// Order matters (see the file header): promote, normalize `align`, then
// propagate to loads.
Changed |= promoteParamAlign(F);
Changed |= setByValParamABIAlign(F);
Changed |= propagateByValParamLoadAlign(F);
}
return Changed;
}
bool NVPTXPromoteParamAlignLegacyPass::runOnModule(Module &M) {
return promoteParamAlignModule(M);
}
ModulePass *llvm::createNVPTXPromoteParamAlignPass() {
return new NVPTXPromoteParamAlignLegacyPass();
}
PreservedAnalyses NVPTXPromoteParamAlignPass::run(Module &M,
ModuleAnalysisManager &AM) {
return promoteParamAlignModule(M) ? PreservedAnalyses::none()
: PreservedAnalyses::all();
}