[llvm] [InstCombine] Narrow signed min/max DAGs before truncation (PR #218688)

via llvm-commits llvm-commits at lists.llvm.org
Tue Aug 25 06:47:32 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-llvm-transforms

Author: KIM MYEONG SEOK (mangsgi)

<details>
<summary>Changes</summary>

Recognize fixed-vector signed `llvm.smin`/`llvm.smax` DAGs whose leaves are
sign extensions from the truncation destination type, and rebuild the same DAG
directly in that narrow type.

For example, this changes the following expression shape:

```text
trunc(smax(smin(sext(C), smax(sext(A), sext(B))),
           smin(sext(A), sext(B))))
```

into:

```text
smax(smin(C, smax(A, B)), smin(A, B))
```

Sign extension preserves signed ordering, so signed minimum and maximum
commute with sign extension. Since each min/max result lane is selected from
the corresponding operand lanes, a DAG whose leaves are sign extensions from
the narrow type also remains within that narrow signed range. The fold therefore
applies whether or not the final `trunc` carries `nsw`.

To avoid increasing code size, require every wide instruction to have no users
outside the matched same-block DAG and the final truncation. Existing one-node
min/max folds remain handled by the general `canEvaluateTruncated` path.

For the motivating pattern, this removes three sign extensions and the final
truncation, and performs all four min/max operations in the narrow vector type.

Tests cover:

- A shared `<8 x i16>` to `<8 x i32>` signed min/max DAG
- Non-adjacent narrowing from `<4 x i64>` to `<4 x i8>`
- Final truncation both with and without `nsw`
- A wide DAG node with an external user
- A DAG containing a leaf that is not a matching sign extension

Testing:

- `llvm-lit -v llvm/test/Transforms/InstCombine/trunc-minmax-intrinsics.ll`
- `llvm-lit -sv -j2 llvm/test/Transforms/InstCombine`
- `opt -passes=instcombine -verify-each -disable-output llvm/test/Transforms/InstCombine/trunc-minmax-intrinsics.ll`

Proof:

- Full fixed-vector form (with `--disable-undef-input`):
  https://alive2.llvm.org/ce/z/ngfMYZ
- Scalar per-lane relation (without that option):
  https://alive2.llvm.org/ce/z/7VtWe8

Fixes #<!-- -->214224

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


2 Files Affected:

- (modified) llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp (+72-1) 
- (modified) llvm/test/Transforms/InstCombine/trunc-minmax-intrinsics.ll (+118) 


``````````diff
diff --git a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
index 2db6396b9d661..4d9e020b27c14 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
@@ -1042,6 +1042,76 @@ static Instruction *shrinkInsertElt(CastInst &Trunc,
   return nullptr;
 }
 
+/// Return true if Trunc has a same-block fixed-vector signed min/max DAG
+/// that can be rebuilt in the destination type without leaving wide uses.
+static bool
+collectNarrowableSignedMinMaxDAG(Value *V, FixedVectorType *WideTy,
+                                 FixedVectorType *NarrowTy, BasicBlock *BB,
+                                 SmallSetVector<Instruction *, 16> &Nodes,
+                                 unsigned &NumMinMax) {
+  auto *I = dyn_cast<Instruction>(V);
+  if (!I || I->getParent() != BB || I->getType() != WideTy)
+    return false;
+
+  // A shared node in the DAG may be reached by multiple paths.
+  if (!Nodes.insert(I))
+    return true;
+
+  // Every leaf must be a matching sext from the destination type.
+  if (auto *Ext = dyn_cast<SExtInst>(I))
+    return Ext->getSrcTy() == NarrowTy;
+
+  // Every non-leaf node must be signed min/max.
+  auto *MM = dyn_cast<MinMaxIntrinsic>(I);
+  if (!MM || !MM->isSigned())
+    return false;
+
+  ++NumMinMax;
+  return collectNarrowableSignedMinMaxDAG(MM->getLHS(), WideTy, NarrowTy, BB,
+                                          Nodes, NumMinMax) &&
+         collectNarrowableSignedMinMaxDAG(MM->getRHS(), WideTy, NarrowTy, BB,
+                                          Nodes, NumMinMax);
+}
+
+/// Return true if Trunc can be replaced by rebuilding its signed min/max
+/// operand DAG in the destination type.
+static bool canNarrowSignedMinMaxDAG(TruncInst &Trunc) {
+  auto *WideTy = dyn_cast<FixedVectorType>(Trunc.getSrcTy());
+  auto *NarrowTy = dyn_cast<FixedVectorType>(Trunc.getDestTy());
+  if (!WideTy || !NarrowTy)
+    return false;
+
+  if (WideTy->getNumElements() != NarrowTy->getNumElements() ||
+      !WideTy->getElementType()->isIntegerTy() ||
+      !NarrowTy->getElementType()->isIntegerTy())
+    return false;
+
+  SmallSetVector<Instruction *, 16> Nodes;
+  unsigned NumMinMax = 0;
+
+  if (!collectNarrowableSignedMinMaxDAG(Trunc.getOperand(0), WideTy, NarrowTy,
+                                        Trunc.getParent(), Nodes, NumMinMax))
+    return false;
+
+  // Avoid overlap with existing one-node folds.
+  if (NumMinMax < 2)
+    return false;
+
+  // Every wide node and extension must become dead after replacing Trunc.
+  for (Instruction *I : Nodes) {
+    for (User *U : I->users()) {
+      if (U == &Trunc)
+        continue;
+
+      auto *UserI = dyn_cast<Instruction>(U);
+      if (!UserI || !Nodes.contains(UserI))
+        return false;
+    }
+  }
+
+  return true;
+}
+
 Instruction *InstCombinerImpl::visitTrunc(TruncInst &Trunc) {
   if (Instruction *Result = commonCastTransforms(Trunc))
     return Result;
@@ -1056,7 +1126,8 @@ Instruction *InstCombinerImpl::visitTrunc(TruncInst &Trunc) {
   // expression tree to something weird like i93 unless the source is also
   // strange.
   if ((DestTy->isVectorTy() || shouldChangeType(SrcTy, DestTy)) &&
-      TypeEvaluationHelper::canEvaluateTruncated(Src, DestTy, *this, &Trunc)) {
+      (TypeEvaluationHelper::canEvaluateTruncated(Src, DestTy, *this, &Trunc) ||
+       canNarrowSignedMinMaxDAG(Trunc))) {
 
     // If this cast is a truncate, evaluting in a different type always
     // eliminates the cast, so it is always a win.
diff --git a/llvm/test/Transforms/InstCombine/trunc-minmax-intrinsics.ll b/llvm/test/Transforms/InstCombine/trunc-minmax-intrinsics.ll
index 8a2d38fb85bc3..4dd6b40fbd536 100644
--- a/llvm/test/Transforms/InstCombine/trunc-minmax-intrinsics.ll
+++ b/llvm/test/Transforms/InstCombine/trunc-minmax-intrinsics.ll
@@ -2,6 +2,7 @@
 ; RUN: opt -passes=instcombine %s -S | FileCheck %s
 
 declare void @use_i32(i32)
+declare void @use_v8i32(<8 x i32>)
 
 define i8 @umin_mul3_clamp(i8 %x) {
 ; CHECK-LABEL: define i8 @umin_mul3_clamp(
@@ -621,3 +622,120 @@ define <vscale x 4 x i8> @smax_sext_scalable_vec(<vscale x 4 x i8> %x, <vscale x
   %t = trunc <vscale x 4 x i32> %u to <vscale x 4 x i8>
   ret <vscale x 4 x i8> %t
 }
+
+define <8 x i16> @smin_smax_dag_shared_sext_vec(
+; CHECK-LABEL: define <8 x i16> @smin_smax_dag_shared_sext_vec(
+; CHECK-SAME: <8 x i16> [[A:%.*]], <8 x i16> [[B:%.*]], <8 x i16> [[C:%.*]]) {
+; CHECK-NEXT:    [[LO:%.*]] = call <8 x i16> @llvm.smin.v8i16(<8 x i16> [[A]], <8 x i16> [[B]])
+; CHECK-NEXT:    [[HI:%.*]] = call <8 x i16> @llvm.smax.v8i16(<8 x i16> [[A]], <8 x i16> [[B]])
+; CHECK-NEXT:    [[MID:%.*]] = call <8 x i16> @llvm.smin.v8i16(<8 x i16> [[C]], <8 x i16> [[HI]])
+; CHECK-NEXT:    [[WIDE:%.*]] = call <8 x i16> @llvm.smax.v8i16(<8 x i16> [[MID]], <8 x i16> [[LO]])
+; CHECK-NEXT:    ret <8 x i16> [[WIDE]]
+;
+  <8 x i16> %a, <8 x i16> %b, <8 x i16> %c) {
+  %aw = sext <8 x i16> %a to <8 x i32>
+  %bw = sext <8 x i16> %b to <8 x i32>
+  %cw = sext <8 x i16> %c to <8 x i32>
+  %lo = call <8 x i32> @llvm.smin.v8i32(<8 x i32> %aw, <8 x i32> %bw)
+  %hi = call <8 x i32> @llvm.smax.v8i32(<8 x i32> %aw, <8 x i32> %bw)
+  %mid = call <8 x i32> @llvm.smin.v8i32(<8 x i32> %cw, <8 x i32> %hi)
+  %wide = call <8 x i32> @llvm.smax.v8i32(<8 x i32> %mid, <8 x i32> %lo)
+  %result = trunc nsw <8 x i32> %wide to <8 x i16>
+  ret <8 x i16> %result
+}
+
+; Check that the fold is independent of the element widths and also handles a
+; non-adjacent narrowing from i64 to i8.
+define <4 x i8> @smin_smax_dag_sext_i8_i64_vec(
+; CHECK-LABEL: define <4 x i8> @smin_smax_dag_sext_i8_i64_vec(
+; CHECK-SAME: <4 x i8> [[A:%.*]], <4 x i8> [[B:%.*]], <4 x i8> [[C:%.*]]) {
+; CHECK-NEXT:    [[LO:%.*]] = call <4 x i8> @llvm.smin.v4i8(<4 x i8> [[A]], <4 x i8> [[B]])
+; CHECK-NEXT:    [[HI:%.*]] = call <4 x i8> @llvm.smax.v4i8(<4 x i8> [[A]], <4 x i8> [[B]])
+; CHECK-NEXT:    [[MID:%.*]] = call <4 x i8> @llvm.smin.v4i8(<4 x i8> [[C]], <4 x i8> [[HI]])
+; CHECK-NEXT:    [[WIDE:%.*]] = call <4 x i8> @llvm.smax.v4i8(<4 x i8> [[MID]], <4 x i8> [[LO]])
+; CHECK-NEXT:    ret <4 x i8> [[WIDE]]
+;
+    <4 x i8> %a, <4 x i8> %b, <4 x i8> %c) {
+  %aw = sext <4 x i8> %a to <4 x i64>
+  %bw = sext <4 x i8> %b to <4 x i64>
+  %cw = sext <4 x i8> %c to <4 x i64>
+  %lo = call <4 x i64> @llvm.smin.v4i64(<4 x i64> %aw, <4 x i64> %bw)
+  %hi = call <4 x i64> @llvm.smax.v4i64(<4 x i64> %aw, <4 x i64> %bw)
+  %mid = call <4 x i64> @llvm.smin.v4i64(<4 x i64> %cw, <4 x i64> %hi)
+  %wide = call <4 x i64> @llvm.smax.v4i64(<4 x i64> %mid, <4 x i64> %lo)
+  %result = trunc nsw <4 x i64> %wide to <4 x i8>
+  ret <4 x i8> %result
+}
+
+; Do not build a narrow DAG when that would leave an externally-used copy of
+; the wide DAG alive.
+define <8 x i16> @smin_smax_dag_external_wide_use_vec(
+; CHECK-LABEL: define <8 x i16> @smin_smax_dag_external_wide_use_vec(
+; CHECK-SAME: <8 x i16> [[A:%.*]], <8 x i16> [[B:%.*]], <8 x i16> [[C:%.*]]) {
+; CHECK-NEXT:    [[AW:%.*]] = sext <8 x i16> [[A]] to <8 x i32>
+; CHECK-NEXT:    [[BW:%.*]] = sext <8 x i16> [[B]] to <8 x i32>
+; CHECK-NEXT:    [[CW:%.*]] = sext <8 x i16> [[C]] to <8 x i32>
+; CHECK-NEXT:    [[LO:%.*]] = call <8 x i32> @llvm.smin.v8i32(<8 x i32> [[AW]], <8 x i32> [[BW]])
+; CHECK-NEXT:    call void @use_v8i32(<8 x i32> [[LO]])
+; CHECK-NEXT:    [[HI:%.*]] = call <8 x i32> @llvm.smax.v8i32(<8 x i32> [[AW]], <8 x i32> [[BW]])
+; CHECK-NEXT:    [[MID:%.*]] = call <8 x i32> @llvm.smin.v8i32(<8 x i32> [[CW]], <8 x i32> [[HI]])
+; CHECK-NEXT:    [[WIDE:%.*]] = call <8 x i32> @llvm.smax.v8i32(<8 x i32> [[MID]], <8 x i32> [[LO]])
+; CHECK-NEXT:    [[RESULT:%.*]] = trunc nsw <8 x i32> [[WIDE]] to <8 x i16>
+; CHECK-NEXT:    ret <8 x i16> [[RESULT]]
+;
+    <8 x i16> %a, <8 x i16> %b, <8 x i16> %c) {
+  %aw = sext <8 x i16> %a to <8 x i32>
+  %bw = sext <8 x i16> %b to <8 x i32>
+  %cw = sext <8 x i16> %c to <8 x i32>
+  %lo = call <8 x i32> @llvm.smin.v8i32(<8 x i32> %aw, <8 x i32> %bw)
+  call void @use_v8i32(<8 x i32> %lo)
+  %hi = call <8 x i32> @llvm.smax.v8i32(<8 x i32> %aw, <8 x i32> %bw)
+  %mid = call <8 x i32> @llvm.smin.v8i32(<8 x i32> %cw, <8 x i32> %hi)
+  %wide = call <8 x i32> @llvm.smax.v8i32(<8 x i32> %mid, <8 x i32> %lo)
+  %result = trunc nsw <8 x i32> %wide to <8 x i16>
+  ret <8 x i16> %result
+}
+
+; Do not narrow a DAG with a leaf that is not a sign extension from the
+; destination type.
+define <8 x i16> @smin_smax_dag_non_sext_leaf_vec(
+; CHECK-LABEL: define <8 x i16> @smin_smax_dag_non_sext_leaf_vec(
+; CHECK-SAME: <8 x i16> [[A:%.*]], <8 x i16> [[B:%.*]], <8 x i32> [[X:%.*]]) {
+; CHECK-NEXT:    [[AW:%.*]] = sext <8 x i16> [[A]] to <8 x i32>
+; CHECK-NEXT:    [[BW:%.*]] = sext <8 x i16> [[B]] to <8 x i32>
+; CHECK-NEXT:    [[LO:%.*]] = call <8 x i32> @llvm.smin.v8i32(<8 x i32> [[AW]], <8 x i32> [[X]])
+; CHECK-NEXT:    [[WIDE:%.*]] = call <8 x i32> @llvm.smax.v8i32(<8 x i32> [[LO]], <8 x i32> [[BW]])
+; CHECK-NEXT:    [[RESULT:%.*]] = trunc nsw <8 x i32> [[WIDE]] to <8 x i16>
+; CHECK-NEXT:    ret <8 x i16> [[RESULT]]
+;
+    <8 x i16> %a, <8 x i16> %b, <8 x i32> %x) {
+  %aw = sext <8 x i16> %a to <8 x i32>
+  %bw = sext <8 x i16> %b to <8 x i32>
+  %lo = call <8 x i32> @llvm.smin.v8i32(<8 x i32> %aw, <8 x i32> %x)
+  %wide = call <8 x i32> @llvm.smax.v8i32(<8 x i32> %lo, <8 x i32> %bw)
+  %result = trunc nsw <8 x i32> %wide to <8 x i16>
+  ret <8 x i16> %result
+}
+
+; The min/max DAG itself proves that the wide result fits in the narrow type,
+; so the final trunc does not need an nsw flag.
+define <4 x i16> @smin_smax_dag_plain_trunc_vec(
+; CHECK-LABEL: define <4 x i16> @smin_smax_dag_plain_trunc_vec(
+; CHECK-SAME: <4 x i16> [[A:%.*]], <4 x i16> [[B:%.*]], <4 x i16> [[C:%.*]]) {
+; CHECK-NEXT:    [[LO:%.*]] = call <4 x i16> @llvm.smin.v4i16(<4 x i16> [[A]], <4 x i16> [[B]])
+; CHECK-NEXT:    [[HI:%.*]] = call <4 x i16> @llvm.smax.v4i16(<4 x i16> [[A]], <4 x i16> [[B]])
+; CHECK-NEXT:    [[MID:%.*]] = call <4 x i16> @llvm.smin.v4i16(<4 x i16> [[C]], <4 x i16> [[HI]])
+; CHECK-NEXT:    [[WIDE:%.*]] = call <4 x i16> @llvm.smax.v4i16(<4 x i16> [[MID]], <4 x i16> [[LO]])
+; CHECK-NEXT:    ret <4 x i16> [[WIDE]]
+;
+    <4 x i16> %a, <4 x i16> %b, <4 x i16> %c) {
+  %aw = sext <4 x i16> %a to <4 x i32>
+  %bw = sext <4 x i16> %b to <4 x i32>
+  %cw = sext <4 x i16> %c to <4 x i32>
+  %lo = call <4 x i32> @llvm.smin.v4i32(<4 x i32> %aw, <4 x i32> %bw)
+  %hi = call <4 x i32> @llvm.smax.v4i32(<4 x i32> %aw, <4 x i32> %bw)
+  %mid = call <4 x i32> @llvm.smin.v4i32(<4 x i32> %cw, <4 x i32> %hi)
+  %wide = call <4 x i32> @llvm.smax.v4i32(<4 x i32> %mid, <4 x i32> %lo)
+  %result = trunc <4 x i32> %wide to <4 x i16>
+  ret <4 x i16> %result
+}

``````````

</details>


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


More information about the llvm-commits mailing list