[llvm] [AArch64] Fold an equality compare of a masked value into UBFX + CMP #imm (PR #228958)

Andrew Gaul via llvm-commits llvm-commits at lists.llvm.org
Sun Oct 4 17:15:41 PDT 2026


https://github.com/gaul created https://github.com/llvm/llvm-project/pull/228958

For `(and X, Mask) == C` where Mask is one run of bits, C lies inside
it and is not a compare immediate, but C shifted down by the mask's
trailing zeros is, shift both sides down in getAArch64Cmp. The AND and
the shift select as a UBFX and the compare takes an immediate:
```
  and  w8, w0, #0xfc00          ubfx w8, w0, #10, #6
  mov  w9, #0xd800        ->    cmp  w8, #0x36
  cmp  w8, w9
```
The shape is a UTF-16 surrogate test, (c & 0xfc00) == 0xd800, and any
field compare where the field's constant does not fit 12 bits in
place. Only equality compares, and only when the AND has no other use
(otherwise the AND stays and nothing is saved). A mask reaching the top
bit already folds to LSR + CMP; the imm12 and shifted-imm12 forms both
apply after the shift.

Firefox XUL (arm64, 30.3M instructions) has 562 such compares and 69
more under CCMP, which this does not touch.

Found via armlint.

>From f92ec710a98cf605189e6e12a6bc499db2d96b6a Mon Sep 17 00:00:00 2001
From: Andrew Gaul <andrew at gaul.org>
Date: Sun, 4 Oct 2026 17:01:46 -0700
Subject: [PATCH 1/2] [AArch64] Add tests for equality compares of a masked
 value (NFC)

Precommit tests for folding `(and X, Mask) == C` into a UBFX and a
compare immediate when C does not fit the compare but C shifted down by
the mask's trailing zeros does. The checks show the current codegen: a
MOV of the constant, the AND, and a register compare.

Found via armlint.

Co-Authored-By: Claude Fable 5.1 <noreply at anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZMjy58X6Z2sH8fd4SSRi4
---
 .../test/CodeGen/AArch64/cmp-and-mask-ubfx.ll | 147 ++++++++++++++++++
 1 file changed, 147 insertions(+)
 create mode 100644 llvm/test/CodeGen/AArch64/cmp-and-mask-ubfx.ll

diff --git a/llvm/test/CodeGen/AArch64/cmp-and-mask-ubfx.ll b/llvm/test/CodeGen/AArch64/cmp-and-mask-ubfx.ll
new file mode 100644
index 0000000000000..a6adb11796160
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/cmp-and-mask-ubfx.ll
@@ -0,0 +1,147 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 6
+; RUN: llc -mtriple=aarch64-unknown-linux-gnu < %s | FileCheck %s
+
+; An equality compare of a masked value against a constant inside the
+; mask, where the constant is not a compare immediate but the constant
+; shifted down by the mask's trailing zeros is: shift both sides down,
+; so the AND + shift select as a UBFX and the compare takes an immediate.
+
+; (c & 0xfc00) == 0xd800: a UTF-16 high surrogate test.
+define i1 @surrogate_eq(i32 %c) {
+; CHECK-LABEL: surrogate_eq:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    mov w8, #55296 // =0xd800
+; CHECK-NEXT:    and w9, w0, #0xfc00
+; CHECK-NEXT:    cmp w9, w8
+; CHECK-NEXT:    cset w0, eq
+; CHECK-NEXT:    ret
+  %m = and i32 %c, 64512
+  %r = icmp eq i32 %m, 55296
+  ret i1 %r
+}
+
+define i1 @surrogate_ne(i32 %c) {
+; CHECK-LABEL: surrogate_ne:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    mov w8, #55296 // =0xd800
+; CHECK-NEXT:    and w9, w0, #0xfc00
+; CHECK-NEXT:    cmp w9, w8
+; CHECK-NEXT:    cset w0, ne
+; CHECK-NEXT:    ret
+  %m = and i32 %c, 64512
+  %r = icmp ne i32 %m, 55296
+  ret i1 %r
+}
+
+define i1 @mask_i64(i64 %x) {
+; CHECK-LABEL: mask_i64:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    and x8, x0, #0x3fc00
+; CHECK-NEXT:    sub x8, x8, #54, lsl #12 // =221184
+; CHECK-NEXT:    cmp x8, #1024
+; CHECK-NEXT:    cset w0, eq
+; CHECK-NEXT:    ret
+  %m = and i64 %x, 261120
+  %r = icmp eq i64 %m, 222208
+  ret i1 %r
+}
+
+; The shifted constant uses the 12-bit immediate's shifted form.
+define i1 @shifted_imm12(i64 %x) {
+; CHECK-LABEL: shifted_imm12:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    mov x8, #4398046511104 // =0x40000000000
+; CHECK-NEXT:    and x9, x0, #0xfffffffff000000
+; CHECK-NEXT:    cmp x9, x8
+; CHECK-NEXT:    cset w0, eq
+; CHECK-NEXT:    ret
+  %m = and i64 %x, 1152921504590069760
+  %r = icmp eq i64 %m, 4398046511104
+  ret i1 %r
+}
+
+define i32 @branch(i32 %c, i32 %a, i32 %b) {
+; CHECK-LABEL: branch:
+; CHECK:       // %bb.0: // %common.ret
+; CHECK-NEXT:    and w8, w0, #0xfc00
+; CHECK-NEXT:    mov w9, #55296 // =0xd800
+; CHECK-NEXT:    cmp w8, w9
+; CHECK-NEXT:    csel w0, w1, w2, eq
+; CHECK-NEXT:    ret
+  %m = and i32 %c, 64512
+  %r = icmp eq i32 %m, 55296
+  br i1 %r, label %t, label %f
+t:
+  ret i32 %a
+f:
+  ret i32 %b
+}
+
+; The mask reaches the top bit: a plain shift suffices.
+define i1 @high_mask(i32 %x) {
+; CHECK-LABEL: high_mask:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    lsr w8, w0, #30
+; CHECK-NEXT:    cmp w8, #2
+; CHECK-NEXT:    cset w0, eq
+; CHECK-NEXT:    ret
+  %m = and i32 %x, -1073741824
+  %r = icmp eq i32 %m, -2147483648
+  ret i1 %r
+}
+
+; The constant already fits a compare immediate: nothing to gain.
+define i1 @imm_fits(i32 %x) {
+; CHECK-LABEL: imm_fits:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    and w8, w0, #0xf0
+; CHECK-NEXT:    cmp w8, #48
+; CHECK-NEXT:    cset w0, eq
+; CHECK-NEXT:    ret
+  %m = and i32 %x, 240
+  %r = icmp eq i32 %m, 48
+  ret i1 %r
+}
+
+; The shifted constant still does not fit.
+define i1 @shifted_too_big(i64 %x) {
+; CHECK-LABEL: shifted_too_big:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    mov x8, #-9007199254740992 // =0xffe0000000000000
+; CHECK-NEXT:    cmp x8, x0, lsl #1
+; CHECK-NEXT:    cset w0, eq
+; CHECK-NEXT:    ret
+  %m = and i64 %x, 9223372036854775807
+  %r = icmp eq i64 %m, 9218868437227405312
+  ret i1 %r
+}
+
+; The mask is not one run of bits.
+define i1 @split_mask(i32 %x) {
+; CHECK-LABEL: split_mask:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    mov w8, #61455 // =0xf00f
+; CHECK-NEXT:    mov w9, #4099 // =0x1003
+; CHECK-NEXT:    and w8, w0, w8
+; CHECK-NEXT:    cmp w8, w9
+; CHECK-NEXT:    cset w0, eq
+; CHECK-NEXT:    ret
+  %m = and i32 %x, 61455
+  %r = icmp eq i32 %m, 4099
+  ret i1 %r
+}
+
+; The masked value has another use, so the AND stays either way.
+define i32 @multi_use(i32 %c) {
+; CHECK-LABEL: multi_use:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    mov w8, #55296 // =0xd800
+; CHECK-NEXT:    and w9, w0, #0xfc00
+; CHECK-NEXT:    cmp w9, w8
+; CHECK-NEXT:    csel w0, w9, wzr, eq
+; CHECK-NEXT:    ret
+  %m = and i32 %c, 64512
+  %r = icmp eq i32 %m, 55296
+  %s = select i1 %r, i32 %m, i32 0
+  ret i32 %s
+}

>From dee622141d7f381a768e11aa0452d8d2a613528e Mon Sep 17 00:00:00 2001
From: Andrew Gaul <andrew at gaul.org>
Date: Sun, 4 Oct 2026 17:04:53 -0700
Subject: [PATCH 2/2] [AArch64] Fold an equality compare of a masked value into
 UBFX + CMP #imm

For `(and X, Mask) == C` where Mask is one run of bits, C lies inside
it and is not a compare immediate, but C shifted down by the mask's
trailing zeros is, shift both sides down in getAArch64Cmp. The AND and
the shift select as a UBFX and the compare takes an immediate:

  and  w8, w0, #0xfc00          ubfx w8, w0, #10, #6
  mov  w9, #0xd800        ->    cmp  w8, #0x36
  cmp  w8, w9

The shape is a UTF-16 surrogate test, (c & 0xfc00) == 0xd800, and any
field compare where the field's constant does not fit 12 bits in
place. Only equality compares, and only when the AND has no other use
(otherwise the AND stays and nothing is saved). A mask reaching the top
bit already folds to LSR + CMP; the imm12 and shifted-imm12 forms both
apply after the shift.

Firefox XUL (arm64, 30.3M instructions) has 562 such compares and 69
more under CCMP, which this does not touch.

Found via armlint.

Co-Authored-By: Claude Fable 5.1 <noreply at anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZMjy58X6Z2sH8fd4SSRi4
---
 .../Target/AArch64/AArch64ISelLowering.cpp    | 28 +++++++++++++++++++
 .../test/CodeGen/AArch64/cmp-and-mask-ubfx.ll | 25 +++++++----------
 2 files changed, 38 insertions(+), 15 deletions(-)

diff --git a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
index 2ddc7fe934430..e22c4024a2971 100644
--- a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
+++ b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
@@ -4580,6 +4580,34 @@ static SDValue getAArch64Cmp(SDValue LHS, SDValue RHS, ISD::CondCode CC,
   if (isIntEqualitySetCC(CC) && isa<ConstantSDNode>(RHS)) {
     const ConstantSDNode *RHSC = cast<ConstantSDNode>(RHS);
 
+    // (and X, Mask) == C, with Mask one run of bits, C inside it and not a
+    // compare immediate, but C >> trailing-zeros(Mask) one: shift both
+    // sides down. The AND and the shift select as a UBFX, and the compare
+    // takes an immediate, so
+    //   and  w8, w0, #0xfc00
+    //   mov  w9, #0xd800
+    //   cmp  w8, w9
+    // becomes
+    //   ubfx w8, w0, #10, #6
+    //   cmp  w8, #0x36
+    if (LHS.getOpcode() == ISD::AND && LHS.hasOneUse() &&
+        !AArch64_AM::isLegalCmpImmed(RHSC->getAPIntValue())) {
+      EVT VT = LHS.getValueType();
+      if (auto *MaskC = dyn_cast<ConstantSDNode>(LHS.getOperand(1))) {
+        const APInt &Mask = MaskC->getAPIntValue();
+        const APInt &C = RHSC->getAPIntValue();
+        unsigned Shift = Mask.countr_zero();
+        if (Shift != 0 && Shift < VT.getSizeInBits() && Mask.isShiftedMask() &&
+            C.isSubsetOf(Mask) && !C.isZero() &&
+            AArch64_AM::isLegalArithImmed(C.lshr(Shift).getZExtValue())) {
+          LHS = DAG.getNode(ISD::SRL, DL, VT, LHS,
+                            DAG.getShiftAmountConstant(Shift, VT, DL));
+          RHS = DAG.getConstant(C.lshr(Shift), DL, VT);
+          RHSC = cast<ConstantSDNode>(RHS);
+        }
+      }
+    }
+
     // The imm operand of ADDS is an unsigned immediate, in the range 0 to 4095.
     // For the i8 operand, the largest immediate is 255, so this can be easily
     // encoded in the compare instruction. For the i16 operand, however, the
diff --git a/llvm/test/CodeGen/AArch64/cmp-and-mask-ubfx.ll b/llvm/test/CodeGen/AArch64/cmp-and-mask-ubfx.ll
index a6adb11796160..171e4a8d4aeee 100644
--- a/llvm/test/CodeGen/AArch64/cmp-and-mask-ubfx.ll
+++ b/llvm/test/CodeGen/AArch64/cmp-and-mask-ubfx.ll
@@ -10,9 +10,8 @@
 define i1 @surrogate_eq(i32 %c) {
 ; CHECK-LABEL: surrogate_eq:
 ; CHECK:       // %bb.0:
-; CHECK-NEXT:    mov w8, #55296 // =0xd800
-; CHECK-NEXT:    and w9, w0, #0xfc00
-; CHECK-NEXT:    cmp w9, w8
+; CHECK-NEXT:    ubfx w8, w0, #10, #6
+; CHECK-NEXT:    cmp w8, #54
 ; CHECK-NEXT:    cset w0, eq
 ; CHECK-NEXT:    ret
   %m = and i32 %c, 64512
@@ -23,9 +22,8 @@ define i1 @surrogate_eq(i32 %c) {
 define i1 @surrogate_ne(i32 %c) {
 ; CHECK-LABEL: surrogate_ne:
 ; CHECK:       // %bb.0:
-; CHECK-NEXT:    mov w8, #55296 // =0xd800
-; CHECK-NEXT:    and w9, w0, #0xfc00
-; CHECK-NEXT:    cmp w9, w8
+; CHECK-NEXT:    ubfx w8, w0, #10, #6
+; CHECK-NEXT:    cmp w8, #54
 ; CHECK-NEXT:    cset w0, ne
 ; CHECK-NEXT:    ret
   %m = and i32 %c, 64512
@@ -36,9 +34,8 @@ define i1 @surrogate_ne(i32 %c) {
 define i1 @mask_i64(i64 %x) {
 ; CHECK-LABEL: mask_i64:
 ; CHECK:       // %bb.0:
-; CHECK-NEXT:    and x8, x0, #0x3fc00
-; CHECK-NEXT:    sub x8, x8, #54, lsl #12 // =221184
-; CHECK-NEXT:    cmp x8, #1024
+; CHECK-NEXT:    ubfx x8, x0, #10, #8
+; CHECK-NEXT:    cmp x8, #217
 ; CHECK-NEXT:    cset w0, eq
 ; CHECK-NEXT:    ret
   %m = and i64 %x, 261120
@@ -50,9 +47,8 @@ define i1 @mask_i64(i64 %x) {
 define i1 @shifted_imm12(i64 %x) {
 ; CHECK-LABEL: shifted_imm12:
 ; CHECK:       // %bb.0:
-; CHECK-NEXT:    mov x8, #4398046511104 // =0x40000000000
-; CHECK-NEXT:    and x9, x0, #0xfffffffff000000
-; CHECK-NEXT:    cmp x9, x8
+; CHECK-NEXT:    ubfx x8, x0, #24, #36
+; CHECK-NEXT:    cmp x8, #64, lsl #12 // =262144
 ; CHECK-NEXT:    cset w0, eq
 ; CHECK-NEXT:    ret
   %m = and i64 %x, 1152921504590069760
@@ -63,9 +59,8 @@ define i1 @shifted_imm12(i64 %x) {
 define i32 @branch(i32 %c, i32 %a, i32 %b) {
 ; CHECK-LABEL: branch:
 ; CHECK:       // %bb.0: // %common.ret
-; CHECK-NEXT:    and w8, w0, #0xfc00
-; CHECK-NEXT:    mov w9, #55296 // =0xd800
-; CHECK-NEXT:    cmp w8, w9
+; CHECK-NEXT:    ubfx w8, w0, #10, #6
+; CHECK-NEXT:    cmp w8, #54
 ; CHECK-NEXT:    csel w0, w1, w2, eq
 ; CHECK-NEXT:    ret
   %m = and i32 %c, 64512



More information about the llvm-commits mailing list