[llvm] [TargetParser] Encode addrspacecast validity in Triple (PR #217345)
Michal Paszkowski via llvm-commits
llvm-commits at lists.llvm.org
Wed Aug 19 06:46:09 PDT 2026
https://github.com/michalpaszkowski created https://github.com/llvm/llvm-project/pull/217345
Problem
-------
isDereferenceableAndAlignedPointer() in llvm/lib/Analysis/Loads.cpp looks through addrspacecast unconditionally:
if (const AddrSpaceCastOperator *ASC = dyn_cast<AddrSpaceCastOperator>(V))
return isDereferenceableAndAlignedPointer(ASC->getOperand(0), ...);
i.e. the casted pointer is treated as dereferenceable whenever the source pointer is. That is only sound when the cast is a no-op -- when it preserves both the bit pattern and the represented address. For a non-no-op cast the result may denote an entirely different location, so the source's dereferenceability says nothing about the casted pointer.
This makes speculation across non-no-op casts appear legal. SimplifyCFG is the most visible consumer: foldTwoEntryPHINode() gates hoisting on dominatesMergePoint() -> isSafeToSpeculativelyExecute(), whose Load case (ValueTracking.cpp) delegates straight to isDereferenceableAndAlignedPointer(). As a result an address-space-guard such as
define i32 @f(ptr addrspace(4) dereferenceable(4) %p, i1 %c) {
entry:
br i1 %c, label %local, label %global
local:
%lp = addrspacecast ptr addrspace(4) %p to ptr addrspace(3)
%lv = load i32, ptr addrspace(3) %lp
br label %merge
global:
%gp = addrspacecast ptr addrspace(4) %p to ptr addrspace(1)
%gv = load i32, ptr addrspace(1) %gp
br label %merge
merge:
%v = phi i32 [ %lv, %local ], [ %gv, %global ]
ret i32 %v
}
is flattened into two unconditional loads fed to a select:
%lv = load i32, ptr addrspace(3) %lp ; now always executed
%gv = load i32, ptr addrspace(1) %gp ; now always executed
%v = select i1 %c, i32 %lv, i32 %gv
The branch that selected the address space is gone, so the addrspace(3) load executes even when %p refers to addrspace(1). If casting addrspace(4)->(3) is not address-preserving on the target, this reads a different location. The problem became easy to hit once opaque pointers stopped AlignmentAnalysis from recovering the address space from a typed pointer.
Whether an addrspacecast is a no-op is target knowledge, exposed via TargetMachine::isNoopAddrSpaceCast(). Mid-level analyses that hold only a DataLayout cannot reach it. One alternative is to thread TargetTransformInfo through isDereferenceableAndAlignedPointer(), isSafeToSpeculativelyExecute() and
isSafeToSpeculativelyExecuteWithOpcode() and query TTI. But this is intrusive, pushes a codegen-level dependency into core IR analyses, and still only helps callers that happen to have a TTI.
Proposal
--------
For the purpose of isDereferenceableAndAlignedPointer(), the exact "no-op" nature of the cast is not the property we need. What we need is that a dereferenceable pointer in the source address space casts to a dereferenceable pointer in the destination address space, i.e. the notion behind TTI's isValidAddrSpaceCast(). Encoding that full 24-bit x 24-bit function in the data layout is not reasonable, but it can be approximated well enough for mid-level analyses as a pure function of the target Triple.
Add:
bool Triple::isValidAddrSpaceCast(unsigned SrcAS, unsigned DstAS) const;
which returns true when a pointer known to be dereferenceable in SrcAS is guaranteed to remain dereferenceable after an addrspacecast to DstAS on this target. Identity is always valid; the arch-specific rules mirror the existing TargetMachine::isNoopAddrSpaceCast() hooks that can be determined from the arch alone (AMDGPU flat/global/constant group, X86/Mips limit AS < 256, ARM/AArch64/PPC/Hexagon/LoongArch/RISC-V unconditionally true). Everything else is conservatively false.
The addrspacecast case in Loads.cpp looks up the Triple via the module of the AddrSpaceCastOperator (falling back to SQ.CxtI, and to a conservative false when no module is reachable) and only looks through the cast when Triple::isValidAddrSpaceCast() holds.
Compatibility
-------------
The check is target-specific and conservative by default. For triples whose TargetMachine::isNoopAddrSpaceCast() cannot be captured from the Triple alone (or that are not enumerated here), the analysis now stops at addrspacecast instead of unconditionally looking through it, so front ends may see fewer speculations across such casts.
>From 5b8d63706f1163e86fd1f5f7a2aec7040d0fbc0d Mon Sep 17 00:00:00 2001
From: Michal Paszkowski <michal.paszkowski at intel.com>
Date: Mon, 20 Jul 2026 23:16:22 -0700
Subject: [PATCH] [TargetParser] Encode addrspacecast validity in Triple
Problem
-------
isDereferenceableAndAlignedPointer() in llvm/lib/Analysis/Loads.cpp
looks through addrspacecast unconditionally:
if (const AddrSpaceCastOperator *ASC = dyn_cast<AddrSpaceCastOperator>(V))
return isDereferenceableAndAlignedPointer(ASC->getOperand(0), ...);
i.e. the casted pointer is treated as dereferenceable whenever the source
pointer is. That is only sound when the cast is a no-op -- when it
preserves both the bit pattern and the represented address. For a
non-no-op cast the result may denote an entirely different location, so
the source's dereferenceability says nothing about the casted pointer.
This makes speculation across non-no-op casts appear legal. SimplifyCFG
is the most visible consumer: foldTwoEntryPHINode() gates hoisting on
dominatesMergePoint() -> isSafeToSpeculativelyExecute(), whose Load case
(ValueTracking.cpp) delegates straight to isDereferenceableAndAlignedPointer().
As a result an address-space-guard such as
define i32 @f(ptr addrspace(4) dereferenceable(4) %p, i1 %c) {
entry:
br i1 %c, label %local, label %global
local:
%lp = addrspacecast ptr addrspace(4) %p to ptr addrspace(3)
%lv = load i32, ptr addrspace(3) %lp
br label %merge
global:
%gp = addrspacecast ptr addrspace(4) %p to ptr addrspace(1)
%gv = load i32, ptr addrspace(1) %gp
br label %merge
merge:
%v = phi i32 [ %lv, %local ], [ %gv, %global ]
ret i32 %v
}
is flattened into two unconditional loads fed to a select:
%lv = load i32, ptr addrspace(3) %lp ; now always executed
%gv = load i32, ptr addrspace(1) %gp ; now always executed
%v = select i1 %c, i32 %lv, i32 %gv
The branch that selected the address space is gone, so the addrspace(3)
load executes even when %p refers to addrspace(1). If casting
addrspace(4)->(3) is not address-preserving on the target, this reads a
different location. The problem became easy to hit once opaque pointers
stopped AlignmentAnalysis from recovering the address space from a typed
pointer.
Whether an addrspacecast is a no-op is target knowledge, exposed via
TargetMachine::isNoopAddrSpaceCast(). Mid-level analyses that hold only
a DataLayout cannot reach it. One alternative is to thread
TargetTransformInfo through isDereferenceableAndAlignedPointer(),
isSafeToSpeculativelyExecute() and
isSafeToSpeculativelyExecuteWithOpcode() and query TTI. But this is
intrusive, pushes a codegen-level dependency into core IR analyses, and
still only helps callers that happen to have a TTI.
Proposal
--------
For the purpose of isDereferenceableAndAlignedPointer(), the exact
"no-op" nature of the cast is not the property we need. What we need is
that a dereferenceable pointer in the source address space casts to a
dereferenceable pointer in the destination address space, i.e. the
notion behind TTI's isValidAddrSpaceCast(). Encoding that full 24-bit x
24-bit function in the data layout is not reasonable, but it can be
approximated well enough for mid-level analyses as a pure function of
the target Triple.
Add:
bool Triple::isValidAddrSpaceCast(unsigned SrcAS, unsigned DstAS) const;
which returns true when a pointer known to be dereferenceable in SrcAS
is guaranteed to remain dereferenceable after an addrspacecast to DstAS
on this target. Identity is always valid; the arch-specific rules mirror
the existing TargetMachine::isNoopAddrSpaceCast() hooks that can be
determined from the arch alone (AMDGPU flat/global/constant group,
X86/Mips limit AS < 256, ARM/AArch64/PPC/Hexagon/LoongArch/RISC-V
unconditionally true). Everything else is conservatively false.
The addrspacecast case in Loads.cpp looks up the Triple via the module
of the AddrSpaceCastOperator (falling back to SQ.CxtI, and to a
conservative false when no module is reachable) and only looks through
the cast when Triple::isValidAddrSpaceCast() holds.
Compatibility
-------------
The check is target-specific and conservative by default. For triples
whose TargetMachine::isNoopAddrSpaceCast() cannot be captured from the
Triple alone (or that are not enumerated here), the analysis now stops
at addrspacecast instead of unconditionally looking through it, so
front ends may see fewer speculations across such casts.
---
llvm/include/llvm/TargetParser/Triple.h | 5 ++
llvm/lib/Analysis/Loads.cpp | 22 ++++++-
llvm/lib/TargetParser/Triple.cpp | 53 +++++++++++++++
llvm/test/Transforms/SROA/addrspacecast.ll | 1 +
.../speculate-addrspacecast-load.ll | 66 +++++++++++++++++++
5 files changed, 144 insertions(+), 3 deletions(-)
create mode 100644 llvm/test/Transforms/SimplifyCFG/speculate-addrspacecast-load.ll
diff --git a/llvm/include/llvm/TargetParser/Triple.h b/llvm/include/llvm/TargetParser/Triple.h
index 18634a8bbe6ff..112ecb2d23089 100644
--- a/llvm/include/llvm/TargetParser/Triple.h
+++ b/llvm/include/llvm/TargetParser/Triple.h
@@ -1376,6 +1376,11 @@ class Triple {
/// Test whether the target triple is for a GPU.
bool isGPU() const { return isSPIROrSPIRV() || isNVPTX() || isAMDGPU(); }
+ /// Returns true if a pointer known to be dereferenceable in address space
+ /// \p SrcAS is guaranteed to remain dereferenceable after an
+ /// `addrspacecast` to address space \p DstAS on this target.
+ LLVM_ABI bool isValidAddrSpaceCast(unsigned SrcAS, unsigned DstAS) const;
+
/// Merge target triples.
LLVM_ABI std::string merge(const Triple &Other) const;
diff --git a/llvm/lib/Analysis/Loads.cpp b/llvm/lib/Analysis/Loads.cpp
index 84dcbbaa4b6dd..89cef5f9c0309 100644
--- a/llvm/lib/Analysis/Loads.cpp
+++ b/llvm/lib/Analysis/Loads.cpp
@@ -23,6 +23,7 @@
#include "llvm/IR/DataLayout.h"
#include "llvm/IR/GetElementPtrTypeIterator.h"
#include "llvm/IR/IntrinsicInst.h"
+#include "llvm/IR/Module.h"
#include "llvm/IR/Operator.h"
using namespace llvm;
@@ -215,9 +216,24 @@ static bool isDereferenceableAndAlignedPointer(
Alignment, Size, SQ, IgnoreFree,
Visited, MaxDepth);
- if (const AddrSpaceCastOperator *ASC = dyn_cast<AddrSpaceCastOperator>(V))
- return isDereferenceableAndAlignedPointer(
- ASC->getOperand(0), Alignment, Size, SQ, IgnoreFree, Visited, MaxDepth);
+ if (const AddrSpaceCastOperator *ASC = dyn_cast<AddrSpaceCastOperator>(V)) {
+ // Only look through the cast when the target guarantees that a
+ // dereferenceable pointer in the source AS is also dereferenceable in the
+ // destination AS. Otherwise the source's dereferenceability tells us
+ // nothing about the casted pointer, which may denote an unrelated
+ // location. Whether this holds depends on the target.
+ const Module *M = nullptr;
+ if (const auto *I = dyn_cast<Instruction>(ASC))
+ M = I->getModule();
+ else if (SQ.CxtI)
+ M = SQ.CxtI->getModule();
+ if (M && M->getTargetTriple().isValidAddrSpaceCast(
+ ASC->getSrcAddressSpace(), ASC->getDestAddressSpace()))
+ return isDereferenceableAndAlignedPointer(ASC->getOperand(0), Alignment,
+ Size, SQ, IgnoreFree,
+ Visited, MaxDepth);
+ return false;
+ }
return SQ.AC &&
isDereferenceableAndAlignedPointerViaAssumption(
diff --git a/llvm/lib/TargetParser/Triple.cpp b/llvm/lib/TargetParser/Triple.cpp
index 5bfaae7fbc998..c94655037b4db 100644
--- a/llvm/lib/TargetParser/Triple.cpp
+++ b/llvm/lib/TargetParser/Triple.cpp
@@ -10,6 +10,7 @@
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringSwitch.h"
+#include "llvm/Support/AMDGPUAddrSpace.h"
#include "llvm/Support/CodeGen.h"
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/SwapByteOrder.h"
@@ -2321,6 +2322,58 @@ std::string Triple::merge(const Triple &Other) const {
return Other.str();
}
+bool Triple::isValidAddrSpaceCast(unsigned SrcAS, unsigned DstAS) const {
+ if (SrcAS == DstAS)
+ return true;
+
+ switch (getArch()) {
+ // AMDGPU addrspacecasts among flat, global, constant (and reserved
+ // downstream address spaces) preserve dereferenceability.
+ case Triple::amdgpu:
+ case Triple::r600:
+ return AMDGPU::isFlatGlobalAddrSpace(SrcAS) &&
+ AMDGPU::isFlatGlobalAddrSpace(DstAS);
+
+ // X86 address spaces >= 256 are segment-relative offsets that refer to
+ // different memory; only casts within the first 256 spaces preserve
+ // dereferenceability.
+ case Triple::x86:
+ case Triple::x86_64:
+ return SrcAS < 256 && DstAS < 256;
+
+ // Mips reserves the first 256 address spaces for software use (e.g. OpenCL)
+ // and treats casts between them as noops.
+ case Triple::mips:
+ case Triple::mipsel:
+ case Triple::mips64:
+ case Triple::mips64el:
+ return SrcAS < 256 && DstAS < 256;
+
+ // These targets have no distinct hardware address spaces and their
+ // TargetMachine hooks unconditionally treat addrspacecasts as noops.
+ case Triple::arm:
+ case Triple::armeb:
+ case Triple::thumb:
+ case Triple::thumbeb:
+ case Triple::aarch64:
+ case Triple::aarch64_be:
+ case Triple::aarch64_32:
+ case Triple::hexagon:
+ case Triple::loongarch32:
+ case Triple::loongarch64:
+ case Triple::ppc:
+ case Triple::ppcle:
+ case Triple::ppc64:
+ case Triple::ppc64le:
+ case Triple::riscv32:
+ case Triple::riscv64:
+ return true;
+
+ default:
+ return false;
+ }
+}
+
bool Triple::isMacOSXVersionLT(unsigned Major, unsigned Minor,
unsigned Micro) const {
assert(isMacOSX() && "Not an OS X triple!");
diff --git a/llvm/test/Transforms/SROA/addrspacecast.ll b/llvm/test/Transforms/SROA/addrspacecast.ll
index 74201d56a9783..9573b53909c6e 100644
--- a/llvm/test/Transforms/SROA/addrspacecast.ll
+++ b/llvm/test/Transforms/SROA/addrspacecast.ll
@@ -3,6 +3,7 @@
; RUN: opt < %s -passes='sroa<modify-cfg>' -S | FileCheck %s --check-prefixes=CHECK,CHECK-MODIFY-CFG
target datalayout = "e-p:64:64:64-p1:16:16:16-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:32:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-n8:16:32:64"
+target triple = "x86_64-unknown-linux-gnu"
declare void @llvm.memcpy.p0.p1.i32(ptr nocapture writeonly, ptr addrspace(1) nocapture readonly, i32, i1 immarg) #0
declare void @llvm.memcpy.p1.p0.i32(ptr addrspace(1) nocapture writeonly, ptr nocapture readonly, i32, i1 immarg) #0
diff --git a/llvm/test/Transforms/SimplifyCFG/speculate-addrspacecast-load.ll b/llvm/test/Transforms/SimplifyCFG/speculate-addrspacecast-load.ll
new file mode 100644
index 0000000000000..9d4cf6163b3e6
--- /dev/null
+++ b/llvm/test/Transforms/SimplifyCFG/speculate-addrspacecast-load.ll
@@ -0,0 +1,66 @@
+; RUN: opt -S -passes=simplifycfg < %s | FileCheck %s
+
+; SimplifyCFG may only speculate a load through an addrspacecast when a
+; dereferenceable pointer in the source address space is guaranteed to remain
+; dereferenceable in the destination address space. Whether that holds is
+; target knowledge exposed by Triple::isValidAddrSpaceCast().
+;
+; On AMDGPU, casts among flat (0), global (1), and constant (4) preserve
+; dereferenceability, but a cast into local (3) does not. So a load reached
+; through addrspacecast 4->1 can be speculated, while one reached through 4->3
+; must not be.
+
+target triple = "amdgcn-amd-amdhsa"
+
+; The local cast (4->3) is not a dereferenceability-preserving cast on AMDGPU,
+; so the dereferenceable(4) fact on the constant-AS pointer does not carry over
+; and neither load may be speculated. The conditional branch must be preserved.
+define i32 @no_speculate_local(ptr addrspace(4) dereferenceable(4) %p, i1 %c) {
+; CHECK-LABEL: define i32 @no_speculate_local(
+; CHECK: entry:
+; CHECK-NOT: load
+; CHECK: br i1
+; CHECK: then:
+; CHECK: load i32, ptr addrspace(3)
+; CHECK: else:
+; CHECK: load i32, ptr addrspace(1)
+entry:
+ br i1 %c, label %then, label %else
+
+then:
+ %as3 = addrspacecast ptr addrspace(4) %p to ptr addrspace(3)
+ %v1 = load i32, ptr addrspace(3) %as3, align 1
+ br label %exit
+
+else:
+ %as1 = addrspacecast ptr addrspace(4) %p to ptr addrspace(1)
+ %v2 = load i32, ptr addrspace(1) %as1, align 1
+ br label %exit
+
+exit:
+ %res = phi i32 [ %v1, %then ], [ %v2, %else ]
+ ret i32 %res
+}
+
+; The global cast (4->1) preserves dereferenceability on AMDGPU, so the load is
+; known dereferenceable and SimplifyCFG can speculate it, folding the branch
+; into a select.
+define i32 @speculate_global(ptr addrspace(4) dereferenceable(4) %p, i1 %c) {
+; CHECK-LABEL: define i32 @speculate_global(
+; CHECK: [[AS1:%.*]] = addrspacecast ptr addrspace(4) %p to ptr addrspace(1)
+; CHECK: [[V:%.*]] = load i32, ptr addrspace(1) [[AS1]]
+; CHECK: [[RES:%.*]] = select i1 %c, i32 [[V]], i32 0
+; CHECK: ret i32 [[RES]]
+; CHECK-NOT: br i1
+entry:
+ br i1 %c, label %load, label %exit
+
+load:
+ %as1 = addrspacecast ptr addrspace(4) %p to ptr addrspace(1)
+ %v = load i32, ptr addrspace(1) %as1, align 1
+ br label %exit
+
+exit:
+ %res = phi i32 [ %v, %load ], [ 0, %entry ]
+ ret i32 %res
+}
More information about the llvm-commits
mailing list