Bench ZJIT alongside YJIT in the version benchmark - #2913
Merged
Merged
Conversation
Danger ReportNo issues found. |
ericproulx
force-pushed
the
chore/bench-zjit
branch
2 times, most recently
from
September 7, 2026 16:54
23ba6d4 to
9242bc5
Compare
run.rb was structured around a `with_yjit` boolean: one pass without a
JIT, one with `--yjit`, and a report shape chosen by that flag. It now
walks a list of JIT modes -- interpreter, `--yjit`, `--zjit` -- so each
gets its own throughput, both delta columns and a speedup against the
interpreter on the same row. Ruby refuses to boot with both JIT flags
("Only one JIT can be enabled at the same time"), so they cannot share a
pass. With no JIT available the report falls back to the compact layout
it always used.
Availability is now probed by booting `ruby <flag>` and asking the JIT
whether it is `enabled?`. The old probe only checked that RubyVM::YJIT
was defined, which is true on any build that supports YJIT whether or
not the flag was given -- it would have reported YJIT available on a
build where `--yjit` did nothing, and there is no way to write the ZJIT
equivalent, since RubyVM::ZJIT is defined under `--yjit` too.
bench.rb reports which JIT actually ran rather than a yjit on/off flag,
and a pass whose JIT does not match the one asked for is recorded as an
error instead of being published as a column quietly repeating the
interpreter's number.
RESULTS.md regenerated on Ruby 4.0.6, arm64-darwin25, now including the
4.0.0 row that landed in #2914. ZJIT lands between the interpreter and
YJIT throughout: on master, 145,129 i/s against 121,325 without a JIT
and 225,823 with YJIT. Since that ordering is the first question the
table raises, README.md records where the gap comes from -- ~30
uninlined C method calls per request, each syncing interpreter state
first -- along with what was ruled out (warmup, compilation,
deoptimisation, Grape itself) and the --zjit-stats command that
reproduces the counters.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ericproulx
force-pushed
the
chore/bench-zjit
branch
from
September 7, 2026 17:03
9242bc5 to
d851b2c
Compare
Member
|
Impressive improvements! Since YJIT speedup is bigger than ZGIT probably swapping the columns in the report would make it more readable. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
run.rbwas structured around awith_yjitboolean: one pass without a JIT, one with--yjit, and a report shape chosen by that flag. It now walks a list of JIT modes — interpreter,--yjit,--zjit— so each gets its own throughput, both delta columns, and a speedup against the interpreter on the same row. Ruby refuses to boot with both JIT flags (Only one JIT can be enabled at the same time. Exiting), so they cannot share a pass. With no JIT available the report falls back to the compact layout it always used.Availability probe
The old probe was
ruby --yjit -e 'exit(defined?(RubyVM::YJIT) ? 0 : 1)'. That constant is defined on any build supporting YJIT whether or not the flag was given, so it would have reported YJIT available on a build where--yjitdid nothing — and there is no way to write the ZJIT equivalent, sinceRubyVM::ZJITis defined under--yjittoo:It now boots
ruby <flag>and asks the JIT whether it isenabled?.bench.rbadditionally reports which JIT actually ran instead of a yjit on/off flag, and a pass whose JIT does not match the one asked for is recorded as an error rather than published as a column quietly repeating the interpreter's number.Results
Ruby 4.0.6, arm64-darwin25. ZJIT lands between the interpreter and YJIT throughout.
Rebased onto master, so the table picks up the 4.0.0 row added by #2914. Those last two rows bench the same code — master differs from
v4.0.0only inlib/grape/version.rb— which makes their spread a free reading of each pass's noise floor: -0.9% interpreter, -1.0% YJIT, -5.3% ZJIT. Read the ZJIT column with that in mind; it is the noisiest of the three.Full table with
μs/reqand both delta columns per pass is inRESULTS.md.Why ZJIT trails YJIT
That ordering is the first question the table raises, so
README.mdgains a section recording the answer rather than leaving it to be re-derived. Ruled out on Ruby 4.0.6: warmup (throughput flat from a 2s to a 20s warmup; both JITs trigger at 30 calls), compilation (compiled_iseq_count: 306,failed_iseq_count: 0), deoptimisation (guard_type_exit_ratio: 0.0%,guard_shape_exit_ratio: 2.7%, ~4 side exits per request), and Grape itself (a non-Grape JSON control shows the same ordering).What
--zjit-statsdoes show is ~30 uninlined C method calls per request —String#include?,Array#any?,Array#to_a,Array#include?,Regexp#match,Kernel#dup,Hash#fetch,String#to_sym— the set YJIT emits inline. Only ~36% of sends take an inlined cfunc path, and each uninlined one syncs interpreter state first:vm_write_pc_countandvm_write_sp_countboth land near 167 per request. The section carries the--zjit-statscommand that reproduces those counters, and is dated to the Ruby it was measured on.Test plan
falsefor an unsupported flag rather than raising.--zjit-statscommand documented in the README runs verbatim from the repo root and reproduces every counter cited.ruby -cclean on both scripts (benchmark/**/*is excluded from RuboCop).CHANGELOG entry added under 4.1.0 Features, matching #2914.
🤖 Generated with Claude Code