Skip to content

ZJIT: lobsters perf burndown #1039

Description

@k0kubun

I'm focusing on lobsters again, so I filed a separate issue from #1030. Take 3 of the effort after #833 and #713.

I have a branch where ZJIT is faster than YJIT on lobsters. The goal of this issue is to upstream the most reasonable ideas out of it in cleaner patches, not necessarily porting everything.

yjit: ruby 4.1.0dev (2026-08-26T21:52:46Z master 0968fd4b3b) +YJIT +PRISM [x86_64-linux]
zjit-before: ruby 4.1.0dev (2026-08-26T21:52:46Z master 0968fd4b3b) +ZJIT +PRISM [x86_64-linux]
zjit-after: ruby 4.1.0dev (2026-08-26T23:15:25Z zjit/min-straight d2330a712c) +ZJIT +PRISM [x86_64-linux]

--------  ------------  ----------------  ---------------  -------------------  ------------------  ----------------  ---------------
bench        yjit (ms)  zjit-before (ms)  zjit-after (ms)  zjit-before 1st itr  zjit-after 1st itr  yjit/zjit-before  yjit/zjit-after
lobsters  421.7 ± 6.3%      461.9 ± 4.8%     399.5 ± 5.0%                0.533               0.459             0.913            1.056
--------  ------------  ----------------  ---------------  -------------------  ------------------  ----------------  ---------------

Branch

This is a branch that originally had more patches, which got reduced to fewer patches and then rebased.

List of patches

I have previously benchmarked each revision so we can know the impact of each change. Note: The latency was measured on a different CPU from the above.

Assignee # sha speedup ms/itr Δ commit
b59ea8efc7 1.000 452.2 ZJIT: Reserve a label reference's bytes by writing them (ruby#18519) (base)
1 fbccb8ac4f 1.001 451.9 +0.07% ZJIT: Expose bucket counts on DistributionSummary
2 93970417c8 1.001 451.8 +0.04% ZJIT: Guard the profiled receiver types of megamorphic call sites
3 72b5ea6116 1.002 451.2 +0.13% ZJIT: Guard receiver types for sends with a literal block
4 e3e36fd91b 1.003 451.0 +0.05% ZJIT: Dispatch megamorphic-by-class call sites on one ancestor guard
5 a8e4db59ea 1.000 452.1 -0.26% ZJIT: Dispatch to blocks that only return with a direct JIT-to-JIT call
6 0f4fb7e44d 1.002 451.4 +0.16% ZJIT: Look megamorphic send targets up in a class table
7 823afa2949 1.000 452.1 -0.16% ZJIT: Fix invokeblock block-handler profiling and gate its ISEQ chain
8 4e3d59ce9e 0.997 453.6 -0.34% ZJIT: Inline a block whose non-local return escapes to the frame we compile
9 9b1e8fcf85 0.998 453.4 +0.06% ZJIT: Destructure a lone yielded Array for the direct block dispatch
10 6825d5469c 1.000 452.1 +0.28% ZJIT: Destructure a lone yielded Array at polymorphic yield sites too
11 d217d309ae 1.014 446.1 +1.32% ZJIT: Specialize yield whose arity does not match the block
12 823ec0dfe3 1.006 449.7 -0.81% ZJIT: Lay the inlined block frame out on the reshaped yield arguments
13 3c1bfc2d26 1.003 451.0 -0.28% ZJIT: Narrow test reg, imm to the immediate's width
14 9b9dcd8675 0.995 454.6 -0.81% ZJIT: Call megamorphic ISEQ targets without leaving JIT code
@k0kubun 15 6e60630c8c 1.009 448.4 +1.38% ZJIT: Allocate JITFrames below INT32_MAX
@k0kubun 16 2a136622c3 1.015 445.7 +0.60% ZJIT: Compile direct sends to def foo(...) callees
17 b30aaf1a65 1.018 444.4 +0.28% ZJIT: Compile direct sends to callees with a **rest parameter
18 c13d38b154 1.017 444.8 -0.07% ZJIT: Read the throwing frame's ISEQ through CFP_ISEQ() in vm_throw_start
19 3be45317f9 1.016 445.0 -0.06% ZJIT: Let a yield-unlocking iterator past the caller's inline budget
20 a15063d049 1.020 443.2 +0.42% ZJIT: Compile send/send with a profiled method name
21 40c28541b7 1.012 446.9 -0.84% ZJIT: Size megamorphic send tables to the shapes that need them
22 e94829f07e 1.011 447.1 -0.05% ZJIT: Compile splat calls instead of dispatching them dynamically
23 c5537ef02d 1.010 447.6 -0.11% ZJIT: Read nil for an out-of-bounds Array#[] instead of side-exiting
24 54275ab60d 1.011 447.4 +0.05% ZJIT: Forward a splat straight to the callee's rest parameter
25 5a292fe3dc 1.019 443.7 +0.83% ZJIT: Pass a &blk argument through to a direct send
26 4a69b3d8d9 1.019 443.9 -0.06% ZJIT: Dispatch a yield to a block that can break directly
27 d3d8eb23fe 1.022 442.6 +0.30% ZJIT: Only branch on the block param proxy for an ISEQ callee
28 39068fc5cf 1.020 443.2 -0.14% ZJIT: Pass a &blk argument through to a direct C call
29 798df3f76e 1.025 441.4 +0.41% ZJIT: Compile direct sends to callees with an anonymous **
30 b40ada0c4f 1.027 440.2 +0.27% ZJIT: Inline callees that a &blk call site passes a block handler to
31 d146a14009 1.027 440.5 -0.07% ZJIT: Stop side-exiting forever at unstable super sites
32 1766bf5894 1.029 439.3 +0.26% ZJIT: Dispatch polymorphic super on the frame's method entry
33 a3ef689fb5 1.018 444.1 -1.08% ZJIT: Compile protected calls behind a check on the caller's self
34 7e2099dc2f 1.033 437.7 +1.43% ZJIT: Compile a yield to a Symbol block as the send it stands for
35 ba1d1651a9 1.026 440.9 -0.73% ZJIT: Drop the now-unreachable invokeblock may-throw fallback reason
36 c069a9e13d 1.031 438.6 +0.52% ZJIT: Bump VM_INSTRUCTION_SIZE for the new profiled splatarray
37 38eea21016 1.025 441.4 -0.63% ZJIT: Pin the forwardable callee's fallback on a &blk call site
38 5de9a21874 1.011 447.3 -1.34% ZJIT: Let a frozen ivar dispatch earn back the shape it is missing
39 7dd6d08c8b 1.018 444.2 +0.68% ZJIT: Fall back to a generic ivar access when a shape chain misses
40 0f57cbd57f 1.025 441.1 +0.70% ZJIT: Back ivar shape-chain misses with a per-name shape table
41 276574d496 1.027 440.3 +0.20% ZJIT: Weigh an ivar respecialization by the traffic it would take off the fallback
42 89667e6d5e 1.027 440.5 -0.05% ZJIT: Test the ivar reprofile budget inline
43 e38013d7fb 1.031 438.7 +0.41% ZJIT: Hand the SP register back when returning past inlined frames
44 77c4a6b775 1.034 437.3 +0.32% ZJIT: Cache the ivar location of class and module shapes
45 3994816525 1.028 439.9 -0.59% ZJIT: Let a getivar site's profile forget boot-time shapes
46 307ad77f2b 1.033 437.9 +0.44% ZJIT: Test profile eviction and cached class ivars
47 fc2f11dd08 1.028 439.8 -0.43% ZJIT: Size ivar shape tables to the names that need them
48 d370a90838 1.022 442.4 -0.59% ZJIT: Dispatch yield to a run-time block ISEQ without leaving JIT code
49 46e64ced1c 1.025 441.4 +0.24% ZJIT: Insert the missing zjit_splatarray in the checked-in bindings
50 ffae6f64b2 1.018 444.2 -0.65% ZJIT: Dispatch a Symbol block's send through the megamorphic class table
51 360ecfac1a 1.024 441.5 +0.61% ZJIT: Inline def foo(...) callees and compile the bar(...) inside them
52 6257de4477 1.049 431.0 +2.38% ZJIT: Specialize bar(...) in standalone forwardable ISEQs
53 e0a3710002 1.049 431.0 +0.01% ZJIT: Fuse Test and fixnum compares into CondBranch
54 13450fe9e6 1.056 428.4 +0.60% ZJIT: Fuse type tests into the branch that consumes them
55 3e1855f801 1.062 425.9 +0.60% ZJIT: Split cold code into an outlined code region
56 939a7e65d0 1.064 424.9 +0.22% ZJIT: Remove CheckInterrupts in empty inline frames
57 3038319762 1.069 423.1 +0.44% ZJIT: Assert with_write_ptr patches the half it is on
58 98db489047 1.065 424.6 -0.35% ZJIT: Allocate callee-saved registers to call-crossing VRegs
59 a0b2c116dc 1.070 422.5 +0.49% ZJIT: cut the cost of the GC mark hooks
60 a6f8eebde0 1.068 423.5 -0.24% ZJIT: Coalesce block params with the arguments feeding them
61 835fc874df 1.069 423.2 +0.07% ZJIT: Make the block-param coalescing analysis linear
62 8a07b6d824 1.066 424.3 -0.25% ZJIT: Remove the trivial inliner
63 98c2759e34 1.066 424.3 -0.01% ZJIT: Remove NoTracePoint patch points in empty inline frames
64 351b4a43d9 1.074 421.1 +0.76% ZJIT: Elide inline frames around leaf builtin calls
65 7f43b159a0 1.065 424.6 -0.83% ZJIT: Retry inlining annotated builtins after type inference
66 923562d978 1.066 424.1 +0.13% ZJIT: Grow the array instead of side-exiting on out-of-range Array#[]=
67 6feaaadc07 1.066 424.4 -0.09% ZJIT: Compute the coderange instead of side-exiting when it is unknown
68 e1af09605f 1.069 422.9 +0.36% ZJIT: Profile expandarray and compile one shape per site
69 5e770b926b 1.075 420.7 +0.52% ZJIT: Shrink side-exit stubs with a shared metadata table
70 1cf260c006 1.072 421.9 -0.27% ZJIT: Skip the bounds check on the side-exit metadata lookup
71 ead2736f02 1.072 422.0 -0.04% ZJIT: Stop each half of the code region at its own end
72 cc58e8d299 1.068 423.3 -0.29% ZJIT: Break down zjit_alloc_bytes by subsystem
73 a1f73b02f3 1.068 423.3 -0.02% ZJIT: Size operand type profiles exactly
74 47733ebcc3 1.073 421.5 +0.44% ZJIT: Box the polymorphic tail of a type distribution
75 fa4abd1a9e 1.075 420.8 +0.16% ZJIT: Let invalidated ISEQs recompile instead of exiting forever
76 de27701037 1.078 419.7 +0.27% ZJIT: Compile past polymorphic splatkw instead of side-exiting
@k0kubun 77 48aa01a071 1.094 413.4 +1.50% ZJIT: Recognize an unpassed block at unspecialized block-param sites
78 1c3b4c75fb 1.097 412.3 +0.26% ZJIT: Skip redundant cfp->sp stores
79 0322052e36 1.094 413.3 -0.23% ZJIT: Release an ISEQ's profile and GC offsets when the ISEQ is freed
80 c9837b1512 1.107 408.7 +1.11% ZJIT: Free the payload of a GC-freed ISEQ and account dead-version bytes
81 ea64f7c172 1.112 406.6 +0.52% ZJIT: Compile exception handler entries (jit_exception)
82 96044a5277 1.111 407.1 -0.13% ZJIT: Re-enter JIT code for frames unwound back into after a throw
83 5437742af7 1.112 406.5 +0.14% ZJIT: Box the rarely-used profile side tables
84 6a904973bd 1.109 407.9 -0.35% ZJIT: Don't break spilled parallel-copy cycles through SCRATCH_REG
85 67db018a67 1.107 408.6 -0.16% ZJIT: Renumber YJIT's instruction bindings for zjit_expandarray
86 57d93dfa65 1.104 409.6 -0.24% ZJIT: Test that spill elision keeps frame locals observable
@tekknolagi 87 3dafac937b 1.109 407.8 +0.42% ZJIT: Compile the once instruction
88 b3901111f7 1.110 407.5 +0.08% ZJIT: Update opt_tests snapshots for the empty-inline-frame elimination
@tekknolagi 89 3ce8cff3f5 1.106 409.0 -0.36% ZJIT: Rematerialize block params that can only hold one value
90 c0a46fbeef 1.104 409.5 -0.12% ZJIT: Inline a fast path for Array#push and Array#<<
@k0kubun 91 ebbc1f1955 1.108 408.3 +0.29% ZJIT: Profile ISEQs before function stub compilation
92 b0d4c2777b 1.111 407.0 +0.32% ZJIT: Initialize the block param local when a function stub exits
93 5a8b94a018 1.108 408.3 -0.33% ZJIT: Stop over-reserving in long-lived metadata vectors
94 211a75a1d7 1.101 410.6 -0.55% ZJIT: Keep the profiled shape reachable through guards and dispatch arms
95 43fd175d68 1.100 411.2 -0.15% ZJIT: Branch on the ivar shape for attr_accessor sends in polymorphic arms
96 a5a0375149 1.101 410.8 +0.09% ZJIT: Materialize frames before pushing a caught break value
97 512c9c3b1f 1.102 410.4 +0.10% ZJIT: Skip redundant local spill stores
98 be7034c5b9 1.102 410.2 +0.04% ZJIT: Pin the profiling metadata layout the memdiet merge depends on
99 debf00873e 1.102 410.3 -0.01% ZJIT: Drop the now-unreachable invokeblock arity-mismatch fallback reason
100 3dbd4428fa 1.096 412.7 -0.61% ZJIT: Dispatch exception handler entries over several continuations
b59ea8efc7 1.086 416.4 YJIT row — base commit run with --yjit (vs base ZJIT)
Details

Biggest wins in the new order

  • 6257de4477 +2.38% (52) ZJIT: Specialize bar(...) in standalone forwardable ISEQs
  • 48aa01a071 +1.50% (77) ZJIT: Recognize an unpassed block at unspecialized block-param sites
  • 7e2099dc2f +1.43% (34) ZJIT: Compile a yield to a Symbol block as the send it stands for
  • 6e60630c8c +1.38% (15) ZJIT: Allocate JITFrames below INT32_MAX
  • d217d309ae +1.32% (11) ZJIT: Specialize yield whose arity does not match the block
  • c9837b1512 +1.11% (80) ZJIT: Free the payload of a GC-freed ISEQ and account dead-version bytes
  • 5a292fe3dc +0.83% (25) ZJIT: Pass a &blk argument through to a direct send
  • 351b4a43d9 +0.76% (64) ZJIT: Elide inline frames around leaf builtin calls

Biggest local regressions

  • 5de9a21874 -1.34% (38) ZJIT: Let a frozen ivar dispatch earn back the shape it is missing
  • a3ef689fb5 -1.08% (33) ZJIT: Compile protected calls behind a check on the caller's self
  • 40c28541b7 -0.84% (21) ZJIT: Size megamorphic send tables to the shapes that need them
  • 7f43b159a0 -0.83% (65) ZJIT: Retry inlining annotated builtins after type inference
  • 823ec0dfe3 -0.81% (12) ZJIT: Lay the inlined block frame out on the reshaped yield arguments
  • 9b9dcd8675 -0.81% (14) ZJIT: Call megamorphic ISEQ targets without leaving JIT code

Read the per-commit column with care

Per-commit attribution correlates only 0.49 between this campaign and the pre-sort one, even though both are the same 100 patches against the same base. Two reasons:

  • Attribution genuinely moves when the order does. A change measured on top of nine other changes is now measured on top of three, and vice versa; interacting optimizations hand credit back and forth.
  • Single-pass noise. The base spread is 0.17%, but individual rows still swing. The largest disagreements between the two campaigns:
    • ZJIT: Compile protected calls behind a check on the caller's self — now -1.08%, was +0.66%
    • ZJIT: Compile a yield to a Symbol block as the send it stands for — now +1.43%, was -0.18%
    • ZJIT: Lay the inlined block frame out on the reshaped yield arguments — now -0.81%, was +0.43%
    • ZJIT: Drop the now-unreachable invokeblock may-throw fallback reason — now -0.73%, was +0.37%
    • ZJIT: Retry inlining annotated builtins after type inference — now -0.83%, was +0.18%

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions