Skip to content

ZJIT: Forward guarded values to branch-edge args to dedup CFG-join guards#16828

Open
dak2 wants to merge 1 commit intoruby:masterfrom
dak2:zjit-dedup-guards-at-cfg-joins
Open

ZJIT: Forward guarded values to branch-edge args to dedup CFG-join guards#16828
dak2 wants to merge 1 commit intoruby:masterfrom
dak2:zjit-dedup-guards-at-cfg-joins

Conversation

@dak2
Copy link
Copy Markdown
Contributor

@dak2 dak2 commented May 2, 2026

Summary

When a value is type-guarded in predecessor blocks before a CFG join, the merge block parameter inherits only the widened union type of its inputs. The narrower type proven on each edge is lost at the merge, forcing the same value to be guarded again after the join. Code such as if cond; a = n + 1; else; a = n + 2; end; n + a ends up emitting a redundant Fixnum guard on n after the merge, bloating compiled code and adding runtime overhead.

Add a new HIR pass forward_guarded_values_to_jumps that rewrites branch-edge arguments to the most recent GuardType of that value within the same block. A subsequent infer_types run can then narrow merge-block parameter types, letting fold_constants drop redundant guards at CFG joins.

Fixes: Shopify#978

Benchmark

Microbenchmark targeting the optimization (a value type-guarded on both branches before a CFG join, then used after the merge):

def test(n, cond)                                                                                                           
  if cond   
    a = n + 1
  else                                                                                                                      
    a = n + 2
  end                                                                                                                       
  n + a     
end

N = 100_000_000
1_000.times { test(0, true); test(0, false) } # warmup
N.times { |i| test(i, i.even?) }              # measured                                                                    

Run with ./miniruby --zjit bench_cfg_join.rb (10 trials, N=100M):

┌────────┬──────────┬──────────┬───────────┬─────────────┐                                                                  
│ Metric │  Before  │  After   │   Delta   │ Improvement │
├────────┼──────────┼──────────┼───────────┼─────────────┤                                                                  
│ min    │ 1.8597 s │ 1.8222 s │ -0.0375 s │ -2.0%       │
├────────┼──────────┼──────────┼───────────┼─────────────┤
│ median │ 1.8694 s │ 1.8518 s │ -0.0176 s │ -0.94%      │                                                                  
├────────┼──────────┼──────────┼───────────┼─────────────┤
│ mean   │ 1.8717 s │ 1.8532 s │ -0.0185 s │ -0.99%      │                                                                  
├────────┼──────────┼──────────┼───────────┼─────────────┤                                                                  
│ max    │ 1.9024 s │ 1.8936 s │ -0.0088 s │ -0.46%      │
└────────┴──────────┴──────────┴───────────┴─────────────┘                                                                  

The benchmark calls test 100M times with cond alternating via i.even?, so both branches are exercised and the post-merge guard fires on every call before the fix.
Removing that one guard per call saves ~1% of total wall time on this microbenchmark.
Real workloads will see the benefit only where guarded values flow through CFG joins, but I believe the saving is purely additive (one fewer GuardType execution per affected join per call)

@matzbot matzbot requested a review from a team May 2, 2026 14:40
@dak2 dak2 force-pushed the zjit-dedup-guards-at-cfg-joins branch 2 times, most recently from d0302e0 to 815a087 Compare May 2, 2026 15:13
…ards

When a value is type-guarded in predecessor blocks before a CFG join,
the merge block parameter inherits only the widened union type of its
inputs. The narrower type proven on each edge is lost at the merge,
forcing the same value to be guarded again after the join. Code such
as `if cond; a = n + 1; else; a = n + 2; end; n + a` ends up emitting
a redundant Fixnum guard on `n` after the merge, bloating compiled
code and adding runtime overhead.

Add a new HIR pass `forward_guarded_values_to_jumps` that rewrites
branch-edge arguments to the most recent `GuardType` of that value
within the same block. A subsequent `infer_types` run can then narrow
merge-block parameter types, letting `fold_constants` drop redundant
guards at CFG joins.

Fixes: Shopify#978
@dak2 dak2 force-pushed the zjit-dedup-guards-at-cfg-joins branch from 815a087 to df9e51e Compare May 3, 2026 01:21
@ydah ydah self-assigned this May 3, 2026
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.

ZJIT: Redundant GuardType remains at CFG join blocks

2 participants