[llvm] [AArch64] Fold -smin/-smax(X, 0) to neg + masked AND (PR #207671)

via llvm-commits llvm-commits at lists.llvm.org
Mon Jul 6 00:30:23 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-backend-aarch64

Author: Guy David (guy-david)

<details>
<summary>Changes</summary>

`smin(X, 0)` lowers to `X & M` and `smax(X, 0)` to `X & ~M`, where M is X arithmetically shifted right by the bit width minus one (a 0 or -1 sign mask). When negation is applied, `-(X & M)` is equivalent to `(-X) & M`. The latter form is slightly better because the shift no longer depends on the negation and the two can issue in parallel. Instruction count is unchanged.

The fold is suppressed when the result feeds a comparison because then the negation is folded into a cmn for free.

smin: https://alive2.llvm.org/ce/z/SfmPfH
smax: https://alive2.llvm.org/ce/z/_lVM0r

Assisted-by: Opus 4.8

---
Full diff: https://github.com/llvm/llvm-project/pull/207671.diff


2 Files Affected:

- (modified) llvm/lib/Target/AArch64/AArch64ISelLowering.cpp (+47) 
- (added) llvm/test/CodeGen/AArch64/neg-smin-zero.ll (+182) 


``````````diff
diff --git a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
index 7df8aa045dbf9..cafc9fb6e0ef6 100644
--- a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
+++ b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
@@ -23425,6 +23425,51 @@ static SDValue performAddCombineForShiftedOperands(SDNode *N,
   return SDValue();
 }
 
+// (sub 0, (smin X, 0)) --> (and (sub 0, X), (sra X, bw-1))
+// (sub 0, (smax X, 0)) --> (and (sub 0, X), (not (sra X, bw-1)))
+// smin(X,0)/smax(X,0) select to (X & M)/(X & ~M) with sign mask M = X sra bw-1.
+// Since M is 0/-1 the negate distributes: -(X & M) == (-X) & M. The latter keeps
+// the shift off the negate's critical path so both issue in parallel; same count.
+static SDValue performNegSMinMaxZeroCombine(SDNode *N, SelectionDAG &DAG) {
+  if (N->getOpcode() != ISD::SUB || !isNullConstant(N->getOperand(0)))
+    return SDValue();
+
+  SDValue MinMax = N->getOperand(1);
+  unsigned Opc = MinMax.getOpcode();
+  if ((Opc != ISD::SMIN && Opc != ISD::SMAX) || !MinMax.hasOneUse())
+    return SDValue();
+
+  // AArch64 AND/BIC with a shifted register is only available for i32/i64.
+  EVT VT = N->getValueType(0);
+  if (VT != MVT::i32 && VT != MVT::i64)
+    return SDValue();
+
+  // If the negate feeds a comparison it folds into a cmn for free; hoisting it
+  // ahead of the sign mask would defeat that and cost an instruction.
+  if (any_of(N->users(), [](const SDNode *U) {
+        unsigned UOpc = U->getOpcode();
+        return UOpc == ISD::SETCC || UOpc == ISD::SELECT_CC || UOpc == ISD::BR_CC;
+      }))
+    return SDValue();
+
+  SDValue X;
+  if (isNullConstant(MinMax.getOperand(1)))
+    X = MinMax.getOperand(0);
+  else if (isNullConstant(MinMax.getOperand(0)))
+    X = MinMax.getOperand(1);
+  else
+    return SDValue();
+
+  SDLoc DL(N);
+  SDValue NegX = DAG.getNegative(X, DL, VT);
+  SDValue Mask = DAG.getNode(ISD::SRA, DL, VT, X,
+                             DAG.getConstant(VT.getSizeInBits() - 1, DL, VT));
+  // smax(X,0) masks with the complement of the sign mask (selects to BIC).
+  if (Opc == ISD::SMAX)
+    Mask = DAG.getNOT(DL, Mask, VT);
+  return DAG.getNode(ISD::AND, DL, VT, NegX, Mask);
+}
+
 // The mid end will reassociate sub(sub(x, m1), m2) to sub(x, add(m1, m2))
 // This reassociates it back to allow the creation of more mls instructions.
 static SDValue performSubAddMULCombine(SDNode *N, SelectionDAG &DAG) {
@@ -23957,6 +24002,8 @@ static SDValue performAddSubCombine(SDNode *N,
     return Val;
   if (SDValue Val = performAddCombineForShiftedOperands(N, DCI.DAG))
     return Val;
+  if (SDValue Val = performNegSMinMaxZeroCombine(N, DCI.DAG))
+    return Val;
   if (SDValue Val = performSubAddMULCombine(N, DCI.DAG))
     return Val;
   if (SDValue Val = performSVEMulAddSubCombine(N, DCI))
diff --git a/llvm/test/CodeGen/AArch64/neg-smin-zero.ll b/llvm/test/CodeGen/AArch64/neg-smin-zero.ll
new file mode 100644
index 0000000000000..de271736c3dd4
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/neg-smin-zero.ll
@@ -0,0 +1,182 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 6
+; RUN: llc -mtriple=aarch64-none-linux-gnu < %s | FileCheck %s
+
+; -smin(X,0) == -(X & M) and -smax(X,0) == -(X & ~M) for the 0/-1 sign mask
+; M = (X asr bw-1).  Since M is 0 or -1 the negate distributes onto the AND, so
+; these can be emitted as neg ; and/bic(...,asr) with the shift kept on the
+; original X, letting the negate and shift issue in parallel.
+;
+; When the negate instead feeds a comparison it is folded into a cmn, so the
+; fold is suppressed there (see neg_smin_cmp / neg_smax_cmp).
+
+define i32 @neg_smin_zero_i32(i32 %x) {
+; CHECK-LABEL: neg_smin_zero_i32:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    neg w8, w0
+; CHECK-NEXT:    and w0, w8, w0, asr #31
+; CHECK-NEXT:    ret
+  %m = call i32 @llvm.smin.i32(i32 %x, i32 0)
+  %n = sub i32 0, %m
+  ret i32 %n
+}
+
+define i64 @neg_smin_zero_i64(i64 %x) {
+; CHECK-LABEL: neg_smin_zero_i64:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    neg x8, x0
+; CHECK-NEXT:    and x0, x8, x0, asr #63
+; CHECK-NEXT:    ret
+  %m = call i64 @llvm.smin.i64(i64 %x, i64 0)
+  %n = sub i64 0, %m
+  ret i64 %n
+}
+
+; smin operands commuted (smin 0, X).
+define i32 @neg_smin_zero_commuted(i32 %x) {
+; CHECK-LABEL: neg_smin_zero_commuted:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    neg w8, w0
+; CHECK-NEXT:    and w0, w8, w0, asr #31
+; CHECK-NEXT:    ret
+  %m = call i32 @llvm.smin.i32(i32 0, i32 %x)
+  %n = sub i32 0, %m
+  ret i32 %n
+}
+
+define i32 @neg_smax_zero_i32(i32 %x) {
+; CHECK-LABEL: neg_smax_zero_i32:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    neg w8, w0
+; CHECK-NEXT:    bic w0, w8, w0, asr #31
+; CHECK-NEXT:    ret
+  %m = call i32 @llvm.smax.i32(i32 %x, i32 0)
+  %n = sub i32 0, %m
+  ret i32 %n
+}
+
+define i64 @neg_smax_zero_i64(i64 %x) {
+; CHECK-LABEL: neg_smax_zero_i64:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    neg x8, x0
+; CHECK-NEXT:    bic x0, x8, x0, asr #63
+; CHECK-NEXT:    ret
+  %m = call i64 @llvm.smax.i64(i64 %x, i64 0)
+  %n = sub i64 0, %m
+  ret i64 %n
+}
+
+; The negate feeds a comparison, which folds it into a cmn; the fold must not
+; fire here or it would cost an extra instruction.
+define i32 @neg_smin_cmp(i32 %x, i32 %y) {
+; CHECK-LABEL: neg_smin_cmp:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    and w8, w0, w0, asr #31
+; CHECK-NEXT:    neg w8, w8
+; CHECK-NEXT:    cmp w8, w1
+; CHECK-NEXT:    cset w0, gt
+; CHECK-NEXT:    ret
+  %m = call i32 @llvm.smin.i32(i32 %x, i32 0)
+  %n = sub i32 0, %m
+  %c = icmp sgt i32 %n, %y
+  %z = zext i1 %c to i32
+  ret i32 %z
+}
+
+define i32 @neg_smax_cmp(i32 %x, i32 %y) {
+; CHECK-LABEL: neg_smax_cmp:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    bic w8, w0, w0, asr #31
+; CHECK-NEXT:    cmn w1, w8
+; CHECK-NEXT:    cset w0, lt
+; CHECK-NEXT:    ret
+  %m = call i32 @llvm.smax.i32(i32 %x, i32 0)
+  %n = sub i32 0, %m
+  %c = icmp sgt i32 %n, %y
+  %z = zext i1 %c to i32
+  ret i32 %z
+}
+
+; Equality compares fold the negate into a cmn regardless of INT_MIN (they do
+; not read the C/V flags), so smin benefits here too and the fold must stay
+; suppressed for it.
+define i32 @neg_smin_eq(i32 %x, i32 %y) {
+; CHECK-LABEL: neg_smin_eq:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    and w8, w0, w0, asr #31
+; CHECK-NEXT:    cmn w1, w8
+; CHECK-NEXT:    cset w0, eq
+; CHECK-NEXT:    ret
+  %m = call i32 @llvm.smin.i32(i32 %x, i32 0)
+  %n = sub i32 0, %m
+  %c = icmp eq i32 %n, %y
+  %z = zext i1 %c to i32
+  ret i32 %z
+}
+
+define i32 @neg_smax_eq(i32 %x, i32 %y) {
+; CHECK-LABEL: neg_smax_eq:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    bic w8, w0, w0, asr #31
+; CHECK-NEXT:    cmn w1, w8
+; CHECK-NEXT:    cset w0, eq
+; CHECK-NEXT:    ret
+  %m = call i32 @llvm.smax.i32(i32 %x, i32 0)
+  %n = sub i32 0, %m
+  %c = icmp eq i32 %n, %y
+  %z = zext i1 %c to i32
+  ret i32 %z
+}
+
+; Negative test: smin against a non-zero constant is not the sign-mask idiom.
+define i32 @neg_smin_nonzero(i32 %x) {
+; CHECK-LABEL: neg_smin_nonzero:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    mov w8, #5 // =0x5
+; CHECK-NEXT:    cmp w0, #5
+; CHECK-NEXT:    csel w8, w0, w8, lt
+; CHECK-NEXT:    neg w0, w8
+; CHECK-NEXT:    ret
+  %m = call i32 @llvm.smin.i32(i32 %x, i32 5)
+  %n = sub i32 0, %m
+  ret i32 %n
+}
+
+; The compare feeds a select (SELECT_CC), not a bare SETCC; the negate still
+; folds into a cmn, so the fold must stay suppressed here too.
+define i32 @neg_smax_select(i32 %x, i32 %y, i32 %a, i32 %b) {
+; CHECK-LABEL: neg_smax_select:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    bic w8, w0, w0, asr #31
+; CHECK-NEXT:    cmn w1, w8
+; CHECK-NEXT:    csel w0, w2, w3, lt
+; CHECK-NEXT:    ret
+  %m = call i32 @llvm.smax.i32(i32 %x, i32 0)
+  %n = sub i32 0, %m
+  %c = icmp sgt i32 %n, %y
+  %s = select i1 %c, i32 %a, i32 %b
+  ret i32 %s
+}
+
+; Same for a conditional branch (BR_CC).
+define i32 @neg_smax_br(i32 %x, i32 %y, i32 %a, i32 %b) {
+; CHECK-LABEL: neg_smax_br:
+; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    bic w8, w0, w0, asr #31
+; CHECK-NEXT:    cmn w1, w8
+; CHECK-NEXT:    csel w0, w2, w3, lt
+; CHECK-NEXT:    ret
+entry:
+  %m = call i32 @llvm.smax.i32(i32 %x, i32 0)
+  %n = sub i32 0, %m
+  %c = icmp sgt i32 %n, %y
+  br i1 %c, label %t, label %f
+t:
+  ret i32 %a
+f:
+  ret i32 %b
+}
+
+declare i32 @llvm.smin.i32(i32, i32)
+declare i64 @llvm.smin.i64(i64, i64)
+declare i32 @llvm.smax.i32(i32, i32)
+declare i64 @llvm.smax.i64(i64, i64)

``````````

</details>


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


More information about the llvm-commits mailing list