[llvm] [AArch64] Lower cttz(bitcast <Nxi1> to iN) with shrn-based compressed movemask (PR #199081)
Adam Scott via llvm-commits
llvm-commits at lists.llvm.org
Sun Jun 28 19:02:22 PDT 2026
================
@@ -26247,6 +26248,133 @@ static SDValue vectorToScalarBitmask(SDNode *N, SelectionDAG &DAG) {
return DAG.getNode(ISD::VECREDUCE_ADD, DL, ResultVT, RepresentativeBits);
}
+// When taking the trailing-zero count of a bitcast from <N x i1> to scalar
+// iN, we can pack each lane into a small bit-slot of a 64-bit scalar
+// with shrn (for byte lanes) or xtn (for wider lanes) then run a
+// scalar cttz on the result.
+static SDValue performCTTZCombine(SDNode *N,
+ TargetLowering::DAGCombinerInfo &DCI,
+ SelectionDAG &DAG) {
+ // Run before vectorToScalarBitmask() expands it.
+ if (!DCI.isBeforeLegalize())
+ return SDValue();
+
+ EVT VT = N->getValueType(0);
+ if (!VT.isScalarInteger())
+ return SDValue();
+ SDValue BitcastNode = N->getOperand(0);
+ if (BitcastNode.getOpcode() != ISD::BITCAST)
+ return SDValue();
+ SDValue Predicate = BitcastNode.getOperand(0);
+ EVT PredVT = Predicate.getValueType();
+ if (!PredVT.isFixedLengthVector() || PredVT.getVectorElementType() != MVT::i1)
+ return SDValue();
+
+ unsigned NumLanes = PredVT.getVectorNumElements();
+ if (!isPowerOf2_32(NumLanes) || NumLanes < 2 || NumLanes > 32)
+ return SDValue();
+
+ constexpr unsigned DRegBits = 64;
+ constexpr unsigned QRegBits = 128;
+ constexpr unsigned MinLaneBits = 8;
+
+ // Pick a lane width to sext the predicate to. Prefer the original
+ // compare input width if we can find it and it fits in at most two NEON
+ // registers but otherwise pick a minimum-width type that fits.
+ EVT MaskVT = tryGetOriginalBoolVectorType(Predicate);
+ if (!MaskVT.isSimple() ||
+ MaskVT.changeVectorElementTypeToInteger().getSizeInBits() >
+ 2 * QRegBits ||
+ MaskVT.changeVectorElementTypeToInteger().getSizeInBits() < DRegBits) {
+ unsigned LaneBits = std::max(DRegBits / NumLanes, MinLaneBits);
+ MaskVT = MVT::getVectorVT(MVT::getIntegerVT(LaneBits), NumLanes);
+ }
+ MaskVT = MaskVT.changeVectorElementTypeToInteger();
+
+ unsigned MaskBits = MaskVT.getSizeInBits();
+ if (MaskBits != DRegBits && MaskBits != QRegBits && MaskBits != 2 * QRegBits)
+ return SDValue();
+
+ // Target 64 bits when lanes fit, otherwise 128.
+ unsigned TargetBits = DRegBits;
+ if (MaskBits == 2 * QRegBits && NumLanes > 16)
+ TargetBits = QRegBits;
+ unsigned CompressedBits = std::min(MaskBits, TargetBits);
+
+ SDLoc DL(N);
+ SDValue Mask = DAG.getSExtOrTrunc(Predicate, DL, MaskVT);
+
+ LLVMContext &Ctx = *DAG.getContext();
+
+ // NEON's smallest vector lane type is i8 so we can't narrow further.
+ // Instead, bitcast the mask to i16 lanes and shrn by 4 to pack each
+ // input byte into 4 bits of the output
+ auto ShrnPair = [&](SDValue V) -> SDValue {
+ EVT InVT = V.getValueType();
+ EVT OutVT = InVT.getHalfNumVectorElementsVT(Ctx);
+ EVT PairedVT = OutVT.widenIntegerVectorElementType(Ctx);
+ SDValue Paired = DAG.getNode(AArch64ISD::NVCAST, DL, PairedVT, V);
+ SDValue ShAmt = DAG.getConstant(MinLaneBits / 2, DL, PairedVT);
+ SDValue Shifted = DAG.getNode(ISD::SRL, DL, PairedVT, Paired, ShAmt);
+ return DAG.getNode(ISD::TRUNCATE, DL, OutVT, Shifted);
+ };
+
+ // Trailing-zero count of the compressed result as an i64.
+ auto CompressedCttz = [&](SDValue V) -> SDValue {
+ if (V.getValueType().getSizeInBits() == DRegBits) {
+ SDValue Nv = DAG.getNode(AArch64ISD::NVCAST, DL, MVT::v1i64, V);
+ SDValue Scalar = DAG.getNode(ISD::EXTRACT_VECTOR_ELT, DL, MVT::i64, Nv,
+ DAG.getConstant(0, DL, MVT::i64));
+ return DAG.getNode(ISD::CTTZ, DL, MVT::i64, Scalar);
+ }
+ SDValue Compressed = DAG.getBitcast(MVT::i128, V);
+ // BITCAST puts lane 0 at the high end of the scalar on BE (unlike NVCAST
+ // above), so use clz instead of cttz to find the first match
+ unsigned Op = DAG.getDataLayout().isLittleEndian() ? ISD::CTTZ : ISD::CTLZ;
+ SDValue Ctz = DAG.getNode(Op, DL, MVT::i128, Compressed);
+ return DAG.getNode(ISD::TRUNCATE, DL, MVT::i64, Ctz);
+ };
+
+ // Narrow toward CompressedBits. shrn-pair is only used once since it
+ // leaves the i8 lanes already packed.
+ SDValue Cur = Mask;
+ bool UsedShrnPair = false;
+ while (Cur.getValueType().getSizeInBits() > CompressedBits) {
+ EVT CurVT = Cur.getValueType();
+ unsigned CurLaneBits = CurVT.getScalarSizeInBits();
+ if (CurLaneBits == MinLaneBits) {
+ if (UsedShrnPair)
+ break;
----------------
as4230 wrote:
You're right. It should never run and if it did the result would be wrong. The reason it was there is that xtn needs the loop since narrowing wide lanes can take a few steps but the shrn only ever runs once. I had it inside the loop with that bool guard to stop a second shrn but since one shrn always lands exactly on CompressedBits the loop never comes back around to it anyways. I pulled the shrn out into an if after the loop. Now it can only ever run once so the bool guard and the break are both gone.
https://github.com/llvm/llvm-project/pull/199081
More information about the llvm-commits
mailing list