Skip to content

Bench 4.0.0 alongside master in the version throughput benchmark - #2914

Merged
ericproulx merged 1 commit into
masterfrom
chore/bench-4-0-0-default
Sep 7, 2026
Merged

ericproulx merged 1 commit into
masterfrom
chore/bench-4-0-0-default

Conversation

@ericproulx

@ericproulx ericproulx commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds 4.0.0 to DEFAULT_VERSIONS in benchmark/version_throughput/run.rb, between 3.3.5 and master.
  • Regenerates RESULTS.md on the same Ruby 4.0.6 as the previous run.
  • Updates the benchmark README: the usage example follows the new default list, and "Interpreting results" gains a bullet on reading the master row just after a release.

4.0.0 shipped today, so the last released data point in the table was 3.3.5 and every master delta was measured against a release two months and one major behind it. Splitting the row separates what the major delivered from what has landed on master since it was cut.

Benchmarks

Version No-YJIT (i/s) vs prev vs 3.0.1 YJIT (i/s) vs prev vs 3.0.1
3.0.1 34,255 54,997
3.1.1 43,711 +27.6% +27.6% 84,743 +54.1% +54.1%
3.2.1 45,470 +4.0% +32.7% 89,697 +5.8% +63.1%
3.3.5 67,602 +48.7% +97.3% 135,813 +51.4% +146.9%
4.0.0 123,107 +82.1% +259.4% 233,841 +72.2% +325.2%
master 122,134 -0.8% +256.5% 235,134 +0.6% +327.5%

The last two rows bench the same code: master differs from v4.0.0 only in lib/grape/version.rb (the 4.1.0 development bump). Their -0.8% / +0.6% spread is therefore a measurement of the benchmark's own noise floor, not drift — a useful calibration of the README's stated ~5-8% figure, and the reason the new README bullet spells the situation out. The pre-existing rows likewise move only by run-to-run noise against the previous RESULTS.md.

Test plan

  • ruby benchmark/version_throughput/run.rb on Ruby 4.0.6 (arm64-darwin25, YJIT available) — output is the committed RESULTS.md.
  • Benchmark-only change; no lib/ or spec surface touched.
  • CHANGELOG entry added under 4.1.0 Features.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

Danger Report

No issues found.

View run

@ericproulx
ericproulx marked this pull request as ready for review September 7, 2026 16:27
@ericproulx
ericproulx force-pushed the chore/bench-4-0-0-default branch from 7a31162 to b58d160 Compare September 7, 2026 16:29
4.0.0 shipped today, so the version benchmark's last released data point was
3.3.5 and every `master` delta was measured against a release two months and
one major behind it. With 4.0.0 in DEFAULT_VERSIONS the table separates what
the major actually delivered (+82.1% no-YJIT, +72.2% YJIT over 3.3.5) from
what has landed on master since it was cut.

Right now that is nothing: master differs from v4.0.0 only in
lib/grape/version.rb, so the two rows bench identical code and their -0.8% /
+0.6% spread is a direct read of the benchmark's own noise floor. README.md
gains a bullet under "Interpreting results" saying so, since that reading
recurs every time the benchmark is run just after a release.

RESULTS.md is regenerated on the same Ruby 4.0.6 as the previous run, so the
existing rows move only by run-to-run noise. README.md's usage example follows
the new default list.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@ericproulx
ericproulx force-pushed the chore/bench-4-0-0-default branch from b58d160 to 5863315 Compare September 7, 2026 16:30
@ericproulx
ericproulx merged commit 5ae81e2 into master Sep 7, 2026
70 checks passed
@ericproulx
ericproulx deleted the chore/bench-4-0-0-default branch September 7, 2026 16:46
ericproulx added a commit that referenced this pull request Sep 7, 2026
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 added a commit that referenced this pull request Sep 7, 2026
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 added a commit that referenced this pull request Sep 7, 2026
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>
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.

1 participant