Skip to content

Recognize bit-manipulation idioms and emit bzhi/bts/btc/btr/bt on xarch#130481

Open
tannergooding wants to merge 8 commits into
dotnet:mainfrom
tannergooding:tannergooding-jit-recognize-bit-manipulation-patterns
Open

Recognize bit-manipulation idioms and emit bzhi/bts/btc/btr/bt on xarch#130481
tannergooding wants to merge 8 commits into
dotnet:mainfrom
tannergooding:tannergooding-jit-recognize-bit-manipulation-patterns

Conversation

@tannergooding

Copy link
Copy Markdown
Member

Lowers several single-bit and bit-field idioms to dedicated x86 instructions on xarch:

Idiom Instruction
x & ((1 << y) - 1) bzhi (reuses NI_AVX2_ZeroHighBits)
x | (1 << y) bts (GT_BIT_SET)
x & ~(1 << y) btr (GT_BIT_CLEAR)
x ^ (1 << y) btc (GT_BIT_INVERT)
(x >> y) & 1 in == 0 / != 0 bt (GT_BITTEST_EQ/GT_BITTEST_NE)

Details

  • bzhi is recognized in LowerBinaryArithmetic and folded into the existing AVX2 scalar intrinsic, so containment and memory operands come for free. It matches both the ADD(-1) and SUB(1) mask forms and only fires for a variable shift amount, since a constant amount folds to a plain and with an immediate.
  • bts/btr/btc reuse the existing GT_BIT_SET/GT_BIT_CLEAR/GT_BIT_INVERT opers (shared with riscv64) with xarch codegen, LSRA, and emitter support. Only the reg,reg form is used: the value operand is kept in a register so the implicit masking of the bit index (mod the operand width) matches the shl semantics, and because the memory-destination forms are slower. They likewise only fire for a variable bit index.
  • bt recognizes (x >> y) & 1 feeding a == 0 / != 0 and emits the flag-producing bit test.

bextr deferred

#81514 also covers bextr, but constant-control bextr ((x >> c) & mask) is intentionally left out: it must materialize the control word and is multi-uop, so shr/sar + and in place is smaller and faster. SPMI confirmed it was a net regression (+7,671 bytes) and it only wins when the source is a live-after local (saving a preservation copy) — which needs last-use info not available during lowering. The fully-variable (x >> y) & ((1 << z) - 1) case already lowers optimally to shrx + bzhi, so nothing is lost.

A cost-aware follow-up prototyped the variable/mixed-control cases (the only ones not previously implemented) and measured them in isolation with SPMI asmdiffs over 2.6M contexts: +61 bytes, zero improvements, every diff a regression. Non-constant control does not avoid the materialization — it replaces the free and reg, imm mask with a multi-instruction packed-control build feeding a multi-uop bextr, and shrx/bzhi already provide the non-destructive form that was bextr's only structural edge. So bextr is left out in all quadrants.

Diffs

SPMI asmdiffs (x64 windows): overall -3,874 bytes, every collection improves in both size and PerfScore (coreclr_tests PerfScore -5.09%), no asserts, no replay failures. The only regressions are 6 methods at +1/+2 bytes each (memory-RMW bts where the memory destination can't be contained without breaking the bit-index masking) — negligible against the wins.

Contributes to #81514
Fixes #81512

Note

This PR description was generated with the assistance of GitHub Copilot.

tannergooding and others added 3 commits July 9, 2026 22:10
Lowers several single-bit and bit-field idioms to dedicated x86 instructions:

  x & ((1 << y) - 1)  -> bzhi  (reuses NI_AVX2_ZeroHighBits)
  x | (1 << y)        -> bts   (GT_BIT_SET)
  x & ~(1 << y)       -> btr   (GT_BIT_CLEAR)
  x ^ (1 << y)        -> btc   (GT_BIT_INVERT)

bzhi is recognized in LowerBinaryArithmetic and folded into the existing AVX2
scalar intrinsic, so containment and memory operands work for free. It matches
both the ADD(-1) and SUB(1) mask forms and only fires for a variable shift
amount, since a constant amount folds to a plain and with an immediate.

The bts/btr/btc patterns reuse the existing GT_BIT_SET/GT_BIT_CLEAR/GT_BIT_INVERT
opers (shared with riscv64) with xarch codegen, LSRA, and emitter support. Only
the reg,reg form is used: the value operand is kept in a register so the implicit
masking of the bit index (mod the operand width) matches the shl semantics, and
because the memory-destination forms are slower. They likewise only fire for a
variable bit index, since a constant index folds to a plain or/and/xor with an
immediate.

bextr (constant-control field extract) is intentionally left out: it must
materialize the control word and is multi-uop, so shr/sar + and in place is
smaller and faster; the fully-variable (x >> y) & ((1 << z) - 1) case already
lowers optimally to shrx + bzhi. This can be revisited with a cost/liveness-aware
heuristic in a follow-up.

Contributes to dotnet#81514
Fixes dotnet#81512

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Extend tryReduceSingleBitTestOps to also match AND(RSH|RSZ(x, y), 1) so that a
single-bit test written as ((x >> y) & 1) feeding a branch lowers to bt, the
same as the already-handled x & (1 << y) shape. Only bit 0 of the shifted value
is kept so the shift kind is irrelevant, and bt masks the bit index modulo the
operand size, matching the C# masked-shift semantics even for an out-of-range y.

Restricted to a variable index since a constant index keeps the shift and bt has
no immediate form here (the existing constant-mask test is already optimal). The
value operand's containment is cleared because ContainCheckCompare is skipped on
success and the reg,reg bt form requires it in a register.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 10, 2026 12:07
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 10, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR extends xarch JIT lowering/codegen to recognize common single-bit and bit-field idioms and emit dedicated x86 instructions (bts/btr/btc, bt, and bzhi via the existing ZeroHighBits intrinsic). It also adds/updates JIT tests to validate the new instruction selection.

Changes:

  • Add lowering for single-bit set/clear/invert idioms to new GT_BIT_SET/GT_BIT_CLEAR/GT_BIT_INVERT nodes and emit bts/btr/btc in xarch codegen.
  • Extend compare/test optimization to recognize the ((x >> y) & 1) shape and lower it to GT_BITTEST_EQ/NE (xarch bt).
  • Add/adjust instruction-combining tests, including disasm checks for the new bit ops and a new bzhi correctness test.
Show a summary per file
File Description
src/tests/JIT/opt/InstructionCombining/SingleBit.csproj Enables disasm checks for SingleBit.cs.
src/tests/JIT/opt/InstructionCombining/SingleBit.cs Adds disasm-validated coverage for bts/btr/btc and bt, plus 64-bit variants.
src/tests/JIT/opt/InstructionCombining/Bzhi.csproj Adds a new instruction-combining test project for bzhi coverage.
src/tests/JIT/opt/InstructionCombining/Bzhi.cs Adds randomized correctness checks intended to cover x & ((1 << y) - 1) lowering.
src/coreclr/jit/lsraxarch.cpp Teaches LSRA to treat GT_BIT_* nodes as binary ops for use/def modeling.
src/coreclr/jit/lowerxarch.cpp Adds new pattern-based lowering for bzhi and GT_BIT_* nodes; adjusts some existing bit-idiom lowering to accept SUB forms.
src/coreclr/jit/lower.h Declares the new xarch lowering helpers.
src/coreclr/jit/lower.cpp Extends const-compare optimization to recognize ((x >> y) & 1) for bit-test lowering.
src/coreclr/jit/instrsxarch.h Adds instruction definitions for bts/btr/btc.
src/coreclr/jit/emitxarch.cpp Adjusts instruction display for BT-family operand reversal for bts/btr/btc.
src/coreclr/jit/codegenxarch.cpp Implements codegen for GT_BIT_SET/CLEAR/INVERT using bts/btr/btc.
src/coreclr/jit/codegen.h Declares genCodeForBitOp.
src/coreclr/jit/gtlist.h Enables GT_BIT_* nodes for xarch (shared with riscv64).

Copilot's findings

  • Files reviewed: 13/13 changed files
  • Comments generated: 7

Comment thread src/coreclr/jit/lowerxarch.cpp Outdated
Comment thread src/coreclr/jit/lowerxarch.cpp Outdated
Comment thread src/coreclr/jit/lowerxarch.cpp Outdated
Comment thread src/coreclr/jit/lower.h Outdated
Comment thread src/coreclr/jit/lower.cpp
Comment thread src/coreclr/jit/codegenxarch.cpp
Comment thread src/tests/JIT/opt/InstructionCombining/Bzhi.cs Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 10, 2026 12:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 13/13 changed files
  • Comments generated: 10

Comment thread src/tests/JIT/opt/InstructionCombining/SingleBit.cs
Comment thread src/tests/JIT/opt/InstructionCombining/SingleBit.cs
Comment thread src/tests/JIT/opt/InstructionCombining/SingleBit.cs
Comment thread src/tests/JIT/opt/InstructionCombining/SingleBit.cs
Comment thread src/tests/JIT/opt/InstructionCombining/SingleBit.cs
Comment thread src/coreclr/jit/lower.cpp
Comment thread src/coreclr/jit/lowerxarch.cpp Outdated
Comment thread src/coreclr/jit/codegenxarch.cpp
Comment thread src/tests/JIT/opt/InstructionCombining/SingleBit.cs
Comment thread src/tests/JIT/opt/InstructionCombining/SingleBit.cs
C# defines 1 << y as masking the shift count modulo the operand width, and
this JIT already models shl/shr as masked (LowerShift drops redundant & 31/
& 63). �zhi instead leaves the source unchanged when the index is >= width,
so lowering AND(x, (1 << y) - 1) to �zhi(x, y) miscompiled the source pattern
for out-of-range y (e.g. Bzhi_I(x, 32) returned x instead of 0). Mask the index
to the operand width before �zhi so it reproduces the masked-shift result
exactly; the extra �nd reg, imm is contained and the transform stays a net
size/PerfScore win in SPMI (-3,425 bytes overall, all collections green).

Also address related review feedback: rewrite the header comment to document the
masked-index invariant, move TryLowerBitwiseOpToBitOp out of the FEATURE_HW_INTRINSICS
region (bts/btr/btc are not hardware intrinsics), add a defensive assert in
genCodeForBitOp documenting the LSRA delayFree invariant on the bit index, and
expand the Bzhi test to cover indices past the operand width via an oracle that
routes through an opaque masked shift.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 10, 2026 14:21
The disasm checks used `{{[a-z]+}}` for register operands, which only matches
registers made purely of letters (eax/ecx/rax). When register allocation places
an operand in r8-r15 (e.g. `bts r8d, r10d`) the pattern fails to match and the
check spuriously fails; it passes today only when RA happens to pick a lettered
register. Widen the operand patterns to `{{[a-z0-9]+}}`.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 13/13 changed files
  • Comments generated: 3

Comment thread src/coreclr/jit/lower.cpp
Comment thread src/coreclr/jit/codegenxarch.cpp Outdated
Comment thread src/tests/JIT/opt/InstructionCombining/SingleBit.cs Outdated
Copilot AI review requested due to automatic review settings July 10, 2026 14:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new

…diate form

The assert(targetReg != op2) added for the GT_BIT_* codegen is unsound: for
x <op> (1 << x) op1 and op2 share an interval, so AddDelayFreeUses skips the
delayFree and LSRA may place the destination on op2's register. That is safe --
op1 and op2 hold the same value, so the mov writes op2's own value back -- but
it can't be expressed as a cheap codegen assert. JitStressRegs=0x800 reaches the
aliased form and computes the correct result. Also widen the Set10 FileCheck to
accept 1024 or 0x400, matching the file's other immediate patterns.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 10, 2026 14:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 13/13 changed files
  • Comments generated: 1

Comment thread src/coreclr/jit/lowerxarch.cpp
…x contract

TryLowerAndOpToZeroHighBits documented that the shift count is never constant
here (a constant 1 << Y folds to a constant mask during morph), but nothing
enforced it. Like the un-canonicalized SUB form the function already guards
against, a post-morph constant index could reach here and regress the optimal
and reg, imm into mov reg, imm + bzhi, since bzhi has no immediate index form.
Bail explicitly on a constant index. SPMI x64 confirms this is codegen-neutral:
clean diff across all 10 collections (~2.6M contexts).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 10, 2026 15:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new

@tannergooding

Copy link
Copy Markdown
Member Author

@MihuBot -nuget

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[JIT] X64 - Recognize patterns to emit bts, btc, btr instructions

2 participants