[llvm] [DAGCombiner] Fold smax(X, -1) to or(X, ashr(X, BW-1)) for code size (PR #206242)

Aayush Shrivastava via llvm-commits llvm-commits at lists.llvm.org
Sat Jun 27 11:32:31 PDT 2026


https://github.com/iamaayushrivastava updated https://github.com/llvm/llvm-project/pull/206242

>From 251de0bb417b108851e7c978d4024798d5c2b369 Mon Sep 17 00:00:00 2001
From: iamaayushrivastava <iamaayushrivastava at gmail.com>
Date: Sat, 27 Jun 2026 17:55:58 +0530
Subject: [PATCH 1/2] [DAGCombiner] Fold smax(X, -1) to or(X, ashr(X, BW-1))
 for code size

---
 llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp | 16 ++++
 .../test/CodeGen/X86/smax-allones-codesize.ll | 74 +++++++++++++++++++
 2 files changed, 90 insertions(+)
 create mode 100644 llvm/test/CodeGen/X86/smax-allones-codesize.ll

diff --git a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
index 1b9a62211deaf..407ca8ee7bb5e 100644
--- a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
+++ b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
@@ -6307,6 +6307,22 @@ SDValue DAGCombiner::visitIMINMAX(SDNode *N) {
   if (SDValue RMINMAX = reassociateOps(Opcode, DL, N0, N1, N->getFlags()))
     return RMINMAX;
 
+  // For code size: smax(X, -1) -> or(X, ashr(X, BW-1))
+  // The arithmetic right shift sign-extends: 0 for X >= 0, -1 for X < 0.
+  // OR-ing X with this mask yields X when non-negative and -1 when negative,
+  // which matches smax(X, -1) using two instructions instead of compare+cmov.
+  if (ForCodeSize && Opcode == ISD::SMAX) {
+    if (auto *N1C = isConstOrConstSplat(N1)) {
+      if (N1C->isAllOnes() &&
+          !TLI.shouldAvoidTransformToShift(VT, VT.getScalarSizeInBits() - 1)) {
+        SDValue Shift = DAG.getNode(
+            ISD::SRA, DL, VT, N0,
+            DAG.getShiftAmountConstant(VT.getScalarSizeInBits() - 1, VT, DL));
+        return DAG.getNode(ISD::OR, DL, VT, N0, Shift);
+      }
+    }
+  }
+
   // If both operands are known to have the same sign (both non-negative or both
   // negative), flip between UMIN/UMAX and SMIN/SMAX.
   // Only do this if:
diff --git a/llvm/test/CodeGen/X86/smax-allones-codesize.ll b/llvm/test/CodeGen/X86/smax-allones-codesize.ll
new file mode 100644
index 0000000000000..3c1620baffe12
--- /dev/null
+++ b/llvm/test/CodeGen/X86/smax-allones-codesize.ll
@@ -0,0 +1,74 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 5
+; RUN: llc < %s -mtriple=x86_64-linux -O2 | FileCheck %s
+;
+; Verify smax(X, -1) folds to or(X, ashr(X, BW-1)) under minsize.
+; This saves a byte vs compare+cmov on x86-64 (-Oz / minsize attribute).
+; GCC PR 125921, LLVM issue #206153.
+
+; Without minsize: keep as compare+cmov
+define i32 @smax_allones_i32_normal(i32 %x) {
+; CHECK-LABEL: smax_allones_i32_normal:
+; CHECK:       # %bb.0:
+; CHECK-NEXT:    testl %edi, %edi
+; CHECK-NEXT:    movl $-1, %eax
+; CHECK-NEXT:    cmovnsl %edi, %eax
+; CHECK-NEXT:    retq
+  %r = call i32 @llvm.smax.i32(i32 %x, i32 -1)
+  ret i32 %r
+}
+
+; With minsize: fold to or(X, ashr(X, 31))
+define i32 @smax_allones_i32(i32 %x) minsize {
+; CHECK-LABEL: smax_allones_i32:
+; CHECK:       # %bb.0:
+; CHECK-NEXT:    movl %edi, %eax
+; CHECK-NEXT:    sarl $31, %eax
+; CHECK-NEXT:    orl %edi, %eax
+; CHECK-NEXT:    retq
+  %r = call i32 @llvm.smax.i32(i32 %x, i32 -1)
+  ret i32 %r
+}
+
+; i64 variant with minsize
+define i64 @smax_allones_i64(i64 %x) minsize {
+; CHECK-LABEL: smax_allones_i64:
+; CHECK:       # %bb.0:
+; CHECK-NEXT:    movq %rdi, %rax
+; CHECK-NEXT:    sarq $63, %rax
+; CHECK-NEXT:    orq %rdi, %rax
+; CHECK-NEXT:    retq
+  %r = call i64 @llvm.smax.i64(i64 %x, i64 -1)
+  ret i64 %r
+}
+
+; smax(X, 0) with minsize should NOT transform (no 2-instruction bitwise form)
+define i32 @smax_zero_minsize(i32 %x) minsize {
+; CHECK-LABEL: smax_zero_minsize:
+; CHECK:       # %bb.0:
+; CHECK-NEXT:    xorl %eax, %eax
+; CHECK-NEXT:    testl %edi, %edi
+; CHECK-NEXT:    cmovgl %edi, %eax
+; CHECK-NEXT:    retq
+  %r = call i32 @llvm.smax.i32(i32 %x, i32 0)
+  ret i32 %r
+}
+
+; smin(X, -1) with minsize should NOT transform (no 2-instruction bitwise form)
+define i32 @smin_allones_minsize(i32 %x) minsize {
+; CHECK-LABEL: smin_allones_minsize:
+; CHECK:       # %bb.0:
+; CHECK-NEXT:    cmpl $-1, %edi
+; CHECK-NEXT:    pushq $-1
+; CHECK-NEXT:    .cfi_adjust_cfa_offset 8
+; CHECK-NEXT:    popq %rax
+; CHECK-NEXT:    .cfi_adjust_cfa_offset -8
+; CHECK-NEXT:    cmovll %edi, %eax
+; CHECK-NEXT:    retq
+  %r = call i32 @llvm.smin.i32(i32 %x, i32 -1)
+  ret i32 %r
+}
+
+declare i32 @llvm.smax.i32(i32, i32)
+declare i64 @llvm.smax.i64(i64, i64)
+declare i16 @llvm.smax.i16(i16, i16)
+declare i32 @llvm.smin.i32(i32, i32)

>From 99f397403606c3e61cfde0c88eea26865dde1712 Mon Sep 17 00:00:00 2001
From: iamaayushrivastava <iamaayushrivastava at gmail.com>
Date: Sun, 28 Jun 2026 00:02:05 +0530
Subject: [PATCH 2/2] [DAGCombiner] Fold smax(X, -1) to or(X, ashr(X, BW-1))
 unconditionally

---
 llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp |  6 ++-
 llvm/test/CodeGen/AArch64/smax-allones.ll     | 23 ++++++++++
 llvm/test/CodeGen/X86/known-never-zero.ll     | 12 ++---
 .../test/CodeGen/X86/smax-allones-codesize.ll | 46 +++++--------------
 4 files changed, 45 insertions(+), 42 deletions(-)
 create mode 100644 llvm/test/CodeGen/AArch64/smax-allones.ll

diff --git a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
index 407ca8ee7bb5e..e745cdd63bba1 100644
--- a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
+++ b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
@@ -6307,11 +6307,13 @@ SDValue DAGCombiner::visitIMINMAX(SDNode *N) {
   if (SDValue RMINMAX = reassociateOps(Opcode, DL, N0, N1, N->getFlags()))
     return RMINMAX;
 
-  // For code size: smax(X, -1) -> or(X, ashr(X, BW-1))
+  // smax(X, -1) -> or(X, ashr(X, BW-1))
   // The arithmetic right shift sign-extends: 0 for X >= 0, -1 for X < 0.
   // OR-ing X with this mask yields X when non-negative and -1 when negative,
   // which matches smax(X, -1) using two instructions instead of compare+cmov.
-  if (ForCodeSize && Opcode == ISD::SMAX) {
+  // Beneficial for code size on x86-64 and for instruction count on
+  // AArch64/APX.
+  if (Opcode == ISD::SMAX) {
     if (auto *N1C = isConstOrConstSplat(N1)) {
       if (N1C->isAllOnes() &&
           !TLI.shouldAvoidTransformToShift(VT, VT.getScalarSizeInBits() - 1)) {
diff --git a/llvm/test/CodeGen/AArch64/smax-allones.ll b/llvm/test/CodeGen/AArch64/smax-allones.ll
new file mode 100644
index 0000000000000..ef083f38d3ec4
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/smax-allones.ll
@@ -0,0 +1,23 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 5
+; RUN: llc < %s -mtriple=aarch64-linux -O2 | FileCheck %s
+;
+; Verify smax(X, -1) folds to or(X, ashr(X, BW-1)), reducing instruction count
+; on AArch64 (2 instructions -> 1 shifted ORR). LLVM issue #206153.
+
+define i32 @smax_allones_i32(i32 %x) {
+; CHECK-LABEL: smax_allones_i32:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    orr w0, w0, w0, asr #31
+; CHECK-NEXT:    ret
+  %r = call i32 @llvm.smax.i32(i32 %x, i32 -1)
+  ret i32 %r
+}
+
+define i64 @smax_allones_i64(i64 %x) {
+; CHECK-LABEL: smax_allones_i64:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    orr x0, x0, x0, asr #63
+; CHECK-NEXT:    ret
+  %r = call i64 @llvm.smax.i64(i64 %x, i64 -1)
+  ret i64 %r
+}
diff --git a/llvm/test/CodeGen/X86/known-never-zero.ll b/llvm/test/CodeGen/X86/known-never-zero.ll
index c90ab282d2255..478449bb1702d 100644
--- a/llvm/test/CodeGen/X86/known-never-zero.ll
+++ b/llvm/test/CodeGen/X86/known-never-zero.ll
@@ -927,9 +927,9 @@ define i32 @smax_known_zero(i32 %x, i32 %y) {
 ; X86-LABEL: smax_known_zero:
 ; X86:       # %bb.0:
 ; X86-NEXT:    movl {{[0-9]+}}(%esp), %eax
-; X86-NEXT:    testl %eax, %eax
-; X86-NEXT:    movl $-1, %ecx
-; X86-NEXT:    cmovnsl %eax, %ecx
+; X86-NEXT:    movl %eax, %ecx
+; X86-NEXT:    sarl $31, %ecx
+; X86-NEXT:    orl %eax, %ecx
 ; X86-NEXT:    bsfl %ecx, %ecx
 ; X86-NEXT:    movl $32, %eax
 ; X86-NEXT:    cmovnel %ecx, %eax
@@ -937,9 +937,9 @@ define i32 @smax_known_zero(i32 %x, i32 %y) {
 ;
 ; X64-LABEL: smax_known_zero:
 ; X64:       # %bb.0:
-; X64-NEXT:    testl %edi, %edi
-; X64-NEXT:    movl $-1, %ecx
-; X64-NEXT:    cmovnsl %edi, %ecx
+; X64-NEXT:    movl %edi, %ecx
+; X64-NEXT:    sarl $31, %ecx
+; X64-NEXT:    orl %edi, %ecx
 ; X64-NEXT:    movl $32, %eax
 ; X64-NEXT:    rep bsfl %ecx, %eax
 ; X64-NEXT:    retq
diff --git a/llvm/test/CodeGen/X86/smax-allones-codesize.ll b/llvm/test/CodeGen/X86/smax-allones-codesize.ll
index 3c1620baffe12..592a5c93feef6 100644
--- a/llvm/test/CodeGen/X86/smax-allones-codesize.ll
+++ b/llvm/test/CodeGen/X86/smax-allones-codesize.ll
@@ -1,24 +1,11 @@
 ; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 5
 ; RUN: llc < %s -mtriple=x86_64-linux -O2 | FileCheck %s
 ;
-; Verify smax(X, -1) folds to or(X, ashr(X, BW-1)) under minsize.
-; This saves a byte vs compare+cmov on x86-64 (-Oz / minsize attribute).
-; GCC PR 125921, LLVM issue #206153.
+; Verify smax(X, -1) folds to or(X, ashr(X, BW-1)) at all optimization levels.
+; On x86-64 this saves a byte vs compare+cmov; on AArch64/APX it also reduces
+; instruction count. GCC PR 125921, LLVM issue #206153.
 
-; Without minsize: keep as compare+cmov
-define i32 @smax_allones_i32_normal(i32 %x) {
-; CHECK-LABEL: smax_allones_i32_normal:
-; CHECK:       # %bb.0:
-; CHECK-NEXT:    testl %edi, %edi
-; CHECK-NEXT:    movl $-1, %eax
-; CHECK-NEXT:    cmovnsl %edi, %eax
-; CHECK-NEXT:    retq
-  %r = call i32 @llvm.smax.i32(i32 %x, i32 -1)
-  ret i32 %r
-}
-
-; With minsize: fold to or(X, ashr(X, 31))
-define i32 @smax_allones_i32(i32 %x) minsize {
+define i32 @smax_allones_i32(i32 %x) {
 ; CHECK-LABEL: smax_allones_i32:
 ; CHECK:       # %bb.0:
 ; CHECK-NEXT:    movl %edi, %eax
@@ -29,8 +16,7 @@ define i32 @smax_allones_i32(i32 %x) minsize {
   ret i32 %r
 }
 
-; i64 variant with minsize
-define i64 @smax_allones_i64(i64 %x) minsize {
+define i64 @smax_allones_i64(i64 %x) {
 ; CHECK-LABEL: smax_allones_i64:
 ; CHECK:       # %bb.0:
 ; CHECK-NEXT:    movq %rdi, %rax
@@ -41,9 +27,9 @@ define i64 @smax_allones_i64(i64 %x) minsize {
   ret i64 %r
 }
 
-; smax(X, 0) with minsize should NOT transform (no 2-instruction bitwise form)
-define i32 @smax_zero_minsize(i32 %x) minsize {
-; CHECK-LABEL: smax_zero_minsize:
+; smax(X, 0) should NOT transform -- no 2-instruction bitwise form
+define i32 @smax_zero(i32 %x) {
+; CHECK-LABEL: smax_zero:
 ; CHECK:       # %bb.0:
 ; CHECK-NEXT:    xorl %eax, %eax
 ; CHECK-NEXT:    testl %edi, %edi
@@ -53,22 +39,14 @@ define i32 @smax_zero_minsize(i32 %x) minsize {
   ret i32 %r
 }
 
-; smin(X, -1) with minsize should NOT transform (no 2-instruction bitwise form)
-define i32 @smin_allones_minsize(i32 %x) minsize {
-; CHECK-LABEL: smin_allones_minsize:
+; smin(X, -1) should NOT transform -- no 2-instruction bitwise form exists
+define i32 @smin_allones(i32 %x) {
+; CHECK-LABEL: smin_allones:
 ; CHECK:       # %bb.0:
 ; CHECK-NEXT:    cmpl $-1, %edi
-; CHECK-NEXT:    pushq $-1
-; CHECK-NEXT:    .cfi_adjust_cfa_offset 8
-; CHECK-NEXT:    popq %rax
-; CHECK-NEXT:    .cfi_adjust_cfa_offset -8
+; CHECK-NEXT:    movl $-1, %eax
 ; CHECK-NEXT:    cmovll %edi, %eax
 ; CHECK-NEXT:    retq
   %r = call i32 @llvm.smin.i32(i32 %x, i32 -1)
   ret i32 %r
 }
-
-declare i32 @llvm.smax.i32(i32, i32)
-declare i64 @llvm.smax.i64(i64, i64)
-declare i16 @llvm.smax.i16(i16, i16)
-declare i32 @llvm.smin.i32(i32, i32)



More information about the llvm-commits mailing list