[llvm] [IR] Preserve TBAA metadata when merging equivalent memory accesses (PR #208499)
via llvm-commits
llvm-commits at lists.llvm.org
Thu Jul 9 09:33:36 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-llvm-transforms
Author: 222rohan
<details>
<summary>Changes</summary>
### Description
When CSE/GVN merge two equivalent memory accesses (e.g. two loads of the same
address) and only one carries `!tbaa`, whether the survivor keeps the tag
depends on source order: `combineMetadata` only looks at the survivor's
metadata, so if the survivor is untyped the removed access's tag is dropped.
This handles TBAA after the metadata loop (like `prof`/`mmra`/`memprof`/
`callsite`): keep the existing merge when the survivor has a tag, and copy the
removed access's tag when it is the only one, so the result is order-independent.
### Example
```llvm
; opt -passes=early-cse -S
define i32 @<!-- -->untyped_first(ptr %p) {
%b = load i32, ptr %p, align 4 ; untyped, becomes survivor
%a = load i32, ptr %p, align 4, !tbaa !0 ; typed, removed
%s = add i32 %a, %b
ret i32 %s
}
```
Before: survivor loses `!tbaa`. After: survivor keeps `!tbaa !0` (same as the
typed-first ordering).
### Microbenchmark
The dropped tag prevents TBAA from proving no-alias, which blocks LICM. In this
kernel a loop-invariant division chain is hoisted only when the tag survives
(`clang -O1`):
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
__attribute__((always_inline))
static inline float div_chain(float x, float d) {
x = x / d;
x = x / d;
x = x / d;
x = x / d;
return x;
}
// Typed *p read first -> survives CSE -> load keeps !tbaa "int".
__attribute__((noinline))
float order_tbaa_first(float *arr, int *p, int n) {
for (int i = 0; i < n; i++) {
int a = *p; // typed load: load i32, !tbaa "int"
int b; memcpy(&b, p, sizeof(int)); // memcpy load: CSE'd away (second)
float d = (float)(a + b);
float r = div_chain(1.0e30f, d); // function of *p only
arr[i] = r;
}
return arr[n - 1];
}
// memcpy read first -> survives CSE -> load has no !tbaa.
__attribute__((noinline))
float order_tbaa_second(float *arr, int *p, int n) {
for (int i = 0; i < n; i++) {
int b; memcpy(&b, p, sizeof(int)); // memcpy load: load i32 (no tbaa, first)
int a = *p; // typed load: CSE'd away (second)
float d = (float)(a + b);
float r = div_chain(1.0e30f, d);
arr[i] = r;
}
return arr[n - 1];
}
static double now_sec(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
}
int main(int argc, char **argv) {
int n = argc > 1 ? atoi(argv[1]) : 200 * 1000 * 1000;
int reps = argc > 2 ? atoi(argv[2]) : 7;
float *arr = malloc(sizeof(float) * (size_t)n);
if (!arr) { perror("malloc"); return 1; }
int val = 3;
double bt = 1e18, bm = 1e18;
float r1 = 0, r2 = 0;
for (int r = 0; r < reps; r++) {
double t0 = now_sec();
r1 = order_tbaa_first(arr, &val, n);
double dt = now_sec() - t0;
if (dt < bt) bt = dt;
}
for (int r = 0; r < reps; r++) {
double t0 = now_sec();
r2 = order_tbaa_second(arr, &val, n);
double dt = now_sec() - t0;
if (dt < bm) bm = dt;
}
printf("n = %d, reps = %d \n", n, reps);
printf("order_tbaa_first (tbaa kept -> chain hoisted by LICM) result=%.3e best=%.4f s (%.2f ns/iter)\n",
r1, bt, bt * 1e9 / n);
printf("order_tbaa_second (tbaa dropped -> chain per iteration) result=%.3e best=%.4f s (%.2f ns/iter)\n",
r2, bm, bm * 1e9 / n);
printf("speedup (second / first) = %.2fx\n", bm / bt);
free(arr);
return 0;
}
```
(Generated with the help of Cursor)
| | survivor tag | div chain | time |
|---|---|---|---|
| tag kept (fixed) | `!tbaa` | hoisted out of loop | 0.22 ns/iter |
| tag dropped (before) | none | recomputed every iteration | 2.11 ns/iter |
**9.6x** on this kernel (0.22 vs 2.11 ns/iter, `n=2e8`).
Setup: AMD Ryzen 9 7950X, 32GB RAM, Ubuntu 22.04.5 (kernel 6.8.0), clang built
from this branch, `clang -O1` (no `-ffast-math`). The gap is specific to `-O1`.
---
Full diff: https://github.com/llvm/llvm-project/pull/208499.diff
2 Files Affected:
- (modified) llvm/lib/Transforms/Utils/Local.cpp (+20-7)
- (added) llvm/test/Transforms/EarlyCSE/tbaa-merge-order.ll (+39)
``````````diff
diff --git a/llvm/lib/Transforms/Utils/Local.cpp b/llvm/lib/Transforms/Utils/Local.cpp
index b17740c0bc192..b067dbfb5ac22 100644
--- a/llvm/lib/Transforms/Utils/Local.cpp
+++ b/llvm/lib/Transforms/Utils/Local.cpp
@@ -2964,10 +2964,6 @@ static void combineMetadata(Instruction *K, const Instruction *J,
if (!AAOnly)
K->mergeDIAssignID(J);
break;
- case LLVMContext::MD_tbaa:
- if (DoesKMove)
- K->setMetadata(Kind, MDNode::getMostGenericTBAA(JMD, KMD));
- break;
case LLVMContext::MD_alias_scope:
if (DoesKMove)
K->setMetadata(Kind, MDNode::getMostGenericAliasScope(JMD, KMD));
@@ -3007,9 +3003,10 @@ static void combineMetadata(Instruction *K, const Instruction *J,
if (!AAOnly && (DoesKMove || !K->hasMetadata(LLVMContext::MD_noundef)))
K->setMetadata(Kind, JMD);
break;
- // Keep empty cases for prof, mmra, memprof, and callsite to prevent them
- // from being removed as unknown metadata. The actual merging is handled
- // separately below.
+ // Keep empty cases for tbaa, prof, mmra, memprof, and callsite to prevent
+ // them from being removed as unknown metadata. The actual merging is
+ // handled separately below.
+ case LLVMContext::MD_tbaa:
case LLVMContext::MD_prof:
case LLVMContext::MD_mmra:
case LLVMContext::MD_memprof:
@@ -3113,6 +3110,22 @@ static void combineMetadata(Instruction *K, const Instruction *J,
K->setMetadata(LLVMContext::MD_prof,
MDNode::getMergedProfMetadata(KProf, JProf, K, J));
}
+
+ // Merge TBAA metadata.
+ // Handle separately to support cases where only one instruction has the
+ // metadata.
+ MDNode *JTBAA = J->getMetadata(LLVMContext::MD_tbaa);
+ MDNode *KTBAA = K->getMetadata(LLVMContext::MD_tbaa);
+ if (KTBAA) {
+ if (DoesKMove)
+ K->setMetadata(LLVMContext::MD_tbaa,
+ MDNode::getMostGenericTBAA(JTBAA, KTBAA));
+ } else if (!AAOnly && JTBAA &&
+ isa<LoadInst, StoreInst, CallInst, VAArgInst, AtomicRMWInst,
+ AtomicCmpXchgInst>(K)) {
+ // Only J has a tag: copy it so the result is independent of CSE order.
+ K->setMetadata(LLVMContext::MD_tbaa, JTBAA);
+ }
}
void llvm::combineMetadataForCSE(Instruction *K, const Instruction *J,
diff --git a/llvm/test/Transforms/EarlyCSE/tbaa-merge-order.ll b/llvm/test/Transforms/EarlyCSE/tbaa-merge-order.ll
new file mode 100644
index 0000000000000..a3f742a490eec
--- /dev/null
+++ b/llvm/test/Transforms/EarlyCSE/tbaa-merge-order.ll
@@ -0,0 +1,39 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 5
+; RUN: opt -passes=early-cse -S < %s | FileCheck %s
+
+; Two loads of the same address are CSE'd into one. Only one of them carries
+; !tbaa. The surviving load's metadata must not depend on which load was
+; written first in the source (i.e. which becomes the CSE survivor).
+
+; typed (tbaa) load first, untyped load second.
+define i32 @typed_first(ptr %p) {
+; CHECK-LABEL: define i32 @typed_first(
+; CHECK-SAME: ptr [[P:%.*]]) {
+; CHECK-NEXT: [[A:%.*]] = load i32, ptr [[P]], align 4, !tbaa [[INT_TBAA0:![0-9]+]]
+; CHECK-NEXT: [[S:%.*]] = add i32 [[A]], [[A]]
+; CHECK-NEXT: ret i32 [[S]]
+;
+ %a = load i32, ptr %p, align 4, !tbaa !0
+ %b = load i32, ptr %p, align 4
+ %s = add i32 %a, %b
+ ret i32 %s
+}
+
+; untyped load first, typed (tbaa) load second. Result must match typed_first.
+define i32 @untyped_first(ptr %p) {
+; CHECK-LABEL: define i32 @untyped_first(
+; CHECK-SAME: ptr [[P:%.*]]) {
+; CHECK-NEXT: [[B:%.*]] = load i32, ptr [[P]], align 4, !tbaa [[INT_TBAA0]]
+; CHECK-NEXT: [[S:%.*]] = add i32 [[B]], [[B]]
+; CHECK-NEXT: ret i32 [[S]]
+;
+ %b = load i32, ptr %p, align 4
+ %a = load i32, ptr %p, align 4, !tbaa !0
+ %s = add i32 %a, %b
+ ret i32 %s
+}
+
+!0 = !{!1, !1, i64 0}
+!1 = !{!"int", !2, i64 0}
+!2 = !{!"omnipotent char", !3, i64 0}
+!3 = !{!"Simple C/C++ TBAA"}
``````````
</details>
https://github.com/llvm/llvm-project/pull/208499
More information about the llvm-commits
mailing list