[llvm] [InstCombine] Optimize GEP comparisons with constant offsets (PR #208547)

via llvm-commits llvm-commits at lists.llvm.org
Mon Jul 13 03:52:08 PDT 2026


=?utf-8?q?Ömer_Sinan_Ağacan?= <omeragacan at gmail.com>,
=?utf-8?q?Ömer_Sinan_Ağacan?= <omeragacan at gmail.com>,
=?utf-8?q?Ömer_Sinan_Ağacan?= <omeragacan at gmail.com>,
=?utf-8?q?Ömer_Sinan_Ağacan?= <omeragacan at gmail.com>,
=?utf-8?q?Ömer_Sinan_Ağacan?= <omeragacan at gmail.com>
Message-ID:
In-Reply-To: <llvm.org/llvm/llvm-project/pull/208547 at github.com>


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-llvm-transforms

Author: Ömer Sinan Ağacan (osa1)

<details>
<summary>Changes</summary>

In a GEP comparison with the same base like

    %1 = gep i8, @<!-- -->base, i64 a
    %2 = gep i8, @<!-- -->base, i64 b
    %cmp = icmp ... %1, %2

When we know that the offsets cross the base's alignment boundary the same
number of times, it means that either both of them will overflow, or none of
them will. In these cases we can turn the comparison into offset comparison:

    %cmp = icmp ... a, b

This optimization opportunity was revealed by this IR:

    target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"

    @<!-- -->a = global [4 x i32] zeroinitializer, align 16

    ; Function Attrs: cold noreturn nounwind memory(inaccessiblemem: write)
    declare void @<!-- -->llvm.ubsantrap(i8 immarg) #<!-- -->0

    define dso_local i32 @<!-- -->read_from_global_array_trap_last_iter() local_unnamed_addr {
    entry:
      br label %for.cond

    for.cond:                                         ; preds = %cont3, %entry
      %res.0 = phi i32 [ 0, %entry ], [ %add, %cont3 ]
      %i.0 = phi i8 [ 0, %entry ], [ %inc, %cont3 ]
      %cmp = icmp samesign ult i8 %i.0, 5
      br i1 %cmp, label %for.body, label %for.cond.cleanup

    for.cond.cleanup:                                 ; preds = %for.cond
      ret i32 %res.0

    for.body:                                         ; preds = %for.cond
      %idxprom = zext nneg i8 %i.0 to i64
      %arrayidx = getelementptr [4 x i8], ptr @<!-- -->a, i64 %idxprom
      %0 = getelementptr i8, ptr %arrayidx, i64 4
      %1 = icmp ule ptr %0, getelementptr inbounds nuw (i8, ptr @<!-- -->a, i64 16)
      %2 = icmp ule ptr %arrayidx, %0
      %or.cond = and i1 %1, %2
      %3 = icmp uge ptr %arrayidx, @<!-- -->a
      %or.cond1 = and i1 %or.cond, %3
      br i1 %or.cond1, label %cont3, label %trap

    trap:                                             ; preds = %for.body
      call void @<!-- -->llvm.ubsantrap(i8 25)
      unreachable

    cont3:                                            ; preds = %for.body
      %4 = load i32, ptr %arrayidx, align 4
      %add = add nsw i32 %res.0, %4
      %inc = add nuw nsw i8 %i.0, 1
      br label %for.cond
    }

With the main branch, `opt -O3` optimizes this into:

    @<!-- -->a = global [4 x i32] zeroinitializer, align 16

    ; Function Attrs: nounwind memory(read, argmem: none, inaccessiblemem: write, target_mem: none)
    define dso_local i32 @<!-- -->read_from_global_array_trap_last_iter() local_unnamed_addr #<!-- -->0 {
    cont3.3:
      %.not = icmp ugt ptr getelementptr (i8, ptr @<!-- -->a, i64 20), getelementptr inbounds nuw (i8, ptr @<!-- -->a, i64 16), !annotation !6
      br i1 %.not, label %trap, label %cont3.4, !prof !7, !annotation !6

    trap:                                             ; preds = %cont3.3
      tail call void @<!-- -->llvm.ubsantrap(i8 25) #<!-- -->2, !annotation !8
      unreachable, !annotation !8

    cont3.4:                                          ; preds = %cont3.3
      %0 = load i32, ptr getelementptr inbounds nuw (i8, ptr @<!-- -->a, i64 12), align 4, !tbaa !9
      %1 = load i32, ptr getelementptr inbounds nuw (i8, ptr @<!-- -->a, i64 8), align 8, !tbaa !9
      %2 = load i32, ptr getelementptr inbounds nuw (i8, ptr @<!-- -->a, i64 4), align 4, !tbaa !9
      %3 = load i32, ptr @<!-- -->a, align 16, !tbaa !9
      %add.1 = add nsw i32 %2, %3
      %add.2 = add nsw i32 %1, %add.1
      %add.3 = add nsw i32 %0, %add.2
      %4 = load i32, ptr getelementptr inbounds nuw (i8, ptr @<!-- -->a, i64 16), align 16, !tbaa !9
      %add.4 = add nsw i32 %4, %add.3
      ret i32 %add.4
    }

Note that this part

      %.not = icmp ugt ptr getelementptr (i8, ptr @<!-- -->a, i64 20), getelementptr inbounds nuw (i8, ptr @<!-- -->a, i64 16), !annotation !6

Is optimized with this PR. `opt -O3` generates this with this PR:

    @<!-- -->a = local_unnamed_addr global [4 x i32] zeroinitializer, align 16

    ; Function Attrs: cold noreturn nounwind memory(inaccessiblemem: write)
    declare void @<!-- -->llvm.ubsantrap(i8 immarg) #<!-- -->0

    ; Function Attrs: cold noreturn nounwind memory(inaccessiblemem: write)
    define dso_local noundef i32 @<!-- -->read_from_global_array_trap_last_iter() local_unnamed_addr #<!-- -->0 {
    trap:
      tail call void @<!-- -->llvm.ubsantrap(i8 25)
      unreachable
    }

    attributes #<!-- -->0 = { cold noreturn nounwind memory(inaccessiblemem: write) }

I also tried this change on two large benchmark suites (14,740 C/C++
files in total). This made a tiny difference in one file only in the
`opt` output, but even that change did not survive the rest of the
compilation pipeline. However the original repro from above is actually
compiled differently with `llc -O3`. With the main branch:

    _read_from_global_array_trap_last_iter: ; @<!-- -->read_from_global_array_trap_last_iter
    Lloh0:
            adrp        x8, _a@<!-- -->PAGE+16
    Lloh1:
            add         x8, x8, _a@<!-- -->PAGEOFF+16
            add         x9, x8, #<!-- -->4
            cmp         x9, x8
            b.hi        LBB0_2
    Lloh2:
            adrp        x8, _a@<!-- -->PAGE
    Lloh3:
            add         x8, x8, _a@<!-- -->PAGEOFF
            ldr         q0, [x8]
            ldr         w8, [x8, #<!-- -->16]
            addv.4s     s0, v0
            fmov        w9, s0
            add         w0, w9, w8
            ret
    LBB0_2:                                 ; %trap
            brk        #<!-- -->0x5519
                                            ; -- End function
            .globl        _a                              ; @<!-- -->a


With this PR:

    _read_from_global_array_trap_last_iter: ; @<!-- -->read_from_global_array_trap_last_iter
            brk         #<!-- -->0x5519
                                            ; -- End function
            .globl        _a                              ; @<!-- -->a

Note that this part in the original assembly

    add        x9, x8, #<!-- -->4
    cmp        x9, x8
    b.hi       ...

This always branches as we know `x9` is aligned on 16-byte boundary and
so `x9 + 4` cannot overflow.

---
Full diff: https://github.com/llvm/llvm-project/pull/208547.diff


2 Files Affected:

- (modified) llvm/lib/Transforms/InstCombine/InstCombineCompares.cpp (+28-7) 
- (modified) llvm/test/Transforms/InstCombine/icmp-gep.ll (+140) 


``````````diff
diff --git a/llvm/lib/Transforms/InstCombine/InstCombineCompares.cpp b/llvm/lib/Transforms/InstCombine/InstCombineCompares.cpp
index e6deb548819e8..b51ed5cce26f7 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineCompares.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineCompares.cpp
@@ -815,14 +815,35 @@ Instruction *InstCombinerImpl::foldGEPICmp(GEPOperator *GEPLHS, Value *RHS,
       }
     }
 
-    if (Base.Ptr && CanFold(Base.LHSNW & Base.RHSNW) && !Base.isExpensive()) {
+    if (Base.Ptr && !Base.isExpensive()) {
       // ((gep Ptr, OFFSET1) cmp (gep Ptr, OFFSET2)  --->  (OFFSET1 cmp OFFSET2)
-      Type *IdxTy = DL.getIndexType(GEPLHS->getType());
-      Value *L =
-          EmitGEPOffsets(Base.LHSGEPs, Base.LHSNW, IdxTy, /*RewriteGEP=*/true);
-      Value *R =
-          EmitGEPOffsets(Base.RHSGEPs, Base.RHSNW, IdxTy, /*RewriteGEP=*/true);
-      return NewICmp(Base.LHSNW & Base.RHSNW, L, R);
+      bool DoFold = CanFold(Base.LHSNW & Base.RHSNW);
+
+      if (!DoFold && Base.Ptr->getType()->isPointerTy()) {
+        // Without the flags, we can still fold if the offsets are constant and
+        // they cross the base's alignment boundary the same number of times, so
+        // either both arguments will wrap, or none of them will.
+        unsigned BW = DL.getIndexTypeSizeInBits(GEPLHS->getType());
+        APInt Alignment = APInt(BW, Base.Ptr->getPointerAlignment(DL).value());
+        APInt LOff(BW, 0);
+        APInt ROff(BW, 0);
+        if (GEPLHS->stripAndAccumulateConstantOffsets(
+                DL, LOff, /*AllowNonInbounds=*/true) == Base.Ptr &&
+            RHS->stripAndAccumulateConstantOffsets(
+                DL, ROff, /*AllowNonInbounds=*/true) == Base.Ptr)
+          DoFold =
+              APIntOps::RoundingSDiv(LOff, Alignment, APInt::Rounding::DOWN) ==
+              APIntOps::RoundingSDiv(ROff, Alignment, APInt::Rounding::DOWN);
+      }
+
+      if (DoFold) {
+        Type *IdxTy = DL.getIndexType(GEPLHS->getType());
+        Value *L = EmitGEPOffsets(Base.LHSGEPs, Base.LHSNW, IdxTy,
+                                  /*RewriteGEP=*/true);
+        Value *R = EmitGEPOffsets(Base.RHSGEPs, Base.RHSNW, IdxTy,
+                                  /*RewriteGEP=*/true);
+        return NewICmp(Base.LHSNW & Base.RHSNW, L, R);
+      }
     }
   }
 
diff --git a/llvm/test/Transforms/InstCombine/icmp-gep.ll b/llvm/test/Transforms/InstCombine/icmp-gep.ll
index 048a4c4a7e5fe..abdd354a60b10 100644
--- a/llvm/test/Transforms/InstCombine/icmp-gep.ll
+++ b/llvm/test/Transforms/InstCombine/icmp-gep.ll
@@ -1124,3 +1124,143 @@ define i1 @gep_gep_multiple_ult_nuw_multi_use(ptr %base, i64 %idx1, i64 %idx2, i
   %cmp = icmp ult ptr %gep2, %gep4
   ret i1 %cmp
 }
+
+define i1 @gep_const_same_block(ptr align 16 %foo) {
+; CHECK-LABEL: @gep_const_same_block(
+; CHECK-NEXT:    ret i1 true
+;
+  %gep1 = getelementptr i8, ptr %foo, i64 4
+  %gep2 = getelementptr i8, ptr %foo, i64 8
+  %cmp = icmp ult ptr %gep1, %gep2
+  ret i1 %cmp
+}
+
+define i1 @gep_const_same_negative_block(ptr align 16 %foo) {
+; CHECK-LABEL: @gep_const_same_negative_block(
+; CHECK-NEXT:    ret i1 true
+;
+  %gep1 = getelementptr i8, ptr %foo, i64 -12
+  %gep2 = getelementptr i8, ptr %foo, i64 -8
+  %cmp = icmp ult ptr %gep1, %gep2
+  ret i1 %cmp
+}
+
+define i1 @gep_const_different_block_1(ptr align 4 %foo) {
+; CHECK-LABEL: @gep_const_different_block_1(
+; CHECK-NEXT:    [[GEP1:%.*]] = getelementptr i8, ptr [[FOO:%.*]], i64 4
+; CHECK-NEXT:    [[GEP2:%.*]] = getelementptr i8, ptr [[FOO]], i64 8
+; CHECK-NEXT:    [[CMP:%.*]] = icmp ult ptr [[GEP1]], [[GEP2]]
+; CHECK-NEXT:    ret i1 [[CMP]]
+;
+  %gep1 = getelementptr i8, ptr %foo, i64 4
+  %gep2 = getelementptr i8, ptr %foo, i64 8
+  %cmp = icmp ult ptr %gep1, %gep2
+  ret i1 %cmp
+}
+
+define i1 @gep_const_different_block_2(ptr align 16 %foo) {
+; CHECK-LABEL: @gep_const_different_block_2(
+; CHECK-NEXT:    [[GEP1:%.*]] = getelementptr i8, ptr [[FOO:%.*]], i64 -4
+; CHECK-NEXT:    [[GEP2:%.*]] = getelementptr i8, ptr [[FOO]], i64 4
+; CHECK-NEXT:    [[CMP:%.*]] = icmp ult ptr [[GEP1]], [[GEP2]]
+; CHECK-NEXT:    ret i1 [[CMP]]
+;
+  %gep1 = getelementptr i8, ptr %foo, i64 -4
+  %gep2 = getelementptr i8, ptr %foo, i64 4
+  %cmp = icmp ult ptr %gep1, %gep2
+  ret i1 %cmp
+}
+
+define i1 @gep_const_edge_case_1(ptr align 16 %foo) {
+; CHECK-LABEL: @gep_const_edge_case_1(
+; CHECK-NEXT:    ret i1 true
+;
+  %gep1 = getelementptr i8, ptr %foo, i64 14
+  %gep2 = getelementptr i8, ptr %foo, i64 15
+  %cmp = icmp ult ptr %gep1, %gep2
+  ret i1 %cmp
+}
+
+define i1 @gep_const_edge_case_2(ptr align 16 %foo) {
+; CHECK-LABEL: @gep_const_edge_case_2(
+; CHECK-NEXT:    [[GEP1:%.*]] = getelementptr i8, ptr [[FOO:%.*]], i64 15
+; CHECK-NEXT:    [[GEP2:%.*]] = getelementptr i8, ptr [[FOO]], i64 16
+; CHECK-NEXT:    [[CMP:%.*]] = icmp ult ptr [[GEP1]], [[GEP2]]
+; CHECK-NEXT:    ret i1 [[CMP]]
+;
+  %gep1 = getelementptr i8, ptr %foo, i64 15
+  %gep2 = getelementptr i8, ptr %foo, i64 16
+  %cmp = icmp ult ptr %gep1, %gep2
+  ret i1 %cmp
+}
+
+define i1 @gep_const_edge_case_3(ptr align 16 %foo) {
+; CHECK-LABEL: @gep_const_edge_case_3(
+; CHECK-NEXT:    ret i1 true
+;
+  %gep1 = getelementptr i8, ptr %foo, i64 16
+  %gep2 = getelementptr i8, ptr %foo, i64 17
+  %cmp = icmp ult ptr %gep1, %gep2
+  ret i1 %cmp
+}
+
+define i1 @gep_const_edge_case_4(ptr align 16 %foo) {
+; CHECK-LABEL: @gep_const_edge_case_4(
+; CHECK-NEXT:    ret i1 false
+;
+  %gep1 = getelementptr i8, ptr %foo, i64 -15
+  %gep2 = getelementptr i8, ptr %foo, i64 -16
+  %cmp = icmp ult ptr %gep1, %gep2
+  ret i1 %cmp
+}
+
+define i1 @gep_const_edge_case_5(ptr align 16 %foo) {
+; CHECK-LABEL: @gep_const_edge_case_5(
+; CHECK-NEXT:    [[GEP1:%.*]] = getelementptr i8, ptr [[FOO:%.*]], i64 -16
+; CHECK-NEXT:    [[GEP2:%.*]] = getelementptr i8, ptr [[FOO]], i64 -17
+; CHECK-NEXT:    [[CMP:%.*]] = icmp ult ptr [[GEP1]], [[GEP2]]
+; CHECK-NEXT:    ret i1 [[CMP]]
+;
+  %gep1 = getelementptr i8, ptr %foo, i64 -16
+  %gep2 = getelementptr i8, ptr %foo, i64 -17
+  %cmp = icmp ult ptr %gep1, %gep2
+  ret i1 %cmp
+}
+
+define i1 @gep_variable_offsets(ptr align 16 %foo, i64 %i, i64 %j) {
+; CHECK-LABEL: @gep_variable_offsets(
+; CHECK-NEXT:    [[GEP1:%.*]] = getelementptr i8, ptr [[FOO:%.*]], i64 [[I:%.*]]
+; CHECK-NEXT:    [[GEP2:%.*]] = getelementptr i8, ptr [[FOO]], i64 [[J:%.*]]
+; CHECK-NEXT:    [[CMP:%.*]] = icmp ult ptr [[GEP1]], [[GEP2]]
+; CHECK-NEXT:    ret i1 [[CMP]]
+;
+  %gep1 = getelementptr i8, ptr %foo, i64 %i
+  %gep2 = getelementptr i8, ptr %foo, i64 %j
+  %cmp = icmp ult ptr %gep1, %gep2
+  ret i1 %cmp
+}
+
+; Similar to tests above, but extracted from an actual program.
+ at g16 = global [4 x i32] zeroinitializer, align 16
+define i1 @gep_global_offsets() {
+; CHECK-LABEL: @gep_global_offsets(
+; CHECK-NEXT:    ret i1 false
+;
+  %cmp = icmp ule ptr getelementptr (i8, ptr @g16, i64 20), getelementptr inbounds nuw (i8, ptr @g16, i64 16)
+  ret i1 %cmp
+}
+
+; Regression test: when checking for folding opportunities check for pointer
+; base before using trying to get the pointer alignment.
+define <2 x i1> @vec_gep_cmp(<2 x ptr> %base, <2 x i64> %i, <2 x i64> %j) {
+; CHECK-LABEL: @vec_gep_cmp(
+; CHECK-NEXT:    [[GEP1:%.*]] = getelementptr i8, <2 x ptr> [[BASE:%.*]], <2 x i64> [[I:%.*]]
+; CHECK-NEXT:    [[GEP2:%.*]] = getelementptr i8, <2 x ptr> [[BASE]], <2 x i64> [[J:%.*]]
+; CHECK-NEXT:    [[CMP:%.*]] = icmp ult <2 x ptr> [[GEP1]], [[GEP2]]
+; CHECK-NEXT:    ret <2 x i1> [[CMP]]
+;
+  %gep1 = getelementptr i8, <2 x ptr> %base, <2 x i64> %i
+  %gep2 = getelementptr i8, <2 x ptr> %base, <2 x i64> %j
+  %cmp = icmp ult <2 x ptr> %gep1, %gep2
+  ret <2 x i1> %cmp
+}

``````````

</details>


https://github.com/llvm/llvm-project/pull/208547


More information about the llvm-commits mailing list