Skip to content

Observe all stores we replace in LastStores; keep track of who observed a store - #14080

Merged
cfallin merged 1 commit into
bytecodealliance:mainfrom
fitzgen:issue-14053
Aug 5, 2026
Merged

Observe all stores we replace in LastStores; keep track of who observed a store#14080
cfallin merged 1 commit into
bytecodealliance:mainfrom
fitzgen:issue-14053

Conversation

@fitzgen

@fitzgen fitzgen commented Aug 5, 2026

Copy link
Copy Markdown
Member

Fixes #14053

@fitzgen
fitzgen requested review from a team as code owners August 5, 2026 15:10
@fitzgen
fitzgen requested review from alexcrichton and cfallin and removed request for a team and alexcrichton August 5, 2026 15:10
@cfallin

cfallin commented Aug 5, 2026

Copy link
Copy Markdown
Member

Thanks for this -- very subtle bug here.

I think I have some concerns about the way in which we're reasoning carefully about divergent blocks, and patching conclusions on top of the core analysis, rather than getting the core analysis to give us the right answer from-first-principles. I worry that (especially given the chain of subtle bugs we've had here) we may miss something else, too; and even if not, it's very subtle and difficult to reason about and maintain.

Instead I think the crux of this comes back to this comment that describes why not to do the "store observes last store that it replaces in the abstract state" step I mentioned here (last point).

This would resolve the bug in a principled way because, entering any divergent loop, either a still-downward-exposed store meets a trapping op and is observed, or no traps ever occur and it is just an infinite loop (so is truly not observed). Basically we turn the last-store state into a may-alias rather than must-alias kind of state: any store that could actually be the most recent to a given memory location is either observed (so "committed" in some sense -- we won't remove it) or is in the flow-sensitive state of every path outward from it. The current lossy situation creates the hole that we have to plug instead, and I'm not confident that that's simple enough to reason about that we want to go there.

A question though: the comment linked above mentions some optimization opportunities that observe-store-we-overwrote-in-abstract-state would miss. Are there examples of that case that we know about?

@fitzgen

fitzgen commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Instead I think the crux of this comes back to this comment that describes why not to do the "store observes last store that it replaces in the abstract state" step I mentioned here (last point).

This would resolve the bug in a principled way because, entering any divergent loop, either a still-downward-exposed store meets a trapping op and is observed, or no traps ever occur and it is just an infinite loop (so is truly not observed). Basically we turn the last-store state into a may-alias rather than must-alias kind of state: any store that could actually be the most recent to a given memory location is either observed (so "committed" in some sense -- we won't remove it) or is in the flow-sensitive state of every path outward from it. The current lossy situation creates the hole that we have to plug instead, and I'm not confident that that's simple enough to reason about that we want to go there.

A question though: the comment linked above mentions some optimization opportunities that observe-store-we-overwrote-in-abstract-state would miss. Are there examples of that case that we know about?

If I understand correctly, what you are proposing ultimately just entails removing that comment and observing the last-store instruction for the region at that comment's old location, correct?

modified   cranelift/codegen/src/alias_analysis.rs
@@ -211,40 +211,17 @@ impl LastStores {
         }
         // Store instructions: update the last-store information for this
         // instruction's alias region, or, if it has no alias region, treat it
         // as a fence.
         else if opcode.can_store() {
             if let Some(memflags) = func.dfg.insts[inst].memflags() {
                 match func.dfg.mem_flags[memflags].alias_region() {
                     Some(region) => {
-                        // NB: The old last-store instruction is *not* observed
-                        // here, even though this new store instruction may not
-                        // fully overwrite it. First, a new store in a block
-                        // does not itself observe an old store in the same
-                        // block. Second, the old store will never be an
-                        // optimization candidate again from here on out:
-                        //
-                        // * We won't consider it again as we process the rest
-                        //   of this block, as it won't be in the last-store
-                        //   slot anymore.
-                        //
-                        // * What if we re-process this block in our initial
-                        //   fixed point loop? That implies this block is a
-                        //   member of a cycle in the CFG, but `meet_from` only
-                        //   propagates a store instruction when all
-                        //   predecessors agree on the same last-store
-                        //   instruction, but the predecessors already won't
-                        //   agree it is the old store since this block (which
-                        //   is on that path and therefore some kind of
-                        //   transitive predecessor) has already overridden it.
-                        //
-                        // Therefore, marking the old last-store as observed
-                        // here is unnecessary (and, in fact, doing so would
-                        // only inhibit optimization).
+                        observe(func, observed_stores, self.regions[region]);
                         self.regions[region] = inst.into();
 
                         // If this store can trap, then we need to observe
                         // all other alias regions, to ensure that their state
                         // is preserved in the case that this store traps
                         // (similar to the `can_trap()` handling above).
                         //
                         // This prevents removing the first store in the

That diff does fix #14053 and passes the new tests added in this PR.

However, it fails with missed optimizations on 10 of our alias analysis filetests (maybe even more individual test failures than that, since filetests can have multiple functions/checks, but the runner bails on the first failure).

Full alias filetest failures
$ cargo run -p cranelift-tools -- test cranelift/filetests/filetests/alias/
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/clif-util test cranelift/filetests/filetests/alias/`
[2026-08-05T15:55:13Z ERROR cranelift_filetests::concurrent] FAIL: optimize
FAIL cranelift/filetests/filetests/alias/dead-store-cross-block.clif: optimize

Caused by:
    compilation of function on line 5 does not match
    the text expectation
    
    --- expected
[2026-08-05T15:55:13Z ERROR cranelift_filetests::concurrent] FAIL: optimize
    +++ actual
    @@ -2,6 +2,7 @@
         region0 = 0 "R0"
     
     block0(v0: i64, v1: i32, v2: i32):
    +    store notrap aligned region0 v1, v0
         jump block1
     
     block1:
    
    
    This test assertion can be automatically updated by setting the
    CRANELIFT_TEST_BLESS=1 environment variable when running this test.
             
[2026-08-05T15:55:13Z ERROR cranelift_filetests::concurrent] FAIL: optimize
[2026-08-05T15:55:13Z ERROR cranelift_filetests::concurrent] FAIL: optimize
[2026-08-05T15:55:13Z ERROR cranelift_filetests::concurrent] FAIL: optimize
[2026-08-05T15:55:13Z ERROR cranelift_filetests::concurrent] FAIL: optimize
[2026-08-05T15:55:13Z ERROR cranelift_filetests::concurrent] FAIL: optimize
[2026-08-05T15:55:13Z ERROR cranelift_filetests::concurrent] FAIL: optimize
[2026-08-05T15:55:13Z ERROR cranelift_filetests::concurrent] FAIL: optimize
[2026-08-05T15:55:13Z ERROR cranelift_filetests::concurrent] FAIL: alias-analysis
FAIL cranelift/filetests/filetests/alias/stale-last-store-after-dead-store.clif: alias-analysis

Caused by:
    filecheck failed for function on line 10:
    #0 not: store notrap aligned region0 v1, v0
    #1 not: store notrap aligned region0 v2, v0
    #2 check: store notrap aligned region0 v3, v0
    #3 check: store notrap aligned region0 v3, v0+8
    #4 not: store
    > function %stale_after_dead_store(i64, i32, i32, i32) fast {
    >     region0 = 0 "R0"
    > 
    > block0(v0: i64, v1: i32, v2: i32, v3: i32):
    >     store notrap aligned region0 v1, v0
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Matched #0 not: \bstore notrap aligned region0 v1, v0\b
    >     store notrap aligned region0 v2, v0
    >     store notrap aligned region0 v3, v0
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Matched #2: \bstore notrap aligned region0 v3, v0\b
    >     store notrap aligned region0 v3, v0+8
    >     return
    > }
    
FAIL cranelift/filetests/filetests/alias/crossing-merges.clif: optimize

Caused by:
    compilation of function on line 85 does not match
    the text expectation
    
    --- expected
    +++ actual
    @@ -2,6 +2,7 @@
         region0 = 0 "R0"
     
     block0(v0: i64, v1: i32, v2: i32, v3: i32):
    +    store notrap aligned region0 v1, v0
         brif v1, block1, block2
     
     block1:
    
    
    This test assertion can be automatically updated by setting the
    CRANELIFT_TEST_BLESS=1 environment variable when running this test.
             
FAIL cranelift/filetests/filetests/alias/idempotent-store.clif: optimize

Caused by:
    compilation of function on line 41 does not match
    the text expectation
    
    --- expected
    +++ actual
    @@ -2,6 +2,7 @@
         region0 = 0 "heap"
     
     block0(v0: i64, v1: i32, v2: i32):
    +    store region0 v1, v0+8
         store region0 v2, v0+8
         return
     }
    
    
    This test assertion can be automatically updated by setting the
    CRANELIFT_TEST_BLESS=1 environment variable when running this test.
             
FAIL cranelift/filetests/filetests/alias/dead-store-chain.clif: optimize

Caused by:
    compilation of function on line 6 does not match
    the text expectation
    
    --- expected
    +++ actual
    @@ -2,6 +2,9 @@
         region0 = 0 "R"
     
     block0(v0: i64, v1: i32, v2: i32, v3: i32, v4: i32):
    +    store notrap aligned region0 v1, v0
    +    store notrap aligned region0 v2, v0
    +    store notrap aligned region0 v3, v0
         store notrap aligned region0 v4, v0
         return
     }
    
    
    This test assertion can be automatically updated by setting the
    CRANELIFT_TEST_BLESS=1 environment variable when running this test.
             
FAIL cranelift/filetests/filetests/alias/dead-store-other-region.clif: optimize

Caused by:
    compilation of function on line 6 does not match
    the text expectation
    
    --- expected
    +++ actual
    @@ -3,6 +3,7 @@
         region1 = 1 "R1"
     
     block0(v0: i64, v1: i64, v2: i32, v3: i32, v4: i32):
    +    store notrap aligned region0 v2, v0
         store notrap aligned region1 v4, v1
         store notrap aligned region0 v3, v0
         return
    
    
    This test assertion can be automatically updated by setting the
    CRANELIFT_TEST_BLESS=1 environment variable when running this test.
             
FAIL cranelift/filetests/filetests/alias/no-region.clif: optimize

Caused by:
    compilation of function on line 8 does not match
    the text expectation
    
    --- expected
    +++ actual
    @@ -2,6 +2,8 @@
         region0 = 0 "R0"
     
     block0(v0: i64, v1: i64):
    +    v2 = iconst.i32 4660
    +    store notrap aligned region0 v2, v0  ; v2 = 4660
         v3 = load.i32 notrap aligned v1
         v4 = iconst.i32 0x5678
         store notrap aligned region0 v4, v0  ; v4 = 0x5678
    
    
    This test assertion can be automatically updated by setting the
    CRANELIFT_TEST_BLESS=1 environment variable when running this test.
             
FAIL cranelift/filetests/filetests/alias/issue-13961.clif: optimize

Caused by:
    compilation of function on line 7 does not match
    the text expectation
    
    --- expected
    +++ actual
    @@ -2,6 +2,8 @@
         region0 = 0 "R0"
     
     block0(v0: i64):
    +    v1 = iconst.i32 1
    +    store notrap region0 v1, v0  ; v1 = 1
         v2 = iconst.i32 2
         store notrap region0 v2, v0  ; v2 = 2
         return v2  ; v2 = 2
    
    
    This test assertion can be automatically updated by setting the
    CRANELIFT_TEST_BLESS=1 environment variable when running this test.
             
FAIL cranelift/filetests/filetests/alias/dead-store-pre-gvn-address.clif: optimize

Caused by:
    compilation of function on line 12 does not match
    the text expectation
    
    --- expected
    +++ actual
    @@ -3,6 +3,7 @@
     
     block0(v0: i64, v1: i64):
         v2 = iconst.i64 0
    +    store notrap aligned region0 v1, v2  ; v2 = 0
         store notrap aligned region0 v0, v2  ; v2 = 0
         return
     }
    
    
    This test assertion can be automatically updated by setting the
    CRANELIFT_TEST_BLESS=1 environment variable when running this test.
             
FAIL cranelift/filetests/filetests/alias/check-unset-reset-flag.clif: optimize

Caused by:
    compilation of function on line 5 does not match
    the text expectation
    
    --- expected
    +++ actual
    @@ -4,6 +4,8 @@
     block0(v0: i64, v1: i32):
         v2 = load.i64 notrap aligned region0 v0
         trapz v2, user42
    +    v3 = iconst.i64 0
    +    store notrap aligned region0 v3, v0  ; v3 = 0
         store notrap aligned region0 v2, v0
         v4 = iadd v1, v1
         return v4
    
    
    This test assertion can be automatically updated by setting the
    CRANELIFT_TEST_BLESS=1 environment variable when running this test.
             
38 tests
Error: 10 failures

Digging into one of the failures, check-unset-reset-flag.clif, which is based on the code pattern we see in component model adapter functions, we have this initial CLIF:

function u1:0(i64 vmctx, i32) -> i32 tail {
    region0 = 0 "flag"

block0(v0: i64, v1: i32):
    v2 = load.i64 notrap aligned region0 v0
    trapz v2, user42
    v3 = iconst.i64 0
    store notrap aligned region0 v3, v0
    v4 = iadd v1, v1
    store notrap aligned region0 v2, v0
    return v4
}

Today, we expect the first store (of v3) to be DSE'd by the second store (of v2), resulting in this output CLIF:

; function u1:0(i64 vmctx, i32) -> i32 tail {
;     region0 = 0 "flag"
;
; block0(v0: i64, v1: i32):
;     v2 = load.i64 notrap aligned region0 v0
;     trapz v2, user42
;     store notrap aligned region0 v2, v0
;     v4 = iadd v1, v1
;     return v4
; }

(Note: removing the remaining store requires additional work to "rewind" the LastStores state after removing a dead store or multiple iterations of our alias analysis; either way, we don't remove it today on main.)

However, the patch above marks the first store observed when we replace it with the second one inside LastStores, so we fail to DSE the first store because it is marked observed, resulting in CLIF output identical to the input.

I think that, effectively, that patch means we would only ever be able to DSE the last store to a region inside a block, and will never be able to DSE when the maybe-dead store and overwriter are both within the same block, which feels pretty restrictive.

@cfallin

cfallin commented Aug 5, 2026

Copy link
Copy Markdown
Member

Nick and I discussed briefly offline; to record for posterity:

  • The issue is that the analysis runs in a separate phase before the editing pass, so store B overwriting store A also overwrites, hence observes, A, so nothing ever actually opts.
  • My proposed tweak is to record which instruction observes; all we need for this is a 0-1-many lattice. If a given store is only ever observed by one other store (and all the other conditions are met) then we should be able to do the opt. We still want the general "removing from flow-sensitive state observes" for other cases for correctness, I think.

@fitzgen fitzgen changed the title Do not DSE on paths that can diverge Observe all stores we replace in LastStores; keep track of who observed a store Aug 5, 2026

@cfallin cfallin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@cfallin
cfallin added this pull request to the merge queue Aug 5, 2026
Merged via the queue into bytecodealliance:main with commit 0b0820c Aug 5, 2026
53 checks passed
@fitzgen
fitzgen deleted the issue-14053 branch August 5, 2026 19:27
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.

Dead-store-elimintation removes visible stores from divergent paths

2 participants