Skip to content

[InstCombine] Let foldSelectBitTest use known bits for the single-bit mask - #2

Open
ThePo1es wants to merge 3 commits into
base-206708from
issue206708-main
Open

ThePo1es wants to merge 3 commits into
base-206708from
issue206708-main

Conversation

@ThePo1es

@ThePo1es ThePo1es commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Fork PR for mentor review before opening upstream. Fixes llvm#206708.

foldSelectBitTest only recognizes an equality bit test when the compared
value is literally and X, Pow2. Known bits from an llvm.assume, a range
attribute, or a dominating branch can establish the same single-bit fact
without the and being spelled out:

%c = icmp ult i32 %a, 2
call void @llvm.assume(i1 %c)
%cmp = icmp eq i32 %a, 0
%res = select i1 %cmp, i32 1, i32 2
  =>
%res = add nuw nsw i32 %a, 1

This falls back to computeKnownBits when the and pattern does not match,
the same way the decomposeBitTestICmp path directly below already refines
its mask with known bits. The query is only reached for selects whose arms
already passed the cheap structural checks hoisted to the function entry in
the preceding NFC commit, which also removes the pre-existing known-bits
query on the decomposeBitTestICmp path for selects that can never fold.

Proofs (generalized, symbolic C / single-bit P)

Compile-time and real-world impact

  • llvm-opt-benchmark (local run over the 2,784 original inputs, instcombine only): no IR changes, no regressions.
  • compile-time-tracker: not run yet (will be requested in the upstream PR)

Notes

  • Pointer compares are rejected explicitly (isIntOrIntVectorTy): the consumers zext/trunc the compared value.
  • No one-use check is needed on the compare: CreateAnd is false on this path, so the transform is instruction-count neutral even when the compare has other users (@select_icmp_bittest_range_multiuse_cmp).
  • Canonicalizing icmp eq X, 0 to trunc nuw in foldICmpUsingKnownBits would only cover the bit-0 case; @select_icmp_bittest_assume_high_bit (the single known bit is the sign bit) is not expressible that way.
  • Pre-commit tests are in the first commit, including negative tests for a non-power-of-two known range, a fully unknown operand, non-adjacent select arms, and pointer compares.

Closes llvm#206708 (upstream).

Thanks to @Kristianerik, who described the same idea in the issue.

AI tool usage

Per the LLVM AI Tool Use Policy: AI assistance was used while writing parts of this patch and its tests. All transformations were verified with Alive2 (links above), llvm-lit passes over llvm/test/Transforms/InstCombine with an assertions-enabled build, and I reviewed the final implementation and every test expectation myself.

} else if (!match(FalseVal, m_BinOp(m_Specific(TrueVal), m_Power2())) &&
!match(TrueVal, m_BinOp(m_Specific(FalseVal), m_Power2()))) {
return nullptr;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style-wise, I think this looks better, but the current version also seems perfectly fine to submit as-is.

const APInt *TC, *FC;
bool ConstArmsCanFold =
    match(TrueVal, m_APInt(TC)) && match(FalseVal, m_APInt(FC)) &&
    ((*TC ^ *FC).isPowerOf2() || (*TC - *FC).isPowerOf2() ||
     (*FC - *TC).isPowerOf2());

bool BinOpArmsCanFold =
    match(FalseVal, m_BinOp(m_Specific(TrueVal), m_Power2())) ||
    match(TrueVal, m_BinOp(m_Specific(FalseVal), m_Power2()));

if (!ConstArmsCanFold && !BinOpArmsCanFold)
  return nullptr;

Both foldSelectICmpAnd and foldSelectICmpAndBinOp only handle selects
whose arms are constants, or differ by a binop with a power-of-two
constant. With constant arms, foldSelectICmpAnd additionally needs them
to differ by a single bit (or/xor) or by a power of two (add/sub). Check
all of that up front, before decomposing the condition and before the
known-bits query on the decomposeBitTestICmp path.

The check is symmetric in the arms, so the later arm swap for ICMP_NE
does not affect it, and it only skips selects the folds reject anyway.

Assisted-by: Claude Code
foldSelectBitTest only recognizes an equality bit test when the compared
value is literally `and X, Pow2`. Known bits from an assume, a range
attribute or a dominating branch can establish the same single-bit fact
without the and being spelled out:

  %c = icmp ult i32 %a, 2
  call void @llvm.assume(i1 %c)
  %cmp = icmp eq i32 %a, 0
  %res = select i1 %cmp, i32 1, i32 2
  =>
  %res = add nuw nsw i32 %a, 1

Fall back to computeKnownBits when the and pattern does not match, the
same way the decomposeBitTestICmp path below already refines its mask.
The query only runs for selects whose arms passed the cheap checks at
the top of the function, so the common `x == 0 ? C1 : C2` shapes that
could never fold do not reach it.

The fallback is limited to selects that already have the compared
value's type. Without an and to remove, a narrower or wider select
would need a cast plus a shift, which is no cheaper than icmp+select
and blocks the later folds of the select with its users (seen as
regressions in llvm-opt-benchmark on getRegState and qemu's csr).

Pointer compares are rejected explicitly: computeKnownBits handles them,
but the folds zext/trunc the compared value.

Proofs:
- add, eq and ne forms, symbolic C: https://alive2.llvm.org/ce/z/8oJ_7N
- add/or/xor/sub, symbolic C and P: https://alive2.llvm.org/ce/z/q4Zeg2
- concrete shapes and flag boundaries: https://alive2.llvm.org/ce/z/vwT7Ke
- negatives (range(0,3); nsw on the INT_MAX arm): https://alive2.llvm.org/ce/z/m5zjKW

Closes llvm#206708

Assisted-by: Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants