blob: c52c09e71aa89d949af7b69c045640ffd5ecddb9 [file]
//===- AArch64PredicateAsCounterLoopRewrites.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
//
//===----------------------------------------------------------------------===//
//
// Rewrites IR for loop-carried wide masks that can be represented as
// predicate-as-counter values. This applies when the mask is used by load/store
// operations that can be mapped to multi-vector instructions (with +sve2p1).
//
// For example, a loop like:
//
// entry:
// %step = vscale x 64
// %mask.entry = @get.active.lane.mask(0, %n)
//
// loop:
// %iv = phi i64 [0, entry], [%iv.next, loop]
// %mask = phi <vscale x 64 x i1> [%mask.entry, entry],
// [%mask.next, loop]
//
// %load = load <vscale x 64 x i8> %src[%iv], %mask
// store <vscale x 64 x i8> %load, %dst[%iv], %mask
//
// %iv.next = %iv + %step
// %mask.next = @get.active.lane.mask(%iv.next, %n)
// br first.active(%mask.next), loop, exit
//
// Could be rewritten to:
//
// entry:
// %step = vscale x 64
// %mask.entry = @whilelo.c8(0, %n, VLx4)
//
// loop:
// %iv = phi i64 [0, entry], [%iv.next, loop]
// %mask = phi target("aarch64.svcount") [%mask.entry, entry],
// [%mask.next, loop]
//
// %load = @ld1.pn.x4 <4 x <vscale x 16 x i8>> %src[%iv], %mask
// @st1.pn.x4 <4 x <vscale x 16 x i8>> %load, %dest[%iv], %mask
//
// %iv.next = %iv + %step
// %mask.next = @whilelo.c8(%iv.next, %n, VLx4)
// br first.active(@pext(%mask.next, 0)), loop, exit
//
// This replaces the `get.active.lane.mask` intrinsics with AArch64
// predicate-as-counter `whilelo` intrinsics and updates the mask phi to use the
// `aarch64.svcount` target type. Within the loop, load/store users are mapped
// to multi-vector load/store intrinsics where possible. Users that cannot be
// mapped to multi-vector instructions materialize vector masks using the `pext`
// intrinsic (which extracts vector predicates from a predicate-as-counter).
//
// This pass may be a temporary solution that is removed if we gain support
// for target-specific VPlan transforms in the loop vectorizer.
//
//===----------------------------------------------------------------------===//
#include "AArch64.h"
#include "AArch64Subtarget.h"
#include "AArch64TargetMachine.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/Statistic.h"
#include "llvm/Analysis/LoopInfo.h"
#include "llvm/Analysis/LoopPass.h"
#include "llvm/CodeGen/TargetPassConfig.h"
#include "llvm/IR/Attributes.h"
#include "llvm/IR/DataLayout.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/IntrinsicInst.h"
#include "llvm/IR/Intrinsics.h"
#include "llvm/IR/IntrinsicsAArch64.h"
#include "llvm/IR/Module.h"
#include "llvm/InitializePasses.h"
#include "llvm/Pass.h"
#include "llvm/Support/Debug.h"
#include "llvm/Transforms/Utils.h"
#include "llvm/Transforms/Utils/Local.h"
#include <optional>
using namespace llvm;
#define DEBUG_TYPE "aarch64-pn-loop-rewrites"
namespace {
STATISTIC(LoopsRewritten, "Number of loops rewritten");
struct MaskRewriteCandidate {
/// The mask phi node (used by masked operations within the loop).
PHINode *MaskPhi = nullptr;
/// The initial value for the mask (incoming value from the preheader).
IntrinsicInst *StartMask = nullptr;
/// The updated value for the mask (incoming value from the loop latch).
IntrinsicInst *NextMask = nullptr;
/// The multi-vector scale for the predicate-as-counter (2 or 4).
unsigned VectorScale = 0;
/// The element size (in bits) for the predicate-as-counter.
unsigned ElementSizeInBits = 0;
};
static void logLoopBailout(const Loop &L, const Twine &Reason) {
LLVM_DEBUG({
dbgs() << "PN loop rewrite: skipping loop with header ";
L.getHeader()->printAsOperand(dbgs(), /*PrintType=*/false);
dbgs() << ": " << Reason << "\n";
});
}
static void logMatchFailure(const PHINode &Phi, const Twine &Reason) {
LLVM_DEBUG({
dbgs() << "PN loop rewrite: failed to match mask phi ";
Phi.printAsOperand(dbgs(), /*PrintType=*/false);
dbgs() << ": " << Reason << "\n";
});
}
/// Returns the scalar size in bits for a type according to the data layout.
static unsigned getScalarSizeInBits(const DataLayout &DL, Type *Ty) {
return DL.getTypeSizeInBits(Ty->getScalarType()).getFixedValue();
}
/// Returns the SVE element count for \p ElementSizeInBits.
static ElementCount getSVEElementCount(unsigned ElementSizeInBits) {
return ElementCount::getScalable(AArch64::SVEBitsPerBlock /
ElementSizeInBits);
}
/// Returns the largest scalar access size of masked loads/stores in loop \p L
/// where \p MaskPhi is used as the mask. TODO: This heuristic may need
/// refinement based on the frequency of different access sizes.
static unsigned getLargestMaskedMemAccessSizeInBits(const Loop &L,
PHINode &MaskPhi) {
const DataLayout &DL = MaskPhi.getModule()->getDataLayout();
unsigned LargestAccessSizeInBits = 0;
for (User *U : MaskPhi.users()) {
auto *II = dyn_cast<IntrinsicInst>(U);
if (!II || !L.contains(II))
continue;
Intrinsic::ID IID = II->getIntrinsicID();
if (IID != Intrinsic::masked_load && IID != Intrinsic::masked_store)
continue;
unsigned MaskOpIdx = IID == Intrinsic::masked_load ? 1 : 2;
if (II->getArgOperand(MaskOpIdx) != &MaskPhi)
continue;
unsigned AccessSizeInBits = getScalarSizeInBits(DL, II->getAccessType());
if (AccessSizeInBits > LargestAccessSizeInBits)
LargestAccessSizeInBits = AccessSizeInBits;
}
return LargestAccessSizeInBits;
}
/// Returns the predicate-as-counter whilelo intrinsic ID for
/// \p ElementSizeInBits.
static Intrinsic::ID getWhileLOIntrinsic(unsigned ElementSizeInBits) {
switch (ElementSizeInBits) {
case 8:
return Intrinsic::aarch64_sve_whilelo_c8;
case 16:
return Intrinsic::aarch64_sve_whilelo_c16;
case 32:
return Intrinsic::aarch64_sve_whilelo_c32;
case 64:
return Intrinsic::aarch64_sve_whilelo_c64;
default:
llvm_unreachable("unsupported predicate-as-counter element size");
}
}
/// Expands the predicate-as-counter mask \p Count into a wide vector mask.
/// Masks are extracted from the counter using the paired pext intrinsics,
/// then concatenated to form the wide mask value.
static Value *buildWideMask(IRBuilder<> &Builder, const MaskRewriteCandidate &C,
Value *Count) {
ElementCount LegalEC = getSVEElementCount(C.ElementSizeInBits);
Type *LegalMaskTy = VectorType::get(Builder.getInt1Ty(), LegalEC);
Value *WideMask = PoisonValue::get(C.MaskPhi->getType());
for (unsigned PairOffset = 0; PairOffset != C.VectorScale / 2; ++PairOffset) {
auto *Pair =
Builder.CreateIntrinsic(Intrinsic::aarch64_sve_pext_x2, {LegalMaskTy},
{Count, Builder.getInt32(PairOffset)},
/*FMFSource=*/{}, "pn.pext.pair");
for (unsigned SliceInPair = 0; SliceInPair != 2; ++SliceInPair) {
Value *Part = Builder.CreateExtractValue(Pair, SliceInPair, "pn.pext");
unsigned Slice = PairOffset * 2 + SliceInPair;
WideMask = Builder.CreateInsertVector(
C.MaskPhi->getType(), WideMask, Part,
Slice * LegalEC.getKnownMinValue(), "pn.mask");
}
}
return WideMask;
}
/// Creates a predicate-as-counter whilelo for \p ElementSizeInBits between
/// \p Start and \p End with multi-vector \p VectorScale (2 or 4).
static Value *createWhileLO(IRBuilder<> &Builder, unsigned ElementSizeInBits,
Value *Start, Value *End, unsigned VectorScale) {
if (Start->getType()->getIntegerBitWidth() < 64) {
Start = Builder.CreateZExt(Start, Builder.getInt64Ty());
End = Builder.CreateZExt(End, Builder.getInt64Ty());
}
Intrinsic::ID WhileLO = getWhileLOIntrinsic(ElementSizeInBits);
return Builder.CreateIntrinsic(WhileLO,
{Start, End, Builder.getInt32(VectorScale)},
/*FMFSource=*/{}, "pn.mask");
}
class AArch64PredicateAsCounterLoopRewrites : public LoopPass {
public:
static char ID;
AArch64PredicateAsCounterLoopRewrites() : LoopPass(ID) {}
void getAnalysisUsage(AnalysisUsage &AU) const override {
AU.addRequired<TargetPassConfig>();
// Require loop simplify to ensure loops have a preheader.
AU.addRequiredID(LoopSimplifyID);
AU.addPreservedID(LoopSimplifyID);
AU.setPreservesCFG();
}
bool runOnLoop(Loop *L, LPPassManager &) override;
private:
std::optional<MaskRewriteCandidate> matchMaskPhi(Loop &L, PHINode &Phi) const;
bool rewriteCandidate(const MaskRewriteCandidate &C, Loop &L) const;
};
} // end anonymous namespace
char AArch64PredicateAsCounterLoopRewrites::ID = 0;
INITIALIZE_PASS_BEGIN(AArch64PredicateAsCounterLoopRewrites, DEBUG_TYPE,
"AArch64 Predicate As Counter Loop Rewrites", false,
false)
INITIALIZE_PASS_DEPENDENCY(TargetPassConfig)
INITIALIZE_PASS_DEPENDENCY(LoopSimplify)
INITIALIZE_PASS_END(AArch64PredicateAsCounterLoopRewrites, DEBUG_TYPE,
"AArch64 Predicate As Counter Loop Rewrites", false, false)
Pass *llvm::createAArch64PredicateAsCounterLoopRewritesPass() {
return new AArch64PredicateAsCounterLoopRewrites();
}
bool AArch64PredicateAsCounterLoopRewrites::runOnLoop(Loop *L,
LPPassManager &) {
if (skipLoop(L)) {
logLoopBailout(*L, "skipLoop requested the loop to be skipped");
return false;
}
Function &F = *L->getHeader()->getParent();
auto &TPC = getAnalysis<TargetPassConfig>();
const AArch64Subtarget *ST =
TPC.getTM<AArch64TargetMachine>().getSubtargetImpl(F);
if (!ST->hasSVE2p1() && !(ST->hasSME2() && ST->isStreaming())) {
logLoopBailout(*L, "neither SVE2.1 nor SME2 is available");
return false;
}
if (!L->getLoopPreheader()) {
logLoopBailout(*L, "loop has no preheader");
return false;
}
if (!L->getLoopLatch()) {
logLoopBailout(*L, "loop has no latch");
return false;
}
bool Changed = false;
BasicBlock *Header = L->getHeader();
for (PHINode &Phi : make_early_inc_range(Header->phis())) {
if (std::optional<MaskRewriteCandidate> Candidate = matchMaskPhi(*L, Phi))
Changed |= rewriteCandidate(*Candidate, *L);
}
if (Changed)
++LoopsRewritten;
return Changed;
}
static IntrinsicInst *getGetActiveLaneMask(Value *V) {
auto *II = dyn_cast<IntrinsicInst>(V);
return II && II->getIntrinsicID() == Intrinsic::get_active_lane_mask
? II
: nullptr;
}
std::optional<MaskRewriteCandidate>
AArch64PredicateAsCounterLoopRewrites::matchMaskPhi(Loop &L,
PHINode &Phi) const {
auto *PhiTy = dyn_cast<ScalableVectorType>(Phi.getType());
if (!PhiTy || !PhiTy->getElementType()->isIntegerTy(1))
return std::nullopt;
if (Phi.getNumIncomingValues() != 2) {
logMatchFailure(Phi, Twine("phi has ") + Twine(Phi.getNumIncomingValues()) +
" incoming values; expected 2");
return std::nullopt;
}
Value *StartValue = Phi.getIncomingValueForBlock(L.getLoopPreheader());
Value *NextValue = Phi.getIncomingValueForBlock(L.getLoopLatch());
IntrinsicInst *StartMask = getGetActiveLaneMask(StartValue);
IntrinsicInst *NextMask = getGetActiveLaneMask(NextValue);
if (!StartMask) {
logMatchFailure(Phi,
"preheader incoming value is not get_active_lane_mask");
return std::nullopt;
}
if (!NextMask) {
logMatchFailure(Phi, "latch incoming value is not get_active_lane_mask");
return std::nullopt;
}
unsigned WideMaskElements = PhiTy->getMinNumElements();
if (!isPowerOf2_32(WideMaskElements)) {
logMatchFailure(Phi,
Twine("wide mask element count is not a power of 2: ") +
Twine(WideMaskElements));
return std::nullopt;
}
if (StartMask->getArgOperand(0)->getType()->getIntegerBitWidth() > 64) {
logMatchFailure(Phi, "start mask induction operand is wider than i64");
return std::nullopt;
}
if (NextMask->getArgOperand(0)->getType()->getIntegerBitWidth() > 64) {
logMatchFailure(Phi, "next mask induction operand is wider than i64");
return std::nullopt;
}
unsigned PreferredMaskElementSizeInBits =
getLargestMaskedMemAccessSizeInBits(L, Phi);
if (!is_contained({8u, 16u, 32u, 64u}, PreferredMaskElementSizeInBits)) {
logMatchFailure(Phi, Twine("unsupported element size in bits: ") +
Twine(PreferredMaskElementSizeInBits));
return std::nullopt;
}
unsigned SVEMaskElements =
AArch64::SVEBitsPerBlock / PreferredMaskElementSizeInBits;
if (WideMaskElements <= SVEMaskElements) {
logMatchFailure(Phi, Twine("wide mask element count ") +
Twine(WideMaskElements) +
" is not wider than the legal mask width " +
Twine(SVEMaskElements));
return std::nullopt;
}
unsigned VectorScale = WideMaskElements / SVEMaskElements;
if (VectorScale != 2 && VectorScale != 4) {
logMatchFailure(Phi, Twine("unsupported predicate-as-counter scale: ") +
Twine(VectorScale));
return std::nullopt;
}
return MaskRewriteCandidate{&Phi, StartMask, NextMask, VectorScale,
PreferredMaskElementSizeInBits};
}
bool AArch64PredicateAsCounterLoopRewrites::rewriteCandidate(
const MaskRewriteCandidate &C, Loop &L) const {
IRBuilder<> Builder(C.StartMask);
Value *NewStart =
createWhileLO(Builder, C.ElementSizeInBits, C.StartMask->getArgOperand(0),
C.StartMask->getArgOperand(1), C.VectorScale);
Builder.SetInsertPoint(C.NextMask);
Value *NewNext =
createWhileLO(Builder, C.ElementSizeInBits, C.NextMask->getArgOperand(0),
C.NextMask->getArgOperand(1), C.VectorScale);
Builder.SetInsertPoint(C.MaskPhi);
auto *NewPhi =
Builder.CreatePHI(NewStart->getType(), 2, C.MaskPhi->getName() + ".pn");
NewPhi->addIncoming(NewStart, L.getLoopPreheader());
NewPhi->addIncoming(NewNext, L.getLoopLatch());
auto RewriteUses = [&](Instruction *OldMask, Value *Count,
function_ref<bool(Use &U)> Predicate = nullptr) {
SmallVector<Use *, 8> UsesToRewrite;
for (Use &U : OldMask->uses()) {
if (!Predicate || Predicate(U))
UsesToRewrite.push_back(&U);
}
Value *WideMask = nullptr;
for (Use *U : UsesToRewrite) {
if (!WideMask) {
BasicBlock::iterator InsertPt = OldMask->getIterator();
if (isa<PHINode>(OldMask))
InsertPt = OldMask->getParent()->getFirstNonPHIIt();
Builder.SetInsertPoint(InsertPt);
Builder.SetCurrentDebugLocation(OldMask->getDebugLoc());
WideMask = buildWideMask(Builder, C, Count);
}
U->set(WideMask);
}
};
// For the start/next mask, we only replace non-phi in-loop users. Due to CSE,
// these masks could be used by other loops, and replacing them with
// predicate-as-counter masks could prevent matching those loops.
auto IsNonPhiUseInLoop = [&](Use &U) {
auto *UseInst = cast<Instruction>(U.getUser());
return !isa<PHINode>(UseInst) && L.contains(UseInst);
};
RewriteUses(C.MaskPhi, NewPhi);
RewriteUses(C.StartMask, NewStart, IsNonPhiUseInLoop);
RewriteUses(C.NextMask, NewNext, IsNonPhiUseInLoop);
RecursivelyDeleteTriviallyDeadInstructions(C.MaskPhi);
return true;
}