[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