[llvm] [AArch64] Fold -smin/-smax(X, 0) to neg + masked AND (PR #207671)
Guy David via llvm-commits
llvm-commits at lists.llvm.org
Mon Jul 6 00:29:51 PDT 2026
https://github.com/guy-david created https://github.com/llvm/llvm-project/pull/207671
`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
>From 2c6cb8484c71cedecadd12e66385e4cf69a86f97 Mon Sep 17 00:00:00 2001
From: Guy David <guyda96 at gmail.com>
Date: Sun, 5 Jul 2026 21:38:58 +0300
Subject: [PATCH 1/2] [AArch64] [NFC] Add tests
---
llvm/test/CodeGen/AArch64/neg-smin-zero.ll | 147 +++++++++++++++++++++
1 file changed, 147 insertions(+)
create mode 100644 llvm/test/CodeGen/AArch64/neg-smin-zero.ll
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..0b77734547694
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/neg-smin-zero.ll
@@ -0,0 +1,147 @@
+; 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: and w8, w0, w0, asr #31
+; CHECK-NEXT: neg w0, w8
+; 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: and x8, x0, x0, asr #63
+; CHECK-NEXT: neg x0, x8
+; 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: and w8, w0, w0, asr #31
+; CHECK-NEXT: neg w0, w8
+; 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: bic w8, w0, w0, asr #31
+; CHECK-NEXT: neg w0, w8
+; 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: bic x8, x0, x0, asr #63
+; CHECK-NEXT: neg x0, x8
+; 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
+}
+
+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)
>From f26241c864be9e12c71fc5fb4ab0a7e137f86683 Mon Sep 17 00:00:00 2001
From: Guy David <guyda96 at gmail.com>
Date: Sun, 5 Jul 2026 21:39:22 +0300
Subject: [PATCH 2/2] [AArch64] Fold -smin/-smax(X,0) to neg + masked AND
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. This covers the compare feeding a
SETCC, SELECT_CC, or BR_CC.
smin: https://alive2.llvm.org/ce/z/SfmPfH
smax: https://alive2.llvm.org/ce/z/_lVM0r
Assisted-by: Opus 4.8
---
.../Target/AArch64/AArch64ISelLowering.cpp | 47 ++++++++++++++++
llvm/test/CodeGen/AArch64/neg-smin-zero.ll | 55 +++++++++++++++----
2 files changed, 92 insertions(+), 10 deletions(-)
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
index 0b77734547694..de271736c3dd4 100644
--- a/llvm/test/CodeGen/AArch64/neg-smin-zero.ll
+++ b/llvm/test/CodeGen/AArch64/neg-smin-zero.ll
@@ -12,8 +12,8 @@
define i32 @neg_smin_zero_i32(i32 %x) {
; CHECK-LABEL: neg_smin_zero_i32:
; CHECK: // %bb.0:
-; CHECK-NEXT: and w8, w0, w0, asr #31
-; CHECK-NEXT: neg w0, w8
+; 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
@@ -23,8 +23,8 @@ define i32 @neg_smin_zero_i32(i32 %x) {
define i64 @neg_smin_zero_i64(i64 %x) {
; CHECK-LABEL: neg_smin_zero_i64:
; CHECK: // %bb.0:
-; CHECK-NEXT: and x8, x0, x0, asr #63
-; CHECK-NEXT: neg x0, x8
+; 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
@@ -35,8 +35,8 @@ define i64 @neg_smin_zero_i64(i64 %x) {
define i32 @neg_smin_zero_commuted(i32 %x) {
; CHECK-LABEL: neg_smin_zero_commuted:
; CHECK: // %bb.0:
-; CHECK-NEXT: and w8, w0, w0, asr #31
-; CHECK-NEXT: neg w0, w8
+; 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
@@ -46,8 +46,8 @@ define i32 @neg_smin_zero_commuted(i32 %x) {
define i32 @neg_smax_zero_i32(i32 %x) {
; CHECK-LABEL: neg_smax_zero_i32:
; CHECK: // %bb.0:
-; CHECK-NEXT: bic w8, w0, w0, asr #31
-; CHECK-NEXT: neg w0, w8
+; 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
@@ -57,8 +57,8 @@ define i32 @neg_smax_zero_i32(i32 %x) {
define i64 @neg_smax_zero_i64(i64 %x) {
; CHECK-LABEL: neg_smax_zero_i64:
; CHECK: // %bb.0:
-; CHECK-NEXT: bic x8, x0, x0, asr #63
-; CHECK-NEXT: neg x0, x8
+; 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
@@ -141,6 +141,41 @@ define i32 @neg_smin_nonzero(i32 %x) {
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)
More information about the llvm-commits
mailing list