[llvm] [GVN] Preserve scoped-alias metadata on coerced loads (PR #206417)
Madhur Amilkanthwar via llvm-commits
llvm-commits at lists.llvm.org
Mon Jun 29 02:18:29 PDT 2026
https://github.com/madhur13490 updated https://github.com/llvm/llvm-project/pull/206417
>From 149d4b4a3665d42b491a17b82cd3a62bf3e55ff9 Mon Sep 17 00:00:00 2001
From: Madhur Amilkanthwar <madhura at nvidia.com>
Date: Sat, 27 Jun 2026 00:03:34 -0700
Subject: [PATCH 1/2] [GVN] Add test for scoped-alias metadata dropped on
coerced loads
Precommit test capturing current behavior: when GVN forwards a wider
load to a narrower load at an offset, dropUnknownNonDebugMetadata
strips !noalias and !alias.scope from the surviving wider load even
though that load is unchanged and the metadata stays valid.
---
.../Transforms/GVN/rle-coerced-noalias.ll | 94 +++++++++++++++++++
1 file changed, 94 insertions(+)
create mode 100644 llvm/test/Transforms/GVN/rle-coerced-noalias.ll
diff --git a/llvm/test/Transforms/GVN/rle-coerced-noalias.ll b/llvm/test/Transforms/GVN/rle-coerced-noalias.ll
new file mode 100644
index 0000000000000..277e054fdccd5
--- /dev/null
+++ b/llvm/test/Transforms/GVN/rle-coerced-noalias.ll
@@ -0,0 +1,94 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt -S -passes=gvn < %s | FileCheck %s
+
+; When GVN coerces a wider load into a narrower load at an offset, it strips
+; !noalias and !alias.scope from the surviving wider load. That load is
+; unchanged and still accesses the same memory, so the metadata stays valid
+; and should be kept. It is currently dropped.
+
+target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
+
+; The i8 load at P+1 partially aliases the i32 load at P and is coerced from
+; it. The surviving i32 load should keep its !noalias.
+define i32 @noalias_dropped_local(ptr %p) {
+; CHECK-LABEL: define i32 @noalias_dropped_local(
+; CHECK-SAME: ptr [[P:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[WIDE:%.*]] = load i32, ptr [[P]], align 4
+; CHECK-NEXT: [[P1:%.*]] = getelementptr inbounds i8, ptr [[P]], i64 1
+; CHECK-NEXT: [[TMP0:%.*]] = lshr i32 [[WIDE]], 8
+; CHECK-NEXT: [[TMP1:%.*]] = trunc i32 [[TMP0]] to i8
+; CHECK-NEXT: [[CONV:%.*]] = zext i8 [[TMP1]] to i32
+; CHECK-NEXT: [[ADD:%.*]] = add nsw i32 [[WIDE]], [[CONV]]
+; CHECK-NEXT: ret i32 [[ADD]]
+;
+entry:
+ %wide = load i32, ptr %p, align 4, !noalias !0
+ %p1 = getelementptr inbounds i8, ptr %p, i64 1
+ %narrow = load i8, ptr %p1, align 1
+ %conv = zext i8 %narrow to i32
+ %add = add nsw i32 %wide, %conv
+ ret i32 %add
+}
+
+; Same as above for !alias.scope on the surviving wider load.
+define i32 @alias_scope_dropped_local(ptr %p) {
+; CHECK-LABEL: define i32 @alias_scope_dropped_local(
+; CHECK-SAME: ptr [[P:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[WIDE:%.*]] = load i32, ptr [[P]], align 4
+; CHECK-NEXT: [[P1:%.*]] = getelementptr inbounds i8, ptr [[P]], i64 1
+; CHECK-NEXT: [[TMP0:%.*]] = lshr i32 [[WIDE]], 8
+; CHECK-NEXT: [[TMP1:%.*]] = trunc i32 [[TMP0]] to i8
+; CHECK-NEXT: [[CONV:%.*]] = zext i8 [[TMP1]] to i32
+; CHECK-NEXT: [[ADD:%.*]] = add nsw i32 [[WIDE]], [[CONV]]
+; CHECK-NEXT: ret i32 [[ADD]]
+;
+entry:
+ %wide = load i32, ptr %p, align 4, !alias.scope !0
+ %p1 = getelementptr inbounds i8, ptr %p, i64 1
+ %narrow = load i8, ptr %p1, align 1
+ %conv = zext i8 %narrow to i32
+ %add = add nsw i32 %wide, %conv
+ ret i32 %add
+}
+
+; Cross-block (non-local) coerced load. The i32 load in entry is forwarded to
+; the i8 load at P+1 in the taken block. The surviving i32 load should keep
+; its !noalias.
+define i32 @noalias_dropped_nonlocal(ptr %p) {
+; CHECK-LABEL: define i32 @noalias_dropped_nonlocal(
+; CHECK-SAME: ptr [[P:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[WIDE:%.*]] = load i32, ptr [[P]], align 4
+; CHECK-NEXT: [[CMP:%.*]] = icmp eq i32 [[WIDE]], 127
+; CHECK-NEXT: [[TMP0:%.*]] = lshr i32 [[WIDE]], 8
+; CHECK-NEXT: [[TMP1:%.*]] = trunc i32 [[TMP0]] to i8
+; CHECK-NEXT: br i1 [[CMP]], label %[[T:.*]], label %[[F:.*]]
+; CHECK: [[T]]:
+; CHECK-NEXT: [[P1:%.*]] = getelementptr inbounds i8, ptr [[P]], i64 1
+; CHECK-NEXT: [[CONV:%.*]] = zext i8 [[TMP1]] to i32
+; CHECK-NEXT: ret i32 [[CONV]]
+; CHECK: [[F]]:
+; CHECK-NEXT: ret i32 52
+;
+entry:
+ %wide = load i32, ptr %p, align 4, !noalias !0
+ %cmp = icmp eq i32 %wide, 127
+ br i1 %cmp, label %t, label %f
+
+t:
+ %p1 = getelementptr inbounds i8, ptr %p, i64 1
+ %narrow = load i8, ptr %p1, align 1
+ %conv = zext i8 %narrow to i32
+ ret i32 %conv
+
+f:
+ ret i32 52
+}
+
+; Alias-scope metadata domain.
+!1 = distinct !{!1, !2, !"scope A"}
+!2 = distinct !{!2, !"test domain"}
+
+!0 = !{!1}
>From 126583db52ddd789c36054c38fb10346b00fd277 Mon Sep 17 00:00:00 2001
From: Madhur Amilkanthwar <madhura at nvidia.com>
Date: Sat, 27 Jun 2026 00:08:11 -0700
Subject: [PATCH 2/2] [GVN] Preserve scoped-alias metadata on coerced loads
When forwarding a wider load to a narrower load at an offset, GVN drops
the wider load's metadata. !noalias and !alias.scope are independent of
the load type and offset, and the wider load is not moved and still
accesses the same memory, so they remain valid and are now kept instead
of dropped.
---
llvm/lib/Transforms/Scalar/GVN.cpp | 8 +++++---
.../Transforms/GVN/rle-coerced-noalias.ll | 19 ++++++++++++-------
2 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/llvm/lib/Transforms/Scalar/GVN.cpp b/llvm/lib/Transforms/Scalar/GVN.cpp
index a619dcc588350..517d33bfba103 100644
--- a/llvm/lib/Transforms/Scalar/GVN.cpp
+++ b/llvm/lib/Transforms/Scalar/GVN.cpp
@@ -1163,13 +1163,15 @@ Value *AvailableValue::MaterializeAdjustedValue(LoadInst *Load,
// Drop all metadata that is not known to cause immediate UB on violation,
// unless the load has !noundef, in which case all metadata violations
// will be promoted to UB.
- // TODO: We can combine noalias/alias.scope metadata here, because it is
- // independent of the load type.
+ // !noalias and !alias.scope are kept: the load is not moved and still
+ // accesses the same memory, and these are independent of the load type
+ // and offset, so they remain valid for the coerced result.
if (!CoercedLoad->hasMetadata(LLVMContext::MD_noundef))
CoercedLoad->dropUnknownNonDebugMetadata(
{LLVMContext::MD_dereferenceable,
LLVMContext::MD_dereferenceable_or_null,
- LLVMContext::MD_invariant_load, LLVMContext::MD_invariant_group});
+ LLVMContext::MD_invariant_load, LLVMContext::MD_invariant_group,
+ LLVMContext::MD_alias_scope, LLVMContext::MD_noalias});
LLVM_DEBUG(dbgs() << "GVN COERCED NONLOCAL LOAD:\nOffset: " << Offset
<< " " << *getCoercedLoadValue() << '\n'
<< *Res << '\n'
diff --git a/llvm/test/Transforms/GVN/rle-coerced-noalias.ll b/llvm/test/Transforms/GVN/rle-coerced-noalias.ll
index 277e054fdccd5..67f7f58543aa8 100644
--- a/llvm/test/Transforms/GVN/rle-coerced-noalias.ll
+++ b/llvm/test/Transforms/GVN/rle-coerced-noalias.ll
@@ -1,10 +1,10 @@
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
; RUN: opt -S -passes=gvn < %s | FileCheck %s
-; When GVN coerces a wider load into a narrower load at an offset, it strips
-; !noalias and !alias.scope from the surviving wider load. That load is
-; unchanged and still accesses the same memory, so the metadata stays valid
-; and should be kept. It is currently dropped.
+; When GVN coerces a wider load into a narrower load at an offset, the wider
+; load is not moved and still accesses the same memory. Its !noalias and
+; !alias.scope are independent of the load type and offset, so they are
+; preserved on the surviving wider load rather than dropped.
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
@@ -14,7 +14,7 @@ define i32 @noalias_dropped_local(ptr %p) {
; CHECK-LABEL: define i32 @noalias_dropped_local(
; CHECK-SAME: ptr [[P:%.*]]) {
; CHECK-NEXT: [[ENTRY:.*:]]
-; CHECK-NEXT: [[WIDE:%.*]] = load i32, ptr [[P]], align 4
+; CHECK-NEXT: [[WIDE:%.*]] = load i32, ptr [[P]], align 4, !noalias [[META0:![0-9]+]]
; CHECK-NEXT: [[P1:%.*]] = getelementptr inbounds i8, ptr [[P]], i64 1
; CHECK-NEXT: [[TMP0:%.*]] = lshr i32 [[WIDE]], 8
; CHECK-NEXT: [[TMP1:%.*]] = trunc i32 [[TMP0]] to i8
@@ -36,7 +36,7 @@ define i32 @alias_scope_dropped_local(ptr %p) {
; CHECK-LABEL: define i32 @alias_scope_dropped_local(
; CHECK-SAME: ptr [[P:%.*]]) {
; CHECK-NEXT: [[ENTRY:.*:]]
-; CHECK-NEXT: [[WIDE:%.*]] = load i32, ptr [[P]], align 4
+; CHECK-NEXT: [[WIDE:%.*]] = load i32, ptr [[P]], align 4, !alias.scope [[META0]]
; CHECK-NEXT: [[P1:%.*]] = getelementptr inbounds i8, ptr [[P]], i64 1
; CHECK-NEXT: [[TMP0:%.*]] = lshr i32 [[WIDE]], 8
; CHECK-NEXT: [[TMP1:%.*]] = trunc i32 [[TMP0]] to i8
@@ -60,7 +60,7 @@ define i32 @noalias_dropped_nonlocal(ptr %p) {
; CHECK-LABEL: define i32 @noalias_dropped_nonlocal(
; CHECK-SAME: ptr [[P:%.*]]) {
; CHECK-NEXT: [[ENTRY:.*:]]
-; CHECK-NEXT: [[WIDE:%.*]] = load i32, ptr [[P]], align 4
+; CHECK-NEXT: [[WIDE:%.*]] = load i32, ptr [[P]], align 4, !noalias [[META0]]
; CHECK-NEXT: [[CMP:%.*]] = icmp eq i32 [[WIDE]], 127
; CHECK-NEXT: [[TMP0:%.*]] = lshr i32 [[WIDE]], 8
; CHECK-NEXT: [[TMP1:%.*]] = trunc i32 [[TMP0]] to i8
@@ -92,3 +92,8 @@ f:
!2 = distinct !{!2, !"test domain"}
!0 = !{!1}
+;.
+; CHECK: [[META0]] = !{[[META1:![0-9]+]]}
+; CHECK: [[META1]] = distinct !{[[META1]], [[META2:![0-9]+]], !"scope A"}
+; CHECK: [[META2]] = distinct !{[[META2]], !"test domain"}
+;.
More information about the llvm-commits
mailing list