[llvm] [X86] Prefer SIMD min/max/abs for scalars when staying in XMM domain (PR #210654)

Simon Pilgrim via llvm-commits llvm-commits at lists.llvm.org
Thu Jul 23 03:26:10 PDT 2026


================
@@ -54649,6 +54649,143 @@ static SDValue narrowBitOpRMW(StoreSDNode *St, const SDLoc &DL,
   return NewStore;
 }
 
+/// True if V is a plain (non-extending) load of VT. Don't peek through
+/// bitcasts: that could pull a float-domain value into an integer vector.
+static bool isPlainScalarLoad(SDValue V, EVT VT) {
+  auto *Ld = dyn_cast<LoadSDNode>(V);
+  if (!Ld || Ld->getExtensionType() != ISD::NON_EXTLOAD)
+    return false;
+  return Ld->getMemoryVT() == VT;
+}
+
+/// Return the widened vector type to use for a scalar min/max/abs SIMD
+/// combine of VT, or std::nullopt if VT/subtarget isn't supported.
+/// i32 needs SSE4.1 (min/max) / SSSE3 (abs); i64 needs AVX512F+VLX (movq
+/// covers the mem->XMM move; VLX avoids zmm widening + vzeroupper); i16 needs
+/// AVX512FP16, since VMOVW is the only instruction that folds a 16-bit scalar
+/// load directly into XMM without a GPR round trip.
+static std::optional<MVT>
+getScalarSIMDVecVT(EVT VT, const X86Subtarget &Subtarget, bool IsAbs) {
+  if (!VT.isSimple())
+    return std::nullopt;
+  switch (VT.getSimpleVT().SimpleTy) {
+  case MVT::i32:
+    if (IsAbs ? Subtarget.hasSSSE3() : Subtarget.hasSSE41())
+      return MVT::v4i32;
+    break;
+  case MVT::i64:
+    // Without VLX, isel widens v2i64 min/max/abs to zmm and emits
+    // vzeroupper — worse than cmp+cmov for a scalar memory op.
+    if (Subtarget.hasAVX512() && Subtarget.hasVLX())
+      return MVT::v2i64;
+    break;
+  case MVT::i16:
+    if (Subtarget.hasFP16())
+      return MVT::v8i16;
+    break;
+  default:
+    break;
+  }
+  return std::nullopt;
+}
+
+/// Fold store(abs/min/max(load…)) of scalar i16/i32/i64 into a vector
+/// abs/min/max, which ISel folds to movd/movq/vmovw + PABS/PMIN/PMAX. This
+/// keeps memory-bound sort2 in the XMM domain, matching GCC, without
+/// introducing GPR↔XMM domain crosses for values that live in GPRs.
+///
+/// Only plain loads are accepted as sources: EXTRACT_VECTOR_ELT typically
+/// materializes in a GPR (movd/pextr), so re-widening would create a domain
+/// cross. Constants and GPR operands are likewise rejected. Relies on scalar
+/// min/max remaining Expand so shouldScalarizeBinop does not undo the
+/// extract(vector_binop) form we create. Store-only by design — other sinks
+/// (e.g. insert_vector_elt) belong in mid-end vectorization, not the backend.
+static SDValue combineScalarMinMaxAbsStore(StoreSDNode *St, const SDLoc &DL,
+                                           SelectionDAG &DAG,
+                                           const X86Subtarget &Subtarget) {
+  if (!ISD::isNormalStore(St) || St->isTruncatingStore())
+    return SDValue();
+
+  SDValue StoredVal = St->getValue();
+  if (!StoredVal.hasOneUse())
+    return SDValue();
+
+  unsigned Opc = StoredVal.getOpcode();
+  bool IsAbs = Opc == ISD::ABS;
+  bool IsMinMax = Opc == ISD::SMIN || Opc == ISD::SMAX || Opc == ISD::UMIN ||
+                  Opc == ISD::UMAX;
+  if (!IsAbs && !IsMinMax)
+    return SDValue();
+
+  EVT VT = StoredVal.getValueType();
+  std::optional<MVT> VecVTOpt = getScalarSIMDVecVT(VT, Subtarget, IsAbs);
+  if (!VecVTOpt)
+    return SDValue();
+  MVT VecVT = *VecVTOpt;
+
+  const Function &F = DAG.getMachineFunction().getFunction();
+  if (F.hasFnAttribute(Attribute::NoImplicitFloat) || Subtarget.useSoftFloat())
+    return SDValue();
+  if (F.hasOptSize())
+    return SDValue();
+
+  if (IsAbs) {
+    SDValue Op0 = StoredVal.getOperand(0);
+    if (!isPlainScalarLoad(Op0, VT) || !Op0.hasOneUse())
+      return SDValue();
+
+    SDValue V0 = DAG.getNode(ISD::SCALAR_TO_VECTOR, DL, VecVT, Op0);
+    SDValue Vec = DAG.getNode(ISD::ABS, DL, VecVT, V0);
+    SDValue NewVal = DAG.getNode(ISD::EXTRACT_VECTOR_ELT, DL, VT, Vec,
+                                 DAG.getVectorIdxConstant(0, DL));
+    return DAG.getStore(St->getChain(), DL, NewVal, St->getBasePtr(),
+                        St->getPointerInfo(), St->getBaseAlign(),
+                        St->getMemOperand()->getFlags());
+  }
+
+  SDValue Op0 = StoredVal.getOperand(0);
+  SDValue Op1 = StoredVal.getOperand(1);
+
+  // Both operands must be plain loads — the only XMM-native source that
+  // doesn't already require a GPR↔XMM move.
+  if (!isPlainScalarLoad(Op0, VT) || !isPlainScalarLoad(Op1, VT))
+    return SDValue();
+
+  // Each load must only feed scalar min/max nodes, or a SCALAR_TO_VECTOR from
+  // a sibling min/max already combined this turn (CSE shares the S2V). This
+  // lets paired smin+smax of the same loads both convert. A GPR consumer
+  // would force the load into a GPR and block memory folding.
+  auto isScalarMinMax = [](SDNode *U) {
+    unsigned O = U->getOpcode();
+    return (O == ISD::SMIN || O == ISD::SMAX || O == ISD::UMIN ||
+            O == ISD::UMAX) &&
+           !U->getValueType(0).isVector();
+  };
+  auto checkLoadUses = [&](SDValue Ld) {
+    for (SDUse &Use : Ld->uses()) {
+      if (Use.getResNo() != Ld.getResNo())
+        continue;
+      SDNode *User = Use.getUser();
+      if (User->getOpcode() == ISD::SCALAR_TO_VECTOR)
+        continue;
+      if (!isScalarMinMax(User))
+        return false;
+    }
+    return true;
+  };
+  if (!checkLoadUses(Op0) || !checkLoadUses(Op1))
+    return SDValue();
+
+  SDValue V0 = DAG.getNode(ISD::SCALAR_TO_VECTOR, DL, VecVT, Op0);
+  SDValue V1 = DAG.getNode(ISD::SCALAR_TO_VECTOR, DL, VecVT, Op1);
+  SDValue Vec = DAG.getNode(Opc, DL, VecVT, V0, V1);
+  SDValue NewVal = DAG.getNode(ISD::EXTRACT_VECTOR_ELT, DL, VT, Vec,
----------------
RKSimon wrote:

getExtractVectorElt

https://github.com/llvm/llvm-project/pull/210654


More information about the llvm-commits mailing list