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

List of patches

speedup is base ms / ms per itr from the same invocation. Δ is the change from the
previous row; + means this commit made lobsters faster.

Assignee # sha speedup ms/itr base ms Δ commit
066814022d 1.000 441.0 master before ruby#18636 (base, --zjit)
@k0kubun 1 76b177533e 1.001 440.6 440.9 ZJIT: Profile invalidated ISEQs and function stubs (ruby#18636)
@k0kubun 2 91db83cf22 1.002 440.5 441.5 +0.16% ZJIT: Wait a configured number of exits before invalidating an ISEQ
@k0kubun 3 b5c32853d6 1.011 435.5 440.3 +0.86% ZJIT: Always inline every block handler case for getblockparamproxy
@tekknolagi 4 5cd3e16bbf 1.015 434.3 441.0 +0.43% ZJIT: Fuse Test and fixnum compares into CondBranch
@tekknolagi 5 ddab20c8d3 1.025 429.8 440.6 +0.95% ZJIT: Fuse type tests into the branch that consumes them
6 4c733852ea 1.023 431.0 440.8 -0.23% ZJIT: Compile exception handlers (ruby#18759)
7 ca12f00c21 1.024 431.0 441.3 +0.12% ZJIT: Read the throwing frame's ISEQ through CFP_ISEQ() in vm_throw_start
@k0kubun 8 35996b7ba5 1.036 426.1 441.4 +1.16% ZJIT: Split cold code into an outlined code region
@tekknolagi 9 70203d64cc 1.037 425.2 441.1 +0.14% ZJIT: Specialize polymorphic sends with blocks (ruby#18567)
@tekknolagi 10 0c0b0ccf4e 1.046 422.1 441.4 +0.79% ZJIT: Fall back to a generic ivar access when a shape chain misses
@tekknolagi 11 421d05c0a2 1.047 421.4 441.2 +0.12% ZJIT: Back ivar shape-chain misses with a per-name shape table
12 9105450e49 1.051 419.9 441.2 +0.35% ZJIT: Cache the ivar location of class and module shapes
13 be2c0c3ac8 1.062 415.8 441.5 +1.05% ZJIT: Pass a &blk argument through to a direct send
14 f307e737da 1.057 417.2 440.8 -0.49% ZJIT: Compile a yield to a Symbol block as the send it stands for
15 75fd14733c 1.046 422.0 441.4 -1.01% ZJIT: Let a frozen ivar dispatch earn back the shape it is missing
16 3de96d3604 1.059 416.8 441.3 +1.21% ZJIT: Weigh an ivar respecialization by the traffic it would take off the fallback
17 d4f8303962 1.064 414.6 441.0 +0.46% ZJIT: Profile expandarray and compile one shape per site
18 91d3e3cddd 1.058 416.5 440.6 -0.55% ZJIT: Let invalidated ISEQs recompile instead of exiting forever
19 874a68f749 1.062 415.7 441.5 +0.40% ZJIT: Inline def foo(...) callees and compile the bar(...) inside them
20 7fbf7f0e09 1.061 415.7 441.2 -0.08% ZJIT: Compile past polymorphic splatkw instead of side-exiting
21 2d26d5bafa 1.063 415.1 441.2 +0.15% ZJIT: Specialize bar(...) in standalone forwardable ISEQs
22 c2f9ad7947 1.065 414.6 441.6 +0.21% ZJIT: Expose bucket counts on DistributionSummary
23 0549244c4e 1.069 413.3 441.9 +0.38% ZJIT: Guard the profiled receiver types of megamorphic call sites
24 f7f0e04a6a 1.061 415.7 441.0 -0.78% ZJIT: Dispatch megamorphic-by-class call sites on one ancestor guard
25 95e970ab5c 1.058 416.7 441.0 -0.25% ZJIT: Dispatch to blocks that only return with a direct JIT-to-JIT call
26 1a2305dc11 1.063 414.6 440.6 +0.41% ZJIT: Look megamorphic send targets up in a class table
27 234d901d50 1.059 416.0 440.7 -0.31% ZJIT: Fix invokeblock block-handler profiling and gate its ISEQ chain
28 e386eb9367 1.060 415.9 441.0 +0.09% ZJIT: Inline a block whose non-local return escapes to the frame we compile
29 2e004e33f3 1.061 416.0 441.5 +0.08% ZJIT: Destructure a lone yielded Array for the direct block dispatch
30 54f0503b88 1.062 414.9 440.6 +0.06% ZJIT: Destructure a lone yielded Array at polymorphic yield sites too
31 036864cea1 1.062 415.6 441.2 -0.03% ZJIT: Specialize yield whose arity does not match the block
32 cfa150ddf9 1.060 416.2 441.3 -0.12% ZJIT: Lay the inlined block frame out on the reshaped yield arguments
33 185d5eabd6 1.059 416.2 440.7 -0.13% ZJIT: Narrow test reg, imm to the immediate's width
34 7bbada3134 1.059 416.8 441.5 +0.04% ZJIT: Call megamorphic ISEQ targets without leaving JIT code
35 2aecabea46 1.060 416.1 441.0 +0.05% ZJIT: Compile direct sends to callees with a **rest parameter
36 b4e172f476 1.058 416.8 441.0 -0.16% ZJIT: Let a yield-unlocking iterator past the caller's inline budget
37 68a43cc289 1.072 411.4 440.9 +1.27% ZJIT: Compile send/send with a profiled method name
38 5e510e5c63 1.069 412.5 440.8 -0.29% ZJIT: Size megamorphic send tables to the shapes that need them
39 b1007b0b17 1.071 412.0 441.4 +0.26% ZJIT: Guard profiled splatarray operands instead of calling rb_vm_splat_array
40 93a9712b29 1.069 412.3 440.9 -0.19% ZJIT: Read nil for an out-of-bounds Array#[] instead of side-exiting
41 c928cc29a6 1.073 410.7 440.9 +0.38% ZJIT: Dispatch a yield to a block that can break directly
42 c96eac3688 1.076 410.2 441.2 +0.20% ZJIT: Only branch on the block param proxy for an ISEQ callee
43 ad7d7a19b7 1.072 411.2 440.9 -0.32% ZJIT: Pass a &blk argument through to a direct C call
44 05992a2874 1.077 409.4 441.1 +0.48% ZJIT: Compile direct sends to callees with an anonymous **
45 f7af91226d 1.072 411.6 441.4 -0.47% ZJIT: Inline callees that a &blk call site passes a block handler to
46 b93d6d06f6 1.068 412.9 440.8 -0.45% ZJIT: Stop side-exiting forever at unstable super sites
47 cbfb8584cb 1.073 410.5 440.5 +0.51% ZJIT: Dispatch polymorphic super on the frame's method entry
48 480e3fdcf8 1.075 410.4 441.3 +0.20% ZJIT: Compile protected calls behind a check on the caller's self
49 ad5054a1aa 1.076 409.5 440.8 +0.10% ZJIT: Drop the now-unreachable invokeblock may-throw fallback reason
50 8d83692390 1.080 408.8 441.6 +0.35% ZJIT: Pin the forwardable callee's fallback on a &blk call site
51 67022c671b 1.076 409.5 440.6 -0.40% ZJIT: Test the ivar reprofile budget inline
52 05449ab73e 1.075 410.0 440.9 -0.05% ZJIT: Hand the SP register back when returning past inlined frames
53 b0151512b6 1.074 411.0 441.4 -0.13% ZJIT: Let a getivar site's profile forget boot-time shapes
54 91d2b5139d 1.076 410.3 441.3 +0.15% ZJIT: Test profile eviction and cached class ivars
55 805b7d7321 1.074 410.8 441.1 -0.17% ZJIT: Size ivar shape tables to the names that need them
56 098a399a61 1.073 411.3 441.2 -0.10% ZJIT: Dispatch yield to a run-time block ISEQ without leaving JIT code
57 7ad26cc3c8 1.072 410.8 440.3 -0.08% ZJIT: Dispatch a Symbol block's send through the megamorphic class table
58 fe932fdb94 1.075 410.6 441.3 +0.28% ZJIT: Remove CheckInterrupts in empty inline frames
59 4bb2ec5aec 1.073 410.9 440.8 -0.19% ZJIT: Assert with_write_ptr patches the half it is on
60 b2e41cbb27 1.074 410.6 440.9 +0.09% ZJIT: Allocate callee-saved registers to call-crossing VRegs
61 bdbee0ee60 1.084 407.5 441.7 +0.93% ZJIT: cut the cost of the GC mark hooks
62 70593e75e0 1.082 407.0 440.3 -0.19% ZJIT: Coalesce block params with the arguments feeding them
63 d1e33bdad2 1.080 408.6 441.3 -0.17% ZJIT: Make the block-param coalescing analysis linear
64 26f3f4f5c6 1.079 408.1 440.4 -0.08% ZJIT: Remove the trivial inliner
65 c4129b2a6b 1.076 410.1 441.2 -0.31% ZJIT: Remove NoTracePoint patch points in empty inline frames
66 e3eca3a258 1.077 409.4 440.9 +0.10% ZJIT: Elide inline frames around leaf builtin calls
67 71e7b305d2 1.083 407.4 441.4 +0.61% ZJIT: Retry inlining annotated builtins after type inference
68 311953991a 1.077 409.2 440.8 -0.58% ZJIT: Grow the array instead of side-exiting on out-of-range Array#[]=
69 1e4e643ca0 1.080 408.1 440.8 +0.27% ZJIT: Compute the coderange instead of side-exiting when it is unknown
70 c99b78293b 1.079 408.7 440.9 -0.12% ZJIT: Shrink side-exit stubs with a shared metadata table
71 18e29fe82e 1.082 407.8 441.4 +0.33% ZJIT: Skip the bounds check on the side-exit metadata lookup
72 f052b50585 1.082 407.8 441.2 -0.05% ZJIT: Stop each half of the code region at its own end
73 b2d270d7e9 1.080 407.5 440.2 -0.16% ZJIT: Break down zjit_alloc_bytes by subsystem
74 fef8304c3c 1.086 406.1 440.9 +0.51% ZJIT: Size operand type profiles exactly
75 058aa08487 1.081 408.0 441.2 -0.40% ZJIT: Box the polymorphic tail of a type distribution
76 0916e4784a 1.081 407.5 440.5 -0.04% ZJIT: Skip redundant cfp->sp stores
77 01ed8a350a 1.077 408.8 440.4 -0.34% ZJIT: Release an ISEQ's profile and GC offsets when the ISEQ is freed
78 1a6a886620 1.088 405.1 440.8 +0.99% ZJIT: Free the payload of a GC-freed ISEQ and account dead-version bytes
79 4d2f98f6cf 1.092 403.5 440.6 +0.35% ZJIT: Box the rarely-used profile side tables
80 488b48d0db 1.092 404.0 441.0 -0.03% ZJIT: Don't break spilled parallel-copy cycles through SCRATCH_REG
81 e194a2d75a 1.096 402.8 441.4 +0.38% ZJIT: Test that spill elision keeps frame locals observable
82 4d692e2f60 1.089 404.6 440.8 -0.58% ZJIT: Inline a fast path for Array#push and Array#<<
83 1f0f64ec01 1.088 405.0 440.8 -0.10% ZJIT: Initialize the block param local when a function stub exits
84 a7c79202c0 1.090 404.4 440.9 +0.17% ZJIT: Stop over-reserving in long-lived metadata vectors
85 1ee1ff0661 1.085 406.1 440.6 -0.49% ZJIT: Keep the profiled shape reachable through guards and dispatch arms
86 82ac8165f1 1.084 406.5 440.8 -0.06% ZJIT: Branch on the ivar shape for attr_accessor sends in polymorphic arms
87 28e750cff8 1.091 404.5 441.2 +0.58% ZJIT: Materialize frames before pushing a caught break value
88 f5eb199f23 1.086 405.8 440.8 -0.41% ZJIT: Skip redundant local spill stores
89 cd38d87122 1.089 405.1 441.2 +0.27% ZJIT: Pin the profiling metadata layout the memdiet merge depends on
90 3124ff46b7 1.089 405.1 441.3 +0.03% ZJIT: Drop the now-unreachable invokeblock arity-mismatch fallback reason
91 34ab69dd2d 1.090 404.8 441.1 +0.03% ZJIT: Drop the splat-expansion path superseded by caller-splat dispatch
066814022d 1.071 411.7 441.0 YJIT at the base commit (--yjit)
Details

Summary

  • Stack so far: 441.0 → 404.8 ms/itr, 1.090× over the base (through row 91).
  • ZJIT first drops below the YJIT reference (411.7 ms) at row 37 (68a43cc289).

Largest single-commit gains

  • +1.27%, row 37, 68a43cc289, ZJIT: Compile send/send with a profiled method name
  • +1.21%, row 16, 3de96d3604, ZJIT: Weigh an ivar respecialization by the traffic it would take off the fallback
  • +1.16%, row 8, 35996b7ba5, ZJIT: Split cold code into an outlined code region
  • +1.05%, row 13, be2c0c3ac8, ZJIT: Pass a &blk argument through to a direct send
  • +0.99%, row 78, 1a6a886620, ZJIT: Free the payload of a GC-freed ISEQ and account dead-version bytes
  • +0.95%, row 5, ddab20c8d3, ZJIT: Fuse type tests into the branch that consumes them
  • +0.93%, row 61, bdbee0ee60, ZJIT: cut the cost of the GC mark hooks
  • +0.86%, row 3, b5c32853d6, ZJIT: Always inline every block handler case for getblockparamproxy
  • +0.79%, row 10, 0c0b0ccf4e, ZJIT: Fall back to a generic ivar access when a shape chain misses
  • +0.61%, row 67, 71e7b305d2, ZJIT: Retry inlining annotated builtins after type inference

Largest single-commit regressions

  • -1.01%, row 15, 75fd14733c, ZJIT: Let a frozen ivar dispatch earn back the shape it is missing
  • -0.78%, row 24, f7f0e04a6a, ZJIT: Dispatch megamorphic-by-class call sites on one ancestor guard
  • -0.58%, row 68, 311953991a, ZJIT: Grow the array instead of side-exiting on out-of-range Array#[]=
  • -0.58%, row 82, 4d692e2f60, ZJIT: Inline a fast path for Array#push and Array#<<
  • -0.55%, row 18, 91d3e3cddd, ZJIT: Let invalidated ISEQs recompile instead of exiting forever
  • -0.49%, row 14, f307e737da, ZJIT: Compile a yield to a Symbol block as the send it stands for
  • -0.49%, row 85, 1ee1ff0661, ZJIT: Keep the profiled shape reachable through guards and dispatch arms
  • -0.47%, row 45, f7af91226d, ZJIT: Inline callees that a &blk call site passes a block handler to

Caveats

  • Base spread 1.7 ms (0.39%) across 91 re-measurements; treat single-row Δ under ~±0.5% as noise.
  • The stack was reordered (upstreaming candidates first); a commit's Δ is its effect at its new position.

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