Skip to content

FixAddrSpaceCast: place lowered PHI loads on their incoming edge - #426

Open
pvelesko wants to merge 2 commits into
intel:masterfrom
pvelesko:fix/phi-edge-load-placement
Open

FixAddrSpaceCast: place lowered PHI loads on their incoming edge#426
pvelesko wants to merge 2 commits into
intel:masterfrom
pvelesko:fix/phi-edge-load-placement

Conversation

@pvelesko

@pvelesko pvelesko commented Aug 4, 2026

Copy link
Copy Markdown

lowerLoadsfromPHI() anchors each per-edge load at the definition of the incoming pointer (FixAddrSpaceCast.cpp:136 and :159) instead of in the PHI's incoming block. The definition only has to dominate the incoming block, so the load can be emitted in an earlier block, ahead of stores the original load was ordered after.

Input:

false.bb:
  %addr.res = addrspacecast ptr addrspace(3) %p to ptr addrspace(4)
  store i32 0, ptr addrspace(4) %addr.res, align 4
  br label %merge

merge:
  %sel = phi ptr addrspace(4) [ %gep.res, %true.bb ], [ %addr.res, %false.bb ]
  %v = load i32, ptr addrspace(4) %sel, align 4

The store writes through %addr.res, which is %p relabelled, so %v has to read what the store wrote.

Before, the lowered load is hoisted above that store and reads stale memory:

 false.bb:
-  %1 = load i32, ptr addrspace(3) %p, align 4
   %addr.res = addrspacecast ptr addrspace(3) %p to ptr addrspace(4)
   store i32 0, ptr addrspace(4) %addr.res, align 4
   br label %merge

After:

 false.bb:
   %addr.res = addrspacecast ptr addrspace(3) %p to ptr addrspace(4)
   store i32 0, ptr addrspace(4) %addr.res, align 4
+  %1 = load i32, ptr addrspace(3) %p, align 4
   br label %merge

Inserting at the incoming block's terminator is always legal: SSA guarantees the incoming value dominates it.

lowerLoadsDifferentIncomingAndParent.ll is updated because it covers exactly this shape. %addr.res is defined in %cond.false3459 while the PHI's incoming block is %cond.between, and the load now lands in the latter. That file is UNSUPPORTED: llvm-17-plus, so I could not execute it here; I ran the same function in opaque-pointer form instead and the load moves from %cond.false3459 to %cond.between as the updated CHECK lines expect.

Fixes #398

removeAddrSpaceCastsFromPhiIncomingBBs() replaces a load from a PHI of
generic pointers with one load per incoming edge. Each of those loads
has to execute on the edge it belongs to.

Add a test with the shape that breaks it: a 3-edge PHI inside a loop
whose incoming values come from mixed storage classes (two Workgroup,
one Function). The Function edge's pointer is defined by an
addrspacecast in the entry block, so anchoring the new load at that
definition hoists it out of the loop entirely, where it reads the
private accumulator before it has been initialised. A second case
covers a load hoisted above a store to the same location.

Expected to fail until the placement is corrected.

Reduced from intel#398.

Signed-off-by: Paulius Velesko <pvelesko@pglc.io>
lowerLoadsfromPHI() rewrites a load from a PHI of generic pointers into one
load per incoming edge, but built its IRBuilder at the definition of the
incoming pointer (IRBuilder<> Builder(LoadSource), NewInst->insertAfter(
LoadSource)). That is only correct when the definition happens to be the last
thing on the edge.

It frequently is not. When the incoming value is an addrspacecast of an
alloca, the cast is hoisted to the entry block or a loop preheader, so the
per-edge load is emitted there too, outside the loop containing the PHI. An
accumulator is then read once, before it is ever initialised, and every
iteration merges that stale value. The same mistake hoists the load above
stores to the location it reads.

Build one IRBuilder per incoming edge at the incoming block's terminator and
insert both the bitcast and the load through it. SSA guarantees the incoming
value dominates that point, so the insertion is always legal, and the load can
only move later than before, never earlier.

This also removes a latent segfault. Under opaque pointers the CreateBitCast
above is a no-op cast, so it returns its operand rather than a new
instruction; the following cast<Instruction> is unchecked in a Release build,
so LoadSource became a bogus Instruction* whenever the addrspacecast operand
was an Argument or a global. insertAfter() then dereferenced it and crashed.
Builder.Insert() never dereferences LoadSource, and the remaining
setOperand(0, LoadSource) is pointer-identical to the operand's Value*, so it
stays correct.

lowerLoadsDifferentIncomingAndParent.ll had the old placement baked into its
CHECK lines: it expected the bitcast and load in the addrspacecast's parent
block (%cond.false3459) rather than in the incoming block (%cond.between).
Updated to the correct block. That file is UNSUPPORTED: llvm-17-plus, so it
only runs on LLVM 16 and older; leaving its CHECK lines alone would have left
it failing there, because the fix definitively moves those instructions out of
%cond.false3459.

Fixes the test added in the preceding commit.

Fixes intel#398

Signed-off-by: Paulius Velesko <pvelesko@pglc.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Miscompile: 3-edge OpPhi with Generic pointers from mixed storage classes

1 participant