[llvm] [AA] Make getSyncEffects sync-scope aware for pointer arguments (PR #211486)

via llvm-commits llvm-commits at lists.llvm.org
Thu Jul 23 00:55:53 PDT 2026


michaelselehov wrote:

## Summary

`getSyncEffects` (used by `getModRefInfo` for fences and stronger-than-monotonic
atomics) reports `NoModRef` for a location whose underlying object does not
escape the function. For a non-byval pointer argument this is wrong under a
cross-thread sync scope: the object is caller-owned and may be shared with peer
threads, so a cross-thread fence/atomic can order accesses to it from other
threads. `noalias`/nocapture is a single-thread aliasing property and says
nothing about other threads; cross-thread ordering is carried by the sync scope.

Symptom (motivating bug): under LTO, GVN/LICM forward a load of a
workgroup-shared buffer across a `fence syncscope("workgroup") acq_rel`,
producing wrong results on GPU targets.

The new test `llvm/test/CodeGen/AMDGPU/gvn-fence-sync-private.ll` demonstrates
the wrong behavior directly: before this change GVN forwards the reload of a
workgroup-shared (`addrspace(3)`) `noalias` buffer across the fence; with the
fix the reload is preserved, while a genuinely thread-private (`addrspace(5)`)
buffer is still forwarded.

This PR makes the exemption sync-scope aware and adds a target hook to recover
precision where it is provably sound.

## Background: what went wrong in #196923

The exemption comes from #196923 ("Reapply [AA] No synchronization effects for
never-escaping identified local"), which introduced `getSyncEffects`: a fence /
stronger-than-monotonic atomic no longer clobbers a location whose underlying
object does not escape the function. That is correct for a genuinely
thread-private object created inside the function (an `alloca`, or a `noalias`
call result such as `malloc`).

The issue is that the same capture/escape reasoning also exempts a `noalias`
pointer *argument*. Escape/capture is a single-thread aliasing property; it says
nothing about whether other threads already hold the address. A `noalias`
argument points to caller-owned memory that peer threads may be concurrently
accessing, which is exactly what a cross-thread fence is there to order. The
exemption only "worked by accident" before because fences were modeled as
clobbering all memory.

This is not hypothetical: it miscompiled real GPU code where one wave produces
values into a workgroup-shared (LDS) buffer and a separate wave consumes them,
with a workgroup-scoped fence ordering the hand-off. The buffer is passed as a
`noalias` pointer, so after the exemption the consumer wave's reload was
forwarded across the fence and read stale data instead of the produced values.

The fix here keeps #196923's win for real thread-private locals and only removes
the exemption where it is unsound: a non-byval pointer argument under a
non-single-thread scope. This also matches the direction #196923 itself hoped
for ("it may be viable to respect potential synchronization inside non-nosync
function calls"): the sync scope, not escape analysis, is what determines
cross-thread ordering.

## The change

1. Generic (`getSyncEffects`, `AliasAnalysis.cpp`): the helper already takes the
   op's `SyncScope::ID`. Under any non-`SingleThread` scope it no longer exempts
   a non-byval pointer argument (it returns the `getModRefInfoMask` result).
   `byval` args and single-thread-scope ops keep the exemption. No address space
   is special-cased.
2. Target hook (`getModRefInfoForSyncOp`): a new virtual on the AA
   `Concept`/`Model` with a `ModRef` default in `AAResultBase`.
   `AAResults::getModRefInfoForSyncOp(Loc, AAQI, SSID)` chains all registered
   AAs (each may narrow to `NoModRef`) and intersects with the generic
   `getSyncEffects` result. Fences and all stronger-than-monotonic atomics route
   through it.
3. AMDGPU (`AMDGPUAliasAnalysis`): overrides the hook to return `NoModRef` when
   every underlying object of the location is `addrspace(5)` (private/scratch).
   Private memory is per-workitem, so no peer thread can reach it under any
   scope. It keys off the underlying objects (not `Loc.Ptr`'s type) so an
   addrspacecast from another space is not mistaken for private.

## Why the generic rule is sound on every target

The generic change only ever *widens* a non-byval pointer argument to the
`getModRefInfoMask` result; it never narrows. So no target can be miscompiled by
it: targets without a refining AA simply become (correctly) conservative
(CHERI purecap, NVPTX, flat-address-space targets all just stay conservative).
The only narrowing path is the opt-in AMDGPU hook, whose `NoModRef` rests on the
invariant that `addrspace(5)` scratch is per-lane and peer-unreachable under all
scopes.

## Why a new AA `Concept` virtual rather than a TTI hook

`getSyncEffects` runs inside `AAResults`, which has no access to
`TargetTransformInfo` (AA does not depend on TTI, and threading TTI through every
AA query would be a large, invasive change). The AA result chain is the
established mechanism for target- and analysis-specific mod/ref refinement, the
same place `ScopedNoAliasAA`, `GlobalsAA` and `TypeBasedAA` already refine
`getModRefInfo`. Adding one virtual there (mirroring the existing
`getModRefInfo(FenceInst*)` hook, which is likewise pure-virtual in the
`Concept`) keeps the refinement in the layer that already owns it.

Routing atomics through the chain is a no-op for every non-AMDGPU AA: they all
inherit the `AAResultBase` default returning `ModRef`, which the intersection
leaves unchanged, so there is no behavior change or compile-time cost for other
targets/AAs. The per-AA hook takes only `(Loc, AAQI)`: a target's answer ("this
address space is peer-unreachable") is scope-independent, and the generic
scope/`SingleThread` logic stays centralized in `getSyncEffects`.

## Tests

- `llvm/test/Transforms/GVN/fence-noalias-syncscope.ll` (new): target-independent
  behavior, cross-thread vs single-thread scope, pointer arg vs `byval`, generic
  vs `addrspace(3)`; all conservative in generic AA.
- `llvm/test/CodeGen/AMDGPU/sync-noalias-private-aa.ll` (new): aa-eval for the
  AMDGPU hook, private `NoModRef`; local(AS3)/global(AS1) `ModRef`;
  addrspacecast-from-generic, select/phi of private, loaded private pointer, and
  `byval` private.
- `llvm/test/CodeGen/AMDGPU/gvn-fence-sync-private.ll` (new): end-to-end GVN on
  amdgcn, AS3 workgroup-shared reload is preserved (the fixed miscompile); AS5
  private load is forwarded (precision preserved by the hook).
- `llvm/test/CodeGen/AMDGPU/rewrite-out-arguments-2.ll` (updated): now includes
  the AMDGPU AA passes; `@fence_seq_cst_after_store` is now rewritten, a sound
  win because the AS5 store's ordering vs the fence is unobservable to peers.
- `llvm/test/Analysis/BasicAA/atomics.ll` (updated): `@noalias_no_escape`
  `%a <-> <sync op>` responses change `NoModRef` to `Both ModRef`; these encoded
  the previous unsound behavior for a cross-thread (default `System`) scope arg.

## Relationship to ScopedNoAliasAA fence metadata (orthogonal)

`ScopedNoAliasAA::getModRefInfo(FenceInst*)` already refines fence mod/ref from
`!alias.scope`/`!noalias` metadata attached to the fence. That is an orthogonal
refinement path (it depends on a pass stamping scoped-noalias metadata onto the
fence). This PR is self-contained and works from the op's inherent sync scope; it
does not depend on, conflict with, or replace the metadata path.


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


More information about the llvm-commits mailing list