[llvm] [Instcombine] Fold a zero-select hexadecimal digit count into ctlz(x | 1) (PR #215073)
Jeong Jihyeon via llvm-commits
llvm-commits at lists.llvm.org
Tue Aug 18 21:22:10 PDT 2026
https://github.com/JihyeonJeong129 updated https://github.com/llvm/llvm-project/pull/215073
>From 33e94428f727c3b978e0dde1cc284c15857afd02 Mon Sep 17 00:00:00 2001
From: Jihyeon Jeong <jh.jeong129 at gmail.com>
Date: Wed, 19 Aug 2026 04:18:10 +0000
Subject: [PATCH 1/2] [InstCombine] Add pre-commit tests for guarded ctlz fold
---
.../InstCombine/select-cmp-cttz-ctlz.ll | 139 +++++++++++++++++-
1 file changed, 137 insertions(+), 2 deletions(-)
diff --git a/llvm/test/Transforms/InstCombine/select-cmp-cttz-ctlz.ll b/llvm/test/Transforms/InstCombine/select-cmp-cttz-ctlz.ll
index 5d76cfb73f2a4..94e6c70cb7ebe 100644
--- a/llvm/test/Transforms/InstCombine/select-cmp-cttz-ctlz.ll
+++ b/llvm/test/Transforms/InstCombine/select-cmp-cttz-ctlz.ll
@@ -744,8 +744,8 @@ define i32 @test_ctlz_sub_zero_poison(i32 %arg) {
ret i32 %sel
}
-define i32 @test_ctlz_sub_wrong_const(i32 %arg) {
-; CHECK-LABEL: @test_ctlz_sub_wrong_const(
+define i32 @fold_ctlz_sub_one(i32 %arg) {
+; CHECK-LABEL: @fold_ctlz_sub_one(
; CHECK-NEXT: [[CMP:%.*]] = icmp eq i32 [[ARG:%.*]], 0
; CHECK-NEXT: [[CTZ:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[ARG]], i1 true)
; CHECK-NEXT: [[SUB:%.*]] = sub nuw nsw i32 32, [[CTZ]]
@@ -1009,6 +1009,141 @@ define <2 x i32> @test_cttz_not_bw_odd_mul_vec_nonsplat(<2 x i32> %x) {
ret <2 x i32> %res
}
+define i64 @fold_ctlz_hex_digits(i64 noundef %x) {
+; CHECK-LABEL: @fold_ctlz_hex_digits(
+; CHECK-NEXT: [[ISZERO:%.*]] = icmp eq i64 [[X:%.*]], 0
+; CHECK-NEXT: [[LZ:%.*]] = call range(i64 0, 65) i64 @llvm.ctlz.i64(i64 [[X]], i1 true)
+; CHECK-NEXT: [[BIAS:%.*]] = sub nuw nsw i64 67, [[LZ]]
+; CHECK-NEXT: [[DIGITS:%.*]] = lshr i64 [[BIAS]], 2
+; CHECK-NEXT: [[RESULT:%.*]] = select i1 [[ISZERO]], i64 1, i64 [[DIGITS]]
+; CHECK-NEXT: ret i64 [[RESULT]]
+;
+ %iszero = icmp eq i64 %x, 0
+ %lz = call i64 @llvm.ctlz.i64(i64 %x, i1 true)
+ %bias = sub nuw nsw i64 67, %lz
+ %digits = lshr i64 %bias, 2
+ %result = select i1 %iszero, i64 1, i64 %digits
+ ret i64 %result
+}
+
+define i64 @fold_ctlz_hex_digits_ne_commuted(i64 noundef %x) {
+; CHECK-LABEL: @fold_ctlz_hex_digits_ne_commuted(
+; CHECK-NEXT: [[ISNOTZERO_NOT:%.*]] = icmp eq i64 [[X:%.*]], 0
+; CHECK-NEXT: [[LZ:%.*]] = call range(i64 0, 65) i64 @llvm.ctlz.i64(i64 [[X]], i1 true)
+; CHECK-NEXT: [[BIAS:%.*]] = sub nuw nsw i64 67, [[LZ]]
+; CHECK-NEXT: [[DIGITS:%.*]] = lshr i64 [[BIAS]], 2
+; CHECK-NEXT: [[RESULT:%.*]] = select i1 [[ISNOTZERO_NOT]], i64 1, i64 [[DIGITS]]
+; CHECK-NEXT: ret i64 [[RESULT]]
+;
+ %isnotzero = icmp ne i64 0, %x
+ %lz = call i64 @llvm.ctlz.i64(i64 %x, i1 true)
+ %bias = sub nuw nsw i64 67, %lz
+ %digits = lshr i64 %bias, 2
+ %result = select i1 %isnotzero, i64 %digits, i64 1
+ ret i64 %result
+}
+
+define i64 @fold_ctlz_hex_digits_exact(i64 noundef %x) {
+; CHECK-LABEL: @fold_ctlz_hex_digits_exact(
+; CHECK-NEXT: [[ISZERO:%.*]] = icmp eq i64 [[X:%.*]], 0
+; CHECK-NEXT: [[LZ:%.*]] = call range(i64 0, 65) i64 @llvm.ctlz.i64(i64 [[X]], i1 true)
+; CHECK-NEXT: [[BIAS:%.*]] = sub nuw nsw i64 67, [[LZ]]
+; CHECK-NEXT: [[DIGITS1:%.*]] = lshr exact i64 [[BIAS]], 2
+; CHECK-NEXT: [[DIGITS:%.*]] = select i1 [[ISZERO]], i64 1, i64 [[DIGITS1]]
+; CHECK-NEXT: ret i64 [[DIGITS]]
+;
+ %iszero = icmp eq i64 %x, 0
+ %lz = call i64 @llvm.ctlz.i64(i64 %x, i1 true)
+ %bias = sub nuw nsw i64 67, %lz
+ %digits = lshr exact i64 %bias, 2
+ %result = select i1 %iszero, i64 1, i64 %digits
+ ret i64 %result
+}
+
+; The transform requires is_zero_poison=true.
+define i64 @do_not_fold_ctlz_hex_digits_defined_zero(i64 noundef %x) {
+; CHECK-LABEL: @do_not_fold_ctlz_hex_digits_defined_zero(
+; CHECK-NEXT: [[ISZERO:%.*]] = icmp eq i64 [[X:%.*]], 0
+; CHECK-NEXT: [[LZ:%.*]] = call range(i64 0, 65) i64 @llvm.ctlz.i64(i64 [[X]], i1 false)
+; CHECK-NEXT: [[BIAS:%.*]] = sub nuw nsw i64 67, [[LZ]]
+; CHECK-NEXT: [[DIGITS:%.*]] = lshr i64 [[BIAS]], 2
+; CHECK-NEXT: [[RESULT:%.*]] = select i1 [[ISZERO]], i64 1, i64 [[DIGITS]]
+; CHECK-NEXT: ret i64 [[RESULT]]
+;
+ %iszero = icmp eq i64 %x, 0
+ %lz = call i64 @llvm.ctlz.i64(i64 %x, i1 false)
+ %bias = sub nuw nsw i64 67, %lz
+ %digits = lshr i64 %bias, 2
+ %result = select i1 %iszero, i64 1, i64 %digits
+ ret i64 %result
+}
+
+; The expression evaluated at X == 0 does not match the selected zero value.
+define i64 @do_not_fold_ctlz_hex_digits_wrong_zero_value(i64 noundef %x) {
+; CHECK-LABEL: @do_not_fold_ctlz_hex_digits_wrong_zero_value(
+; CHECK-NEXT: [[ISZERO:%.*]] = icmp eq i64 [[X:%.*]], 0
+; CHECK-NEXT: [[LZ:%.*]] = call range(i64 0, 65) i64 @llvm.ctlz.i64(i64 [[X]], i1 true)
+; CHECK-NEXT: [[BIAS:%.*]] = sub nuw nsw i64 67, [[LZ]]
+; CHECK-NEXT: [[DIGITS:%.*]] = lshr i64 [[BIAS]], 2
+; CHECK-NEXT: [[RESULT:%.*]] = select i1 [[ISZERO]], i64 2, i64 [[DIGITS]]
+; CHECK-NEXT: ret i64 [[RESULT]]
+;
+ %iszero = icmp eq i64 %x, 0
+ %lz = call i64 @llvm.ctlz.i64(i64 %x, i1 true)
+ %bias = sub nuw nsw i64 67, %lz
+ %digits = lshr i64 %bias, 2
+ %result = select i1 %iszero, i64 2, i64 %digits
+ ret i64 %result
+}
+
+; The generic equivalence fold drops nuw because it would be poison at X == 0.
+define i16 @fold_ctlz_drop_nuw_on_zero(i16 noundef %x) {
+; CHECK-LABEL: @fold_ctlz_drop_nuw_on_zero(
+; CHECK-NEXT: [[LZ:%.*]] = call range(i16 0, 17) i16 @llvm.ctlz.i16(i16 [[X:%.*]], i1 false)
+; CHECK-NEXT: [[BIAS:%.*]] = sub nsw i16 10, [[LZ]]
+; CHECK-NEXT: [[DIGITS:%.*]] = lshr i16 [[BIAS]], 1
+; CHECK-NEXT: ret i16 [[DIGITS]]
+;
+ %iszero = icmp eq i16 %x, 0
+ %lz = call i16 @llvm.ctlz.i16(i16 %x, i1 true)
+ %bias = sub nuw i16 10, %lz
+ %digits = lshr i16 %bias, 1
+ %result = select i1 %iszero, i16 32765, i16 %digits
+ ret i16 %result
+}
+
+; The generic equivalence fold drops nsw because it would be poison at X == 0.
+define i16 @fold_ctlz_drop_nsw_on_zero(i16 noundef %x) {
+; CHECK-LABEL: @fold_ctlz_drop_nsw_on_zero(
+; CHECK-NEXT: [[LZ:%.*]] = call range(i16 0, 17) i16 @llvm.ctlz.i16(i16 [[X:%.*]], i1 false)
+; CHECK-NEXT: [[BIAS:%.*]] = sub nuw i16 -32760, [[LZ]]
+; CHECK-NEXT: [[DIGITS:%.*]] = lshr i16 [[BIAS]], 1
+; CHECK-NEXT: ret i16 [[DIGITS]]
+;
+ %iszero = icmp eq i16 %x, 0
+ %lz = call i16 @llvm.ctlz.i16(i16 %x, i1 true)
+ %bias = sub nsw i16 -32760, %lz
+ %digits = lshr i16 %bias, 1
+ %result = select i1 %iszero, i16 16380, i16 %digits
+ ret i16 %result
+}
+
+; The generic equivalence fold drops exact because it would be poison at X == 0.
+define i16 @fold_ctlz_drop_exact_on_zero(i16 noundef %x) {
+; CHECK-LABEL: @fold_ctlz_drop_exact_on_zero(
+; CHECK-NEXT: [[LZ:%.*]] = call range(i16 0, 17) i16 @llvm.ctlz.i16(i16 [[X:%.*]], i1 false)
+; CHECK-NEXT: [[BIAS:%.*]] = sub nuw nsw i16 20, [[LZ]]
+; CHECK-NEXT: [[DIGITS:%.*]] = lshr i16 [[BIAS]], 2
+; CHECK-NEXT: ret i16 [[DIGITS]]
+;
+ %iszero = icmp eq i16 %x, 0
+ %lz = call i16 @llvm.ctlz.i16(i16 %x, i1 true)
+ %bias = sub i16 20, %lz
+ %digits = lshr exact i16 %bias, 2
+ %result = select i1 %iszero, i16 1, i16 %digits
+ ret i16 %result
+}
+
declare i16 @llvm.ctlz.i16(i16, i1)
declare i32 @llvm.ctlz.i32(i32, i1)
declare i64 @llvm.ctlz.i64(i64, i1)
>From 826eeeb695d16cca0cf29998ca1a9c6a6308bcfb Mon Sep 17 00:00:00 2001
From: Jihyeon Jeong <jh.jeong129 at gmail.com>
Date: Wed, 19 Aug 2026 04:19:12 +0000
Subject: [PATCH 2/2] [InstCombine] Fold select guarding zero-poison ctlz
---
.../InstCombine/InstCombineSelect.cpp | 43 +++++++++++++++++++
.../InstCombine/select-cmp-cttz-ctlz.ll | 37 ++++++++--------
2 files changed, 61 insertions(+), 19 deletions(-)
diff --git a/llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp b/llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp
index 39f0d11df4db9..b8f7d3f10da66 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp
@@ -1767,6 +1767,49 @@ Instruction *InstCombinerImpl::foldSelectValueEquivalence(SelectInst &Sel,
// We have an 'EQ' comparison, so the select's false value will propagate.
// Example:
// (X == 42) ? 43 : (X + 1) --> (X == 42) ? (X + 1) : (X + 1) --> X + 1
+
+ // ctlz(X, true) is poison for X == 0, but ctlz(X | 1, true) is defined and
+ // has the same value for every non-zero X. See whether replacing the ctlz
+ // with ctlz(1, true) makes the false arm equivalent to the zero arm, and let
+ // the generic simplifier handle the surrounding expression and poison flags.
+ auto FoldZeroPoisonCtlz = [&](Value *X, Value *Zero) -> Instruction * {
+ if (!X->getType()->isIntegerTy() || !match(Zero, m_Zero()))
+ return nullptr;
+
+ for (User *U : X->users()) {
+ auto *II = dyn_cast<IntrinsicInst>(U);
+ if (!II || II->getIntrinsicID() != Intrinsic::ctlz ||
+ II->getArgOperand(0) != X || !match(II->getArgOperand(1), m_One()))
+ continue;
+
+ Constant *CtlzOfOne = ConstantInt::get(
+ X->getType(), X->getType()->getIntegerBitWidth() - 1);
+ SmallVector<Instruction *> CtlzDropFlags;
+ if (simplifyWithOpReplaced(FalseVal, II, CtlzOfOne, SQ,
+ /* AllowRefinement=*/false,
+ &CtlzDropFlags) != TrueVal)
+ continue;
+
+ Builder.SetInsertPoint(II);
+ Value *NonZero = Builder.CreateOr(X, ConstantInt::get(X->getType(), 1),
+ "ctlz.nonzero");
+ replaceOperand(*II, 0, NonZero);
+ II->dropPoisonGeneratingAnnotations();
+ Worklist.add(II);
+ for (Instruction *I : CtlzDropFlags) {
+ I->dropPoisonGeneratingAnnotations();
+ Worklist.add(I);
+ }
+ return replaceInstUsesWith(Sel, FalseVal);
+ }
+ return nullptr;
+ };
+
+ if (Instruction *R = FoldZeroPoisonCtlz(CmpLHS, CmpRHS))
+ return R;
+ if (Instruction *R = FoldZeroPoisonCtlz(CmpRHS, CmpLHS))
+ return R;
+
SmallVector<Instruction *> DropFlags;
if ((CanReplaceCmpLHSWithRHS &&
simplifyWithOpReplaced(FalseVal, CmpLHS, CmpRHS, SQ,
diff --git a/llvm/test/Transforms/InstCombine/select-cmp-cttz-ctlz.ll b/llvm/test/Transforms/InstCombine/select-cmp-cttz-ctlz.ll
index 94e6c70cb7ebe..122a551a6a439 100644
--- a/llvm/test/Transforms/InstCombine/select-cmp-cttz-ctlz.ll
+++ b/llvm/test/Transforms/InstCombine/select-cmp-cttz-ctlz.ll
@@ -746,11 +746,10 @@ define i32 @test_ctlz_sub_zero_poison(i32 %arg) {
define i32 @fold_ctlz_sub_one(i32 %arg) {
; CHECK-LABEL: @fold_ctlz_sub_one(
-; CHECK-NEXT: [[CMP:%.*]] = icmp eq i32 [[ARG:%.*]], 0
-; CHECK-NEXT: [[CTZ:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[ARG]], i1 true)
+; CHECK-NEXT: [[CTLZ_NONZERO:%.*]] = or i32 [[ARG:%.*]], 1
+; CHECK-NEXT: [[CTZ:%.*]] = call range(i32 0, 32) i32 @llvm.ctlz.i32(i32 [[CTLZ_NONZERO]], i1 true)
; CHECK-NEXT: [[SUB:%.*]] = sub nuw nsw i32 32, [[CTZ]]
-; CHECK-NEXT: [[SEL:%.*]] = select i1 [[CMP]], i32 1, i32 [[SUB]]
-; CHECK-NEXT: ret i32 [[SEL]]
+; CHECK-NEXT: ret i32 [[SUB]]
;
%cmp = icmp eq i32 %arg, 0
%ctz = call i32 @llvm.ctlz.i32(i32 %arg, i1 true)
@@ -1011,12 +1010,11 @@ define <2 x i32> @test_cttz_not_bw_odd_mul_vec_nonsplat(<2 x i32> %x) {
define i64 @fold_ctlz_hex_digits(i64 noundef %x) {
; CHECK-LABEL: @fold_ctlz_hex_digits(
-; CHECK-NEXT: [[ISZERO:%.*]] = icmp eq i64 [[X:%.*]], 0
-; CHECK-NEXT: [[LZ:%.*]] = call range(i64 0, 65) i64 @llvm.ctlz.i64(i64 [[X]], i1 true)
+; CHECK-NEXT: [[CTLZ_NONZERO:%.*]] = or i64 [[X:%.*]], 1
+; CHECK-NEXT: [[LZ:%.*]] = call range(i64 0, 64) i64 @llvm.ctlz.i64(i64 [[CTLZ_NONZERO]], i1 true)
; CHECK-NEXT: [[BIAS:%.*]] = sub nuw nsw i64 67, [[LZ]]
; CHECK-NEXT: [[DIGITS:%.*]] = lshr i64 [[BIAS]], 2
-; CHECK-NEXT: [[RESULT:%.*]] = select i1 [[ISZERO]], i64 1, i64 [[DIGITS]]
-; CHECK-NEXT: ret i64 [[RESULT]]
+; CHECK-NEXT: ret i64 [[DIGITS]]
;
%iszero = icmp eq i64 %x, 0
%lz = call i64 @llvm.ctlz.i64(i64 %x, i1 true)
@@ -1028,12 +1026,11 @@ define i64 @fold_ctlz_hex_digits(i64 noundef %x) {
define i64 @fold_ctlz_hex_digits_ne_commuted(i64 noundef %x) {
; CHECK-LABEL: @fold_ctlz_hex_digits_ne_commuted(
-; CHECK-NEXT: [[ISNOTZERO_NOT:%.*]] = icmp eq i64 [[X:%.*]], 0
-; CHECK-NEXT: [[LZ:%.*]] = call range(i64 0, 65) i64 @llvm.ctlz.i64(i64 [[X]], i1 true)
+; CHECK-NEXT: [[CTLZ_NONZERO:%.*]] = or i64 [[X:%.*]], 1
+; CHECK-NEXT: [[LZ:%.*]] = call range(i64 0, 64) i64 @llvm.ctlz.i64(i64 [[CTLZ_NONZERO]], i1 true)
; CHECK-NEXT: [[BIAS:%.*]] = sub nuw nsw i64 67, [[LZ]]
; CHECK-NEXT: [[DIGITS:%.*]] = lshr i64 [[BIAS]], 2
-; CHECK-NEXT: [[RESULT:%.*]] = select i1 [[ISNOTZERO_NOT]], i64 1, i64 [[DIGITS]]
-; CHECK-NEXT: ret i64 [[RESULT]]
+; CHECK-NEXT: ret i64 [[DIGITS]]
;
%isnotzero = icmp ne i64 0, %x
%lz = call i64 @llvm.ctlz.i64(i64 %x, i1 true)
@@ -1045,11 +1042,10 @@ define i64 @fold_ctlz_hex_digits_ne_commuted(i64 noundef %x) {
define i64 @fold_ctlz_hex_digits_exact(i64 noundef %x) {
; CHECK-LABEL: @fold_ctlz_hex_digits_exact(
-; CHECK-NEXT: [[ISZERO:%.*]] = icmp eq i64 [[X:%.*]], 0
-; CHECK-NEXT: [[LZ:%.*]] = call range(i64 0, 65) i64 @llvm.ctlz.i64(i64 [[X]], i1 true)
+; CHECK-NEXT: [[CTLZ_NONZERO:%.*]] = or i64 [[X:%.*]], 1
+; CHECK-NEXT: [[LZ:%.*]] = call range(i64 0, 64) i64 @llvm.ctlz.i64(i64 [[CTLZ_NONZERO]], i1 true)
; CHECK-NEXT: [[BIAS:%.*]] = sub nuw nsw i64 67, [[LZ]]
-; CHECK-NEXT: [[DIGITS1:%.*]] = lshr exact i64 [[BIAS]], 2
-; CHECK-NEXT: [[DIGITS:%.*]] = select i1 [[ISZERO]], i64 1, i64 [[DIGITS1]]
+; CHECK-NEXT: [[DIGITS:%.*]] = lshr i64 [[BIAS]], 2
; CHECK-NEXT: ret i64 [[DIGITS]]
;
%iszero = icmp eq i64 %x, 0
@@ -1099,7 +1095,8 @@ define i64 @do_not_fold_ctlz_hex_digits_wrong_zero_value(i64 noundef %x) {
; The generic equivalence fold drops nuw because it would be poison at X == 0.
define i16 @fold_ctlz_drop_nuw_on_zero(i16 noundef %x) {
; CHECK-LABEL: @fold_ctlz_drop_nuw_on_zero(
-; CHECK-NEXT: [[LZ:%.*]] = call range(i16 0, 17) i16 @llvm.ctlz.i16(i16 [[X:%.*]], i1 false)
+; CHECK-NEXT: [[CTLZ_NONZERO:%.*]] = or i16 [[X:%.*]], 1
+; CHECK-NEXT: [[LZ:%.*]] = call range(i16 0, 16) i16 @llvm.ctlz.i16(i16 [[CTLZ_NONZERO]], i1 true)
; CHECK-NEXT: [[BIAS:%.*]] = sub nsw i16 10, [[LZ]]
; CHECK-NEXT: [[DIGITS:%.*]] = lshr i16 [[BIAS]], 1
; CHECK-NEXT: ret i16 [[DIGITS]]
@@ -1115,7 +1112,8 @@ define i16 @fold_ctlz_drop_nuw_on_zero(i16 noundef %x) {
; The generic equivalence fold drops nsw because it would be poison at X == 0.
define i16 @fold_ctlz_drop_nsw_on_zero(i16 noundef %x) {
; CHECK-LABEL: @fold_ctlz_drop_nsw_on_zero(
-; CHECK-NEXT: [[LZ:%.*]] = call range(i16 0, 17) i16 @llvm.ctlz.i16(i16 [[X:%.*]], i1 false)
+; CHECK-NEXT: [[CTLZ_NONZERO:%.*]] = or i16 [[X:%.*]], 1
+; CHECK-NEXT: [[LZ:%.*]] = call range(i16 0, 16) i16 @llvm.ctlz.i16(i16 [[CTLZ_NONZERO]], i1 true)
; CHECK-NEXT: [[BIAS:%.*]] = sub nuw i16 -32760, [[LZ]]
; CHECK-NEXT: [[DIGITS:%.*]] = lshr i16 [[BIAS]], 1
; CHECK-NEXT: ret i16 [[DIGITS]]
@@ -1131,7 +1129,8 @@ define i16 @fold_ctlz_drop_nsw_on_zero(i16 noundef %x) {
; The generic equivalence fold drops exact because it would be poison at X == 0.
define i16 @fold_ctlz_drop_exact_on_zero(i16 noundef %x) {
; CHECK-LABEL: @fold_ctlz_drop_exact_on_zero(
-; CHECK-NEXT: [[LZ:%.*]] = call range(i16 0, 17) i16 @llvm.ctlz.i16(i16 [[X:%.*]], i1 false)
+; CHECK-NEXT: [[CTLZ_NONZERO:%.*]] = or i16 [[X:%.*]], 1
+; CHECK-NEXT: [[LZ:%.*]] = call range(i16 0, 16) i16 @llvm.ctlz.i16(i16 [[CTLZ_NONZERO]], i1 true)
; CHECK-NEXT: [[BIAS:%.*]] = sub nuw nsw i16 20, [[LZ]]
; CHECK-NEXT: [[DIGITS:%.*]] = lshr i16 [[BIAS]], 2
; CHECK-NEXT: ret i16 [[DIGITS]]
More information about the llvm-commits
mailing list