[llvm] [AArch64][SelectionDAG] Fold late FCMP+CSEL patterns to FMAXNM/FMINNM during AArch64 DAG-to-DAG preprocessing. (PR #197429)

Usha Gupta via llvm-commits llvm-commits at lists.llvm.org
Mon May 18 05:00:35 PDT 2026


https://github.com/usha1830 updated https://github.com/llvm/llvm-project/pull/197429

>From 27f478cd4269364effcab3cb500fe8a60334f356 Mon Sep 17 00:00:00 2001
From: Usha Gupta <usha.gupta at arm.com>
Date: Wed, 13 May 2026 10:55:48 +0000
Subject: [PATCH 1/4] [AArch64][SelectionDAG] Add tests for folding ordered
 FCMP+CSEL patterns to FMAXNM/FMINNM

---
 .../AArch64/fmaximumnum-fminimumnum-select.ll | 178 ++++++++++++++++++
 1 file changed, 178 insertions(+)
 create mode 100644 llvm/test/CodeGen/AArch64/fmaximumnum-fminimumnum-select.ll

diff --git a/llvm/test/CodeGen/AArch64/fmaximumnum-fminimumnum-select.ll b/llvm/test/CodeGen/AArch64/fmaximumnum-fminimumnum-select.ll
new file mode 100644
index 0000000000000..6bc65a1f32189
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/fmaximumnum-fminimumnum-select.ll
@@ -0,0 +1,178 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py
+; RUN: llc --mtriple=aarch64 < %s | FileCheck %s
+
+; Test late AArch64 folding of ordered FCMP+CSEL patterns to fmaxnm/fminnm.
+; Pattern: max(mul, 0.0) then min(result, 1.0)
+; First fcmp/fcsel folds to fmaxnm, outer one does NOT fold as operands cannot
+; be proven as non-sNaN.
+define float @max_after_fmul(float %a, float %b) {
+; CHECK-LABEL: max_after_fmul:
+; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    fmul s0, s0, s1
+; CHECK-NEXT:    movi d1, #0000000000000000
+; CHECK-NEXT:    fcmp s0, #0.0
+; CHECK-NEXT:    fcsel s0, s0, s1, gt
+; CHECK-NEXT:    fmov s1, #1.00000000
+; CHECK-NEXT:    fcmp s0, s1
+; CHECK-NEXT:    fcsel s0, s0, s1, mi
+; CHECK-NEXT:    ret
+entry:
+  %mul = fmul float %a, %b
+  %cmp1 = fcmp nsz ogt float %mul, 0.000000e+00
+  %max = select i1 %cmp1, float %mul, float 0.000000e+00
+  %cmp2 = fcmp nsz olt float %max, 1.000000e+00
+  %min = select i1 %cmp2, float %max, float 1.000000e+00
+  ret float %min
+}
+
+; fadd input
+define float @max_after_fadd(float %a, float %b) {
+; CHECK-LABEL: max_after_fadd:
+; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    fadd s0, s0, s1
+; CHECK-NEXT:    movi d1, #0000000000000000
+; CHECK-NEXT:    fcmp s0, #0.0
+; CHECK-NEXT:    fcsel s0, s0, s1, gt
+; CHECK-NEXT:    ret
+entry:
+  %add = fadd float %a, %b
+  %cmp = fcmp nsz ogt float %add, 0.000000e+00
+  %max = select i1 %cmp, float %add, float 0.000000e+00
+  ret float %max
+}
+
+; Double precision
+define double @max_after_fmul_f64(double %a, double %b) {
+; CHECK-LABEL: max_after_fmul_f64:
+; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    fmul d0, d0, d1
+; CHECK-NEXT:    movi d1, #0000000000000000
+; CHECK-NEXT:    fcmp d0, #0.0
+; CHECK-NEXT:    fcsel d0, d0, d1, gt
+; CHECK-NEXT:    ret
+entry:
+  %mul = fmul double %a, %b
+  %cmp = fcmp nsz ogt double %mul, 0.000000e+00
+  %max = select i1 %cmp, double %mul, double 0.000000e+00
+  ret double %max
+}
+
+; Function argument: isKnownNeverSNaN returns false, so the fold is skipped.
+define float @max_arg(float %x) {
+; CHECK-LABEL: max_arg:
+; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    movi d1, #0000000000000000
+; CHECK-NEXT:    fcmp s0, #0.0
+; CHECK-NEXT:    fcsel s0, s0, s1, gt
+; CHECK-NEXT:    ret
+entry:
+  %cmp = fcmp nsz ogt float %x, 0.000000e+00
+  %max = select i1 %cmp, float %x, float 0.000000e+00
+  ret float %max
+}
+
+; fmin with arg: isKnownNeverSNaN returns false, so the fold is skipped.
+define float @min_arg(float %x) {
+; CHECK-LABEL: min_arg:
+; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    fmov s1, #1.00000000
+; CHECK-NEXT:    fcmp s0, s1
+; CHECK-NEXT:    fcsel s0, s0, s1, mi
+; CHECK-NEXT:    ret
+entry:
+  %cmp = fcmp nsz olt float %x, 1.000000e+00
+  %min = select i1 %cmp, float %x, float 1.000000e+00
+  ret float %min
+}
+
+; fmin with arithmetic input
+define float @min_after_fmul(float %a, float %b) {
+; CHECK-LABEL: min_after_fmul:
+; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    fmul s0, s0, s1
+; CHECK-NEXT:    fmov s1, #1.00000000
+; CHECK-NEXT:    fcmp s0, s1
+; CHECK-NEXT:    fcsel s0, s0, s1, mi
+; CHECK-NEXT:    ret
+entry:
+  %mul = fmul float %a, %b
+  %cmp = fcmp nsz olt float %mul, 1.000000e+00
+  %min = select i1 %cmp, float %mul, float 1.000000e+00
+  ret float %min
+}
+
+; Negative: no nsz flag - cannot transform (signed zero mismatch)
+define float @no_nsz(float %x) {
+; CHECK-LABEL: no_nsz:
+; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    movi d1, #0000000000000000
+; CHECK-NEXT:    fcmp s0, #0.0
+; CHECK-NEXT:    fcsel s0, s0, s1, gt
+; CHECK-NEXT:    ret
+entry:
+  %cmp = fcmp ogt float %x, 0.000000e+00
+  %max = select i1 %cmp, float %x, float 0.000000e+00
+  ret float %max
+}
+
+; Negative: unordered comparison - fold is NOT correct, skip.
+define float @unordered_cmp(float %x) {
+; CHECK-LABEL: unordered_cmp:
+; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    movi d1, #0000000000000000
+; CHECK-NEXT:    fcmp s0, #0.0
+; CHECK-NEXT:    fcsel s0, s0, s1, hi
+; CHECK-NEXT:    ret
+entry:
+  %cmp = fcmp nsz ugt float %x, 0.000000e+00
+  %max = select i1 %cmp, float %x, float 0.000000e+00
+  ret float %max
+}
+
+; nsz on the setcc only (not the select) - should still work
+define float @nsz_on_setcc_only(float %a, float %b) {
+; CHECK-LABEL: nsz_on_setcc_only:
+; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    fmul s0, s0, s1
+; CHECK-NEXT:    movi d1, #0000000000000000
+; CHECK-NEXT:    fcmp s0, #0.0
+; CHECK-NEXT:    fcsel s0, s0, s1, gt
+; CHECK-NEXT:    ret
+entry:
+  %mul = fmul float %a, %b
+  %cmp = fcmp nsz ogt float %mul, 0.000000e+00
+  %max = select i1 %cmp, float %mul, float 0.000000e+00
+  ret float %max
+}
+
+; GE (>=) with arithmetic input: folds to fmaxnm (same semantics as GT with nsz)
+define float @max_oge(float %a, float %b) {
+; CHECK-LABEL: max_oge:
+; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    fmul s0, s0, s1
+; CHECK-NEXT:    movi d1, #0000000000000000
+; CHECK-NEXT:    fcmp s0, #0.0
+; CHECK-NEXT:    fcsel s0, s0, s1, ge
+; CHECK-NEXT:    ret
+entry:
+  %mul = fmul float %a, %b
+  %cmp = fcmp nsz oge float %mul, 0.000000e+00
+  %max = select i1 %cmp, float %mul, float 0.000000e+00
+  ret float %max
+}
+
+; LE (<=) with arithmetic input: folds to fminnm (same semantics as MI with nsz)
+define float @min_ole(float %a, float %b) {
+; CHECK-LABEL: min_ole:
+; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    fmul s0, s0, s1
+; CHECK-NEXT:    fmov s1, #1.00000000
+; CHECK-NEXT:    fcmp s0, s1
+; CHECK-NEXT:    fcsel s0, s0, s1, ls
+; CHECK-NEXT:    ret
+entry:
+  %mul = fmul float %a, %b
+  %cmp = fcmp nsz ole float %mul, 1.000000e+00
+  %min = select i1 %cmp, float %mul, float 1.000000e+00
+  ret float %min
+}

>From 665746209aa793ed4f3ad9003de28ad309701c25 Mon Sep 17 00:00:00 2001
From: Usha Gupta <usha.gupta at arm.com>
Date: Wed, 13 May 2026 11:35:04 +0000
Subject: [PATCH 2/4] [AArch64][SelectionDAG] Fold late FCMP+CSEL patterns to
 FMAXNM/FMINNM during AArch64 DAG-to-DAG preprocessing.

---
 .../Target/AArch64/AArch64ISelDAGToDAG.cpp    | 97 +++++++++++++++++++
 .../Target/AArch64/AArch64ISelLowering.cpp    |  6 +-
 .../AArch64/fmaximumnum-fminimumnum-select.ll | 31 +++---
 3 files changed, 114 insertions(+), 20 deletions(-)

diff --git a/llvm/lib/Target/AArch64/AArch64ISelDAGToDAG.cpp b/llvm/lib/Target/AArch64/AArch64ISelDAGToDAG.cpp
index 64ccf606b4ef7..c5ac49913f9ba 100644
--- a/llvm/lib/Target/AArch64/AArch64ISelDAGToDAG.cpp
+++ b/llvm/lib/Target/AArch64/AArch64ISelDAGToDAG.cpp
@@ -447,6 +447,8 @@ class AArch64DAGToDAGISel : public SelectionDAGISel {
 
   bool trySelectXAR(SDNode *N);
 
+  SDValue tryFoldCselToFMaxMin(SDNode &N);
+
 // Include the pieces autogenerated from the target description.
 #include "AArch64GenDAGISel.inc"
 
@@ -7994,6 +7996,98 @@ bool AArch64DAGToDAGISel::SelectCmpBranchExtOperand(SDValue N, SDValue &Reg,
   return false;
 }
 
+/// Try to fold AArch64 CSEL/FCMP patterns to FMAXNUM_IEEE/FMINNUM_IEEE.
+///
+/// This is intentionally done in PreprocessISelDAG rather than DAGCombine:
+/// doing this earlier based on the defining operation of X can be invalidated
+/// by later DAG combines. At this point the DAG is being prepared for
+/// instruction selection, so the use of isKnownNeverSNaN(X) applies to the
+/// final SDValue being selected.
+///
+/// Only handles FCMP(X, C) with scalar FP types, where C is a non-NaN constant
+/// and the CSEL has nsz. The nsz requirement avoids signed-zero mismatches.
+/// The never-sNaN check is required because AArch64 FMAXNM/FMINNM differ from
+/// fcmp+fcsel for signaling NaN inputs.
+SDValue AArch64DAGToDAGISel::tryFoldCselToFMaxMin(SDNode &N) {
+  EVT VT = N.getValueType(0);
+
+  // Scalar FP only.
+  if (!VT.isFloatingPoint() || VT.isVector())
+    return SDValue();
+
+  // nsz required: without it, fmaxnm(+0,-0)=+0 but fcmp+fcsel returns -0.
+  if (!N.getFlags().hasNoSignedZeros())
+    return SDValue();
+
+  SDValue TVal = N.getOperand(0);
+  SDValue FVal = N.getOperand(1);
+  SDValue CCVal = N.getOperand(2);
+  SDValue Cmp = N.getOperand(3);
+
+  if (Cmp.getOpcode() != AArch64ISD::FCMP)
+    return SDValue();
+
+  auto *CC = dyn_cast<ConstantSDNode>(CCVal);
+  if (!CC)
+    return SDValue();
+
+  SDValue CmpLHS = Cmp.getOperand(0);
+  SDValue CmpRHS = Cmp.getOperand(1);
+  unsigned CondCode = CC->getZExtValue();
+
+  // Match GT, GE, MI, LS conditions.
+  // When nsz is present, GE and LS have the same semantics as GT and MI
+  // respectively:
+  unsigned IEEEOpc;
+  SDValue Operand, Constant;
+
+  if (CondCode == AArch64CC::GT || CondCode == AArch64CC::GE) {
+    if (TVal == CmpLHS && FVal == CmpRHS) {
+      IEEEOpc = ISD::FMAXNUM_IEEE;
+      Operand = CmpLHS;
+      Constant = CmpRHS;
+    } else if (TVal == CmpRHS && FVal == CmpLHS) {
+      IEEEOpc = ISD::FMINNUM_IEEE;
+      Operand = CmpLHS;
+      Constant = CmpRHS;
+    } else {
+      return SDValue();
+    }
+  } else if (CondCode == AArch64CC::MI || CondCode == AArch64CC::LS) {
+    if (TVal == CmpLHS && FVal == CmpRHS) {
+      IEEEOpc = ISD::FMINNUM_IEEE;
+      Operand = CmpLHS;
+      Constant = CmpRHS;
+    } else if (TVal == CmpRHS && FVal == CmpLHS) {
+      IEEEOpc = ISD::FMAXNUM_IEEE;
+      Operand = CmpLHS;
+      Constant = CmpRHS;
+    } else {
+      return SDValue();
+    }
+  } else {
+    return SDValue();
+  }
+
+  const TargetLowering *TLI = getTargetLowering();
+  if (!TLI->isOperationLegalOrCustom(IEEEOpc, VT))
+    return SDValue();
+
+  // Constant must be non-NaN.
+  auto *CFP = dyn_cast<ConstantFPSDNode>(Constant);
+  if (!CFP || CFP->getValueAPF().isNaN())
+    return SDValue();
+
+  // Only fold if variable operand is never sNaN.
+  // This runs after DAG combines, so later combines cannot remove a defining
+  // operation used by isKnownNeverSNaN().
+  if (!CurDAG->isKnownNeverSNaN(Operand))
+    return SDValue();
+
+  SDLoc DL(&N);
+  return CurDAG->getNode(IEEEOpc, DL, VT, Operand, Constant);
+}
+
 void AArch64DAGToDAGISel::PreprocessISelDAG() {
   bool MadeChange = false;
   for (SDNode &N : llvm::make_early_inc_range(CurDAG->allnodes())) {
@@ -8010,6 +8104,9 @@ void AArch64DAGToDAGISel::PreprocessISelDAG() {
 
       break;
     }
+    case AArch64ISD::CSEL:
+      Result = tryFoldCselToFMaxMin(N);
+      break;
     default:
       break;
     }
diff --git a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
index 9de0b461a216c..a3aa11dbf59a4 100644
--- a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
+++ b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
@@ -12473,8 +12473,12 @@ SDValue AArch64TargetLowering::LowerSELECT_CC(
   }
 
   // Emit first, and possibly only, CSEL.
+  // Propagate nsz to the CSEL so downstream passes can use it.
+  SDNodeFlags CSELFlags;
+  CSELFlags.setNoSignedZeros(Flags.hasNoSignedZeros());
   SDValue CC1Val = getCondCode(DAG, CC1);
-  SDValue CS1 = DAG.getNode(AArch64ISD::CSEL, DL, VT, TVal, FVal, CC1Val, Cmp);
+  SDValue CS1 = DAG.getNode(AArch64ISD::CSEL, DL, VT, {TVal, FVal, CC1Val, Cmp},
+                            CSELFlags);
 
   // If we need a second CSEL, emit it, using the output of the first as the
   // RHS.  We're effectively OR'ing the two CC's together.
diff --git a/llvm/test/CodeGen/AArch64/fmaximumnum-fminimumnum-select.ll b/llvm/test/CodeGen/AArch64/fmaximumnum-fminimumnum-select.ll
index 6bc65a1f32189..5e21c25215887 100644
--- a/llvm/test/CodeGen/AArch64/fmaximumnum-fminimumnum-select.ll
+++ b/llvm/test/CodeGen/AArch64/fmaximumnum-fminimumnum-select.ll
@@ -8,11 +8,10 @@
 define float @max_after_fmul(float %a, float %b) {
 ; CHECK-LABEL: max_after_fmul:
 ; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    movi d2, #0000000000000000
 ; CHECK-NEXT:    fmul s0, s0, s1
-; CHECK-NEXT:    movi d1, #0000000000000000
-; CHECK-NEXT:    fcmp s0, #0.0
-; CHECK-NEXT:    fcsel s0, s0, s1, gt
 ; CHECK-NEXT:    fmov s1, #1.00000000
+; CHECK-NEXT:    fmaxnm s0, s0, s2
 ; CHECK-NEXT:    fcmp s0, s1
 ; CHECK-NEXT:    fcsel s0, s0, s1, mi
 ; CHECK-NEXT:    ret
@@ -29,10 +28,9 @@ entry:
 define float @max_after_fadd(float %a, float %b) {
 ; CHECK-LABEL: max_after_fadd:
 ; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    movi d2, #0000000000000000
 ; CHECK-NEXT:    fadd s0, s0, s1
-; CHECK-NEXT:    movi d1, #0000000000000000
-; CHECK-NEXT:    fcmp s0, #0.0
-; CHECK-NEXT:    fcsel s0, s0, s1, gt
+; CHECK-NEXT:    fmaxnm s0, s0, s2
 ; CHECK-NEXT:    ret
 entry:
   %add = fadd float %a, %b
@@ -45,10 +43,9 @@ entry:
 define double @max_after_fmul_f64(double %a, double %b) {
 ; CHECK-LABEL: max_after_fmul_f64:
 ; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    movi d2, #0000000000000000
 ; CHECK-NEXT:    fmul d0, d0, d1
-; CHECK-NEXT:    movi d1, #0000000000000000
-; CHECK-NEXT:    fcmp d0, #0.0
-; CHECK-NEXT:    fcsel d0, d0, d1, gt
+; CHECK-NEXT:    fmaxnm d0, d0, d2
 ; CHECK-NEXT:    ret
 entry:
   %mul = fmul double %a, %b
@@ -91,8 +88,7 @@ define float @min_after_fmul(float %a, float %b) {
 ; CHECK:       // %bb.0: // %entry
 ; CHECK-NEXT:    fmul s0, s0, s1
 ; CHECK-NEXT:    fmov s1, #1.00000000
-; CHECK-NEXT:    fcmp s0, s1
-; CHECK-NEXT:    fcsel s0, s0, s1, mi
+; CHECK-NEXT:    fminnm s0, s0, s1
 ; CHECK-NEXT:    ret
 entry:
   %mul = fmul float %a, %b
@@ -133,10 +129,9 @@ entry:
 define float @nsz_on_setcc_only(float %a, float %b) {
 ; CHECK-LABEL: nsz_on_setcc_only:
 ; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    movi d2, #0000000000000000
 ; CHECK-NEXT:    fmul s0, s0, s1
-; CHECK-NEXT:    movi d1, #0000000000000000
-; CHECK-NEXT:    fcmp s0, #0.0
-; CHECK-NEXT:    fcsel s0, s0, s1, gt
+; CHECK-NEXT:    fmaxnm s0, s0, s2
 ; CHECK-NEXT:    ret
 entry:
   %mul = fmul float %a, %b
@@ -149,10 +144,9 @@ entry:
 define float @max_oge(float %a, float %b) {
 ; CHECK-LABEL: max_oge:
 ; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    movi d2, #0000000000000000
 ; CHECK-NEXT:    fmul s0, s0, s1
-; CHECK-NEXT:    movi d1, #0000000000000000
-; CHECK-NEXT:    fcmp s0, #0.0
-; CHECK-NEXT:    fcsel s0, s0, s1, ge
+; CHECK-NEXT:    fmaxnm s0, s0, s2
 ; CHECK-NEXT:    ret
 entry:
   %mul = fmul float %a, %b
@@ -167,8 +161,7 @@ define float @min_ole(float %a, float %b) {
 ; CHECK:       // %bb.0: // %entry
 ; CHECK-NEXT:    fmul s0, s0, s1
 ; CHECK-NEXT:    fmov s1, #1.00000000
-; CHECK-NEXT:    fcmp s0, s1
-; CHECK-NEXT:    fcsel s0, s0, s1, ls
+; CHECK-NEXT:    fminnm s0, s0, s1
 ; CHECK-NEXT:    ret
 entry:
   %mul = fmul float %a, %b

>From 99a820f2dba749f0a82b69d5ca44c59549a5aca5 Mon Sep 17 00:00:00 2001
From: Usha Gupta <usha.gupta at arm.com>
Date: Fri, 15 May 2026 12:16:37 +0000
Subject: [PATCH 3/4] Address review comments: Generate machine node directly,
 impose nsz requirement only when constant is 0

---
 .../Target/AArch64/AArch64ISelDAGToDAG.cpp    | 80 ++++++++++---------
 .../Target/AArch64/AArch64ISelLowering.cpp    |  4 +-
 2 files changed, 44 insertions(+), 40 deletions(-)

diff --git a/llvm/lib/Target/AArch64/AArch64ISelDAGToDAG.cpp b/llvm/lib/Target/AArch64/AArch64ISelDAGToDAG.cpp
index c5ac49913f9ba..edd0b45afd5f1 100644
--- a/llvm/lib/Target/AArch64/AArch64ISelDAGToDAG.cpp
+++ b/llvm/lib/Target/AArch64/AArch64ISelDAGToDAG.cpp
@@ -7996,7 +7996,7 @@ bool AArch64DAGToDAGISel::SelectCmpBranchExtOperand(SDValue N, SDValue &Reg,
   return false;
 }
 
-/// Try to fold AArch64 CSEL/FCMP patterns to FMAXNUM_IEEE/FMINNUM_IEEE.
+/// Try to fold AArch64 CSEL/FCMP patterns to FMAXNM/FMINNM.
 ///
 /// This is intentionally done in PreprocessISelDAG rather than DAGCombine:
 /// doing this earlier based on the defining operation of X can be invalidated
@@ -8004,10 +8004,10 @@ bool AArch64DAGToDAGISel::SelectCmpBranchExtOperand(SDValue N, SDValue &Reg,
 /// instruction selection, so the use of isKnownNeverSNaN(X) applies to the
 /// final SDValue being selected.
 ///
-/// Only handles FCMP(X, C) with scalar FP types, where C is a non-NaN constant
-/// and the CSEL has nsz. The nsz requirement avoids signed-zero mismatches.
-/// The never-sNaN check is required because AArch64 FMAXNM/FMINNM differ from
-/// fcmp+fcsel for signaling NaN inputs.
+/// Only handles FCMP(X, C) with scalar FP types, where C is a non-NaN constant.
+/// The nsz requirement is needed only when C is zero, to avoid signed-zero
+/// mismatches. The never-sNaN check is required because AArch64 FMAXNM/FMINNM
+/// differ from fcmp+fcsel for signaling NaN inputs.
 SDValue AArch64DAGToDAGISel::tryFoldCselToFMaxMin(SDNode &N) {
   EVT VT = N.getValueType(0);
 
@@ -8015,10 +8015,6 @@ SDValue AArch64DAGToDAGISel::tryFoldCselToFMaxMin(SDNode &N) {
   if (!VT.isFloatingPoint() || VT.isVector())
     return SDValue();
 
-  // nsz required: without it, fmaxnm(+0,-0)=+0 but fcmp+fcsel returns -0.
-  if (!N.getFlags().hasNoSignedZeros())
-    return SDValue();
-
   SDValue TVal = N.getOperand(0);
   SDValue FVal = N.getOperand(1);
   SDValue CCVal = N.getOperand(2);
@@ -8035,57 +8031,63 @@ SDValue AArch64DAGToDAGISel::tryFoldCselToFMaxMin(SDNode &N) {
   SDValue CmpRHS = Cmp.getOperand(1);
   unsigned CondCode = CC->getZExtValue();
 
-  // Match GT, GE, MI, LS conditions.
-  // When nsz is present, GE and LS have the same semantics as GT and MI
-  // respectively:
-  unsigned IEEEOpc;
-  SDValue Operand, Constant;
+  // Map VT and operation (max/min) to machine opcode.
+  auto getOpc = [](EVT VT, bool isMax) -> unsigned {
+    if (VT == MVT::f16)
+      return isMax ? AArch64::FMAXNMHrr : AArch64::FMINNMHrr;
+    else if (VT == MVT::f32)
+      return isMax ? AArch64::FMAXNMSrr : AArch64::FMINNMSrr;
+    else if (VT == MVT::f64)
+      return isMax ? AArch64::FMAXNMDrr : AArch64::FMINNMDrr;
+    else
+      return 0; // unsupported
+  };
 
+  // Determine whether to use max or min based on condition code and operands.
+  bool isMax;
   if (CondCode == AArch64CC::GT || CondCode == AArch64CC::GE) {
-    if (TVal == CmpLHS && FVal == CmpRHS) {
-      IEEEOpc = ISD::FMAXNUM_IEEE;
-      Operand = CmpLHS;
-      Constant = CmpRHS;
-    } else if (TVal == CmpRHS && FVal == CmpLHS) {
-      IEEEOpc = ISD::FMINNUM_IEEE;
-      Operand = CmpLHS;
-      Constant = CmpRHS;
-    } else {
+    if (TVal == CmpLHS && FVal == CmpRHS)
+      isMax = true;
+    else if (TVal == CmpRHS && FVal == CmpLHS)
+      isMax = false;
+    else
       return SDValue();
-    }
   } else if (CondCode == AArch64CC::MI || CondCode == AArch64CC::LS) {
-    if (TVal == CmpLHS && FVal == CmpRHS) {
-      IEEEOpc = ISD::FMINNUM_IEEE;
-      Operand = CmpLHS;
-      Constant = CmpRHS;
-    } else if (TVal == CmpRHS && FVal == CmpLHS) {
-      IEEEOpc = ISD::FMAXNUM_IEEE;
-      Operand = CmpLHS;
-      Constant = CmpRHS;
-    } else {
+    if (TVal == CmpLHS && FVal == CmpRHS)
+      isMax = false;
+    else if (TVal == CmpRHS && FVal == CmpLHS)
+      isMax = true;
+    else
       return SDValue();
-    }
   } else {
     return SDValue();
   }
 
-  const TargetLowering *TLI = getTargetLowering();
-  if (!TLI->isOperationLegalOrCustom(IEEEOpc, VT))
+  // Get the machine opcode for this VT and operation.
+  unsigned Opc = getOpc(VT, isMax);
+  if (!Opc)
     return SDValue();
 
   // Constant must be non-NaN.
-  auto *CFP = dyn_cast<ConstantFPSDNode>(Constant);
+  auto *CFP = dyn_cast<ConstantFPSDNode>(CmpRHS);
   if (!CFP || CFP->getValueAPF().isNaN())
     return SDValue();
 
+  // nsz flag required only when constant is zero: fmaxnm(+0,-0)=+0 differs from
+  // fcmp+select's -0. For non-zero constants, semantics are identical.
+  if (CFP->isZero() && !N.getFlags().hasNoSignedZeros())
+    return SDValue();
+
   // Only fold if variable operand is never sNaN.
   // This runs after DAG combines, so later combines cannot remove a defining
   // operation used by isKnownNeverSNaN().
-  if (!CurDAG->isKnownNeverSNaN(Operand))
+  if (!CurDAG->isKnownNeverSNaN(CmpLHS))
     return SDValue();
 
   SDLoc DL(&N);
-  return CurDAG->getNode(IEEEOpc, DL, VT, Operand, Constant);
+
+  // Directly emit the machine node
+  return SDValue(CurDAG->getMachineNode(Opc, DL, VT, CmpLHS, CmpRHS), 0);
 }
 
 void AArch64DAGToDAGISel::PreprocessISelDAG() {
diff --git a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
index a3aa11dbf59a4..1a1a2a9ca9e84 100644
--- a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
+++ b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
@@ -12473,7 +12473,9 @@ SDValue AArch64TargetLowering::LowerSELECT_CC(
   }
 
   // Emit first, and possibly only, CSEL.
-  // Propagate nsz to the CSEL so downstream passes can use it.
+  // Propagate only nsz flag as it directly affects CSEL semantics by ignoring
+  // signed-zero differences and relaxing equivalence.
+  // Other flags (reassoc, contract, arcp, etc.) are not applicable to CSEL.
   SDNodeFlags CSELFlags;
   CSELFlags.setNoSignedZeros(Flags.hasNoSignedZeros());
   SDValue CC1Val = getCondCode(DAG, CC1);

>From 257471b5b4090ed0d43816bcd72f64044520c47f Mon Sep 17 00:00:00 2001
From: Usha Gupta <usha.gupta at arm.com>
Date: Mon, 18 May 2026 09:20:20 +0000
Subject: [PATCH 4/4] Pass all flags for the CSEL node instead of only nsz

---
 llvm/lib/Target/AArch64/AArch64ISelDAGToDAG.cpp |  1 -
 llvm/lib/Target/AArch64/AArch64ISelLowering.cpp | 10 +++-------
 2 files changed, 3 insertions(+), 8 deletions(-)

diff --git a/llvm/lib/Target/AArch64/AArch64ISelDAGToDAG.cpp b/llvm/lib/Target/AArch64/AArch64ISelDAGToDAG.cpp
index edd0b45afd5f1..6002ee4c7b7f7 100644
--- a/llvm/lib/Target/AArch64/AArch64ISelDAGToDAG.cpp
+++ b/llvm/lib/Target/AArch64/AArch64ISelDAGToDAG.cpp
@@ -8003,7 +8003,6 @@ bool AArch64DAGToDAGISel::SelectCmpBranchExtOperand(SDValue N, SDValue &Reg,
 /// by later DAG combines. At this point the DAG is being prepared for
 /// instruction selection, so the use of isKnownNeverSNaN(X) applies to the
 /// final SDValue being selected.
-///
 /// Only handles FCMP(X, C) with scalar FP types, where C is a non-NaN constant.
 /// The nsz requirement is needed only when C is zero, to avoid signed-zero
 /// mismatches. The never-sNaN check is required because AArch64 FMAXNM/FMINNM
diff --git a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
index 1a1a2a9ca9e84..ab9362f7fc5b8 100644
--- a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
+++ b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
@@ -12473,14 +12473,10 @@ SDValue AArch64TargetLowering::LowerSELECT_CC(
   }
 
   // Emit first, and possibly only, CSEL.
-  // Propagate only nsz flag as it directly affects CSEL semantics by ignoring
-  // signed-zero differences and relaxing equivalence.
-  // Other flags (reassoc, contract, arcp, etc.) are not applicable to CSEL.
-  SDNodeFlags CSELFlags;
-  CSELFlags.setNoSignedZeros(Flags.hasNoSignedZeros());
+  // Propagate all flags to the CSEL node for downstream optimization passes.
   SDValue CC1Val = getCondCode(DAG, CC1);
-  SDValue CS1 = DAG.getNode(AArch64ISD::CSEL, DL, VT, {TVal, FVal, CC1Val, Cmp},
-                            CSELFlags);
+  SDValue CS1 =
+      DAG.getNode(AArch64ISD::CSEL, DL, VT, {TVal, FVal, CC1Val, Cmp}, Flags);
 
   // If we need a second CSEL, emit it, using the output of the first as the
   // RHS.  We're effectively OR'ing the two CC's together.



More information about the llvm-commits mailing list