[llvm] [InstCombine] Modify zero-indexed GEPs in place rather than cloning (PR #185053)

via llvm-commits llvm-commits at lists.llvm.org
Fri Mar 6 09:17:44 PST 2026


llvmbot wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-llvm-transforms

Author: Drew Kersnar (dakersnar)

<details>
<summary>Changes</summary>

When analyzing operands of loads/stores, if we can guarantee that a GEP is always zero-indexed, it is better to modify the GEP such that other users can take advantage of the simplification, rather than just cloning it for one specific load/store user.

Without this change, replaceGEPIdxWithZero clones the GEP for the triggering load/store, leaving the original variable-indexed GEP in place. Other users of that GEP (e.g., a constant-offset GEP feeding a second load) miss the simplification. Testcase demonstrates this: without the first load _modifying_ the gep, the _second_ load will still be dependent on both GEPs, and thus unnecessarily dependent on the %idx. This lack of simplification can cause issues with later passes such as LICM.

Alternative approaches could be to add a version of this transform into visitGEP, but there is precedent to doing so in visitLoad/visitStore, see simplifyNonNullOperand. And because the optimization is tied to the dereference that happens in the load/store, I think it reasonably fits here.

Alive2 proof: https://alive2.llvm.org/ce/z/-HZd9c

Alive2 counterexample showing why we cannot blindly modify the gep in place without some sort of condition: https://alive2.llvm.org/ce/z/dzKuc3


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


2 Files Affected:

- (modified) llvm/lib/Transforms/InstCombine/InstCombineLoadStoreAlloca.cpp (+11) 
- (added) llvm/test/Transforms/InstCombine/gep-replace-idx-zero-multi-use.ll (+52) 


``````````diff
diff --git a/llvm/lib/Transforms/InstCombine/InstCombineLoadStoreAlloca.cpp b/llvm/lib/Transforms/InstCombine/InstCombineLoadStoreAlloca.cpp
index 277f81245ade2..4ba6d37893d5c 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineLoadStoreAlloca.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineLoadStoreAlloca.cpp
@@ -991,6 +991,17 @@ static Instruction *replaceGEPIdxWithZero(InstCombinerImpl &IC, Value *Ptr,
   if (GetElementPtrInst *GEPI = dyn_cast<GetElementPtrInst>(Ptr)) {
     unsigned Idx;
     if (canReplaceGEPIdxWithZero(IC, GEPI, &MemI, Idx)) {
+      // If the memory instruction is guaranteed to execute whenever the GEP
+      // does, the dereference proves the index is unconditionally zero.
+      // Modify the GEP in place so all users benefit.
+      if (GEPI->getParent() == MemI.getParent() &&
+          isGuaranteedToTransferExecutionToSuccessor(GEPI->getIterator(),
+                                                     MemI.getIterator())) {
+        IC.replaceOperand(
+            *GEPI, Idx, ConstantInt::get(GEPI->getOperand(Idx)->getType(), 0));
+        IC.addToWorklist(GEPI);
+        return GEPI;
+      }
       Instruction *NewGEPI = GEPI->clone();
       NewGEPI->setOperand(Idx,
         ConstantInt::get(GEPI->getOperand(Idx)->getType(), 0));
diff --git a/llvm/test/Transforms/InstCombine/gep-replace-idx-zero-multi-use.ll b/llvm/test/Transforms/InstCombine/gep-replace-idx-zero-multi-use.ll
new file mode 100644
index 0000000000000..9f9d114e40e49
--- /dev/null
+++ b/llvm/test/Transforms/InstCombine/gep-replace-idx-zero-multi-use.ll
@@ -0,0 +1,52 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt -S -passes=instcombine < %s | FileCheck %s
+
+%struct.pair = type { i64, i64 }
+
+declare void @init(ptr)
+
+; When a variable-indexed inbounds GEP into a single-element allocation is
+; dereferenced, the index is provably zero. If the GEP and the dereference
+; are in the same basic block, replaceGEPIdxWithZero should modify the GEP
+; in place so that all users benefit -- not just the triggering load/store.
+;
+; Here %gep feeds both a direct load and a constant-offset GEP (%off) used
+; by a second load. Both should resolve to constant offsets from %base.
+define i64 @gep_idx_zero_multi_use(i64 %idx) {
+; CHECK-LABEL: define i64 @gep_idx_zero_multi_use(
+; CHECK-SAME: i64 [[IDX:%.*]]) {
+; CHECK-NEXT:    [[BASE:%.*]] = alloca [1 x [[STRUCT_PAIR:%.*]]], align 16
+; CHECK-NEXT:    call void @init(ptr nonnull [[BASE]])
+; CHECK-NEXT:    [[LOAD1:%.*]] = load i64, ptr [[BASE]], align 16
+; CHECK-NEXT:    [[OFF:%.*]] = getelementptr inbounds nuw i8, ptr [[BASE]], i64 8
+; CHECK-NEXT:    [[LOAD2:%.*]] = load i64, ptr [[OFF]], align 8
+; CHECK-NEXT:    [[SUM:%.*]] = add i64 [[LOAD1]], [[LOAD2]]
+; CHECK-NEXT:    ret i64 [[SUM]]
+;
+  %base = alloca [1 x %struct.pair], align 16
+  call void @init(ptr %base)
+  %gep = getelementptr inbounds [1 x %struct.pair], ptr %base, i64 %idx
+  %load1 = load i64, ptr %gep, align 16
+  %off = getelementptr inbounds i8, ptr %gep, i64 8
+  %load2 = load i64, ptr %off, align 8
+  %sum = add i64 %load1, %load2
+  ret i64 %sum
+}
+
+; Same pattern but with a store as the triggering memory instruction.
+define i64 @gep_idx_zero_multi_use_store(i64 %idx, i64 %val) {
+; CHECK-LABEL: define i64 @gep_idx_zero_multi_use_store(
+; CHECK-SAME: i64 [[IDX:%.*]], i64 [[VAL:%.*]]) {
+; CHECK-NEXT:    [[BASE:%.*]] = alloca [1 x [[STRUCT_PAIR:%.*]]], align 16
+; CHECK-NEXT:    store i64 [[VAL]], ptr [[BASE]], align 16
+; CHECK-NEXT:    [[OFF:%.*]] = getelementptr inbounds nuw i8, ptr [[BASE]], i64 8
+; CHECK-NEXT:    [[LOAD:%.*]] = load i64, ptr [[OFF]], align 8
+; CHECK-NEXT:    ret i64 [[LOAD]]
+;
+  %base = alloca [1 x %struct.pair], align 16
+  %gep = getelementptr inbounds [1 x %struct.pair], ptr %base, i64 %idx
+  store i64 %val, ptr %gep, align 16
+  %off = getelementptr inbounds i8, ptr %gep, i64 8
+  %load = load i64, ptr %off, align 8
+  ret i64 %load
+}

``````````

</details>


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


More information about the llvm-commits mailing list