Skip to content

Give the module benchmarks a clock that can see them, and say where each module runs - #748

Merged
tannevaled merged 2 commits into
mainfrom
bench/a-clock-that-can-see-the-rows
Oct 1, 2026
Merged

tannevaled merged 2 commits into
mainfrom
bench/a-clock-that-can-see-the-rows

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Closes the first two parts of #747 and gives the third one a guard.

The clock

bench/modules/run.sh timed with /usr/bin/time -p, which prints two decimals
of a second
:

$ for i in $(seq 10); do { /usr/bin/time -p /usr/bin/true >/dev/null; } 2>&1 | awk '/real/{printf "%s ",$2}'; done
0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

10 ms resolution, and anything under one tick reads zero. Of the 171 timing
cells in the three per-module sections of BENCHMARKS.md, all 171 are
multiples of 10 ms
; 36 are ten ticks or fewer.

That is where the bolded rows lived. matrix | 20ms | 50ms | 0.40× was a
two-tick measurement. tsort | 80ms | 90ms | 0.89× claimed an 11% win over
MRI from a difference of one tick — re-taken, it is 1.42×, a loss.

Three changes to the harness

  • Time::HiRes instead of /usr/bin/time -p, so the figures carry the
    precision the ratios were already quoted to.
  • A run that exits non-zero is an error, not a fast run. best() kept the
    smallest time of N without looking at the exit status, so a command that died
    immediately won its own series and got published. It now marks that arm and
    carries on with the others.
  • The runtimes are interleaved, one run each in rotation with the order
    rotating every round, instead of each taking its own block of wall-clock time.
    A machine whose load moves during a block charges the movement to whichever
    runtime was running, and nothing in the output says so. See Compare implementations, not moments: the Fugue row was 14% too high go-crdt/crdt#131:
    the same defect put a published ratio 14% out, and contention does not fall on
    every arm alike — it falls hardest on the shortest-running one, which is
    the fastest one.

The table, re-taken

A new dated section; the June ones are kept as they were taken. All 35 modules
passed the byte-identical-to-MRI gate under every installed runtime, no row was
dropped and no run exited non-zero.

MRI is the control. Across the 24 rows the two runs share it reproduces
within a few percent — 220→215 ms on set, 1450→1546 on did-you-mean,
570→567 on scanf, 330→329 on securerandom — so the machine and the workloads
are the same, and what moved in the rbgo column is rbgo's.

Five of those 24 rows change which side of parity they sit on:

June now
cmath 0.67× 1.20× a win that is a loss
prettyprint 0.89× 1.42× a win that is a loss
tsort 0.89× 1.42× a win that is a loss
prime 1.67× 0.94× a loss that is a win
ipaddr 1.91× 0.54× a loss that is a win

Three of the eight modules the June text lists as "rbgo beats MRI outright" are
not beating MRI, and all three sat on one or two ticks.

Other rows moved because the code did, which is the larger part: strscan
10720 → 262 ms and json 1520 → 701 ms are #89 and #88 landing the day after
those tables were taken. regexp 1530 → 4968 ms moved that far the other way.
Three months separate the runs; the new section does not try to attribute each
row, only to say which comparisons the old clock could not support.

set, and the guard

set reads 38.2× where June published 10.09×, and that one is not the clock
either. #56 bound Set to go-ruby-set/set on 2026-06-29; #330 reimplemented it
here on 2026-08-08
for MRI 4.0 conformance, with the bulk of the Ruby-observable
API in the prelude; #333's go mod tidy dropped the now-unused require the next
day. The benchmark exercises exactly that algebra, so it now measures prelude Ruby
where it used to measure Go.

Nothing about that trade was wrong — conformance was the point. What was missing
was anything that would say it had happened, and a page in another org went on
attributing this number to a library we had stopped using, for seven weeks, with
a figure that had moved 3.7× underneath it.

bench/modules/BACKING.md now records which modules run on an external
go-ruby-<mod> library and which run on code in this repository. It is generated,
and TestBenchmarkedModulesSayWhereTheirCodeLives fails when it and go.mod
disagree — I checked that it does, both by flipping one row back to
github.com/go-ruby-set/set (it names the line) and by deleting the file. Today
complex, rational and set are in-tree.

The four documentation pages in the other orgs are untouched; #747 records which
they are.

🤖 Generated with Claude Code

tannevaled and others added 2 commits September 30, 2026 10:01
…ach runs

`bench/modules/run.sh` timed with `/usr/bin/time -p`, which prints two decimals
of a second. Its resolution is 10 ms, and anything faster than one tick reads
zero:

    $ { /usr/bin/time -p /usr/bin/true >/dev/null; } 2>&1 | awk '/real/{print $2}'
    0.00

Of the 171 timing cells in the three per-module sections of BENCHMARKS.md, all
171 are multiples of 10 ms; 36 are ten ticks or fewer and 11 are five or fewer.
`matrix | 20ms | 50ms | 0.40x` was a two-tick measurement. `tsort | 80ms | 90ms
| 0.89x` claimed an 11% win over MRI from a difference of one tick — re-taken,
it is 1.42x, a loss.

Three changes to the harness:

  Time::HiRes instead of /usr/bin/time -p, so the figures have the precision the
  ratios were already being quoted to.

  A run that exits non-zero is an error, not a fast run. `best()` kept the
  smallest time of N without ever looking at the exit status, so a command that
  died immediately won its own series and got published. It now marks that arm
  and carries on with the others rather than timing a failure.

  The runtimes are interleaved, one run each in rotation with the order rotating
  every round, instead of each taking its own block of wall-clock time. A machine
  whose load moves during a block charges the movement to whichever runtime was
  running, and nothing in the output says so. See go-crdt/crdt#131: the same
  defect put a published ratio 14% out, and contention does not fall on every arm
  alike — it falls hardest on the shortest-running one, which is the fastest.

The table is re-taken in a new dated section; the June ones are kept as they were
taken. MRI is the control and reproduces within a few percent across all 24 rows
the two runs share, so what moved in the rbgo column is rbgo's. Five of those 24
change which side of parity they sit on: cmath, prettyprint and tsort were
published as wins over MRI and are losses; prime and ipaddr were losses and are
wins.

`set` reads 38.2x where June published 10.09x, and that one is not the clock.
#56 bound Set to go-ruby-set/set on 2026-06-29; #330 reimplemented it here on
2026-08-08 for MRI 4.0 conformance, with the bulk of the Ruby-observable API in
the prelude; #333's `go mod tidy` dropped the now-unused require the next day.
The benchmark exercises exactly that algebra, so it measures prelude Ruby where
it used to measure Go. Nothing about the trade was wrong. What was missing was
anything that would say it had happened — a page in another org went on
attributing this number to a library we had stopped using.

So: bench/modules/BACKING.md now records, beside the scripts, which modules run
on an external go-ruby library and which run on code in this repository. It is
generated, and TestBenchmarkedModulesSayWhereTheirCodeLives fails when it and
go.mod disagree. complex, rational and set are in-tree today.

Refs #747.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
git hands BACKING.md to a Windows checkout with CRLF endings, and the test
compared bytes, so the first CI run that saw it reported the file as no longer
matching go.mod — over carriage returns. A guard about which module runs where
must not depend on how the working tree was written.

Compared with carriage returns dropped from both sides. Checked on a CRLF copy
that the correct table passes and that flipping `set` back to
github.com/go-ruby-set/set still fails and still names the line: the point was to
stop it failing for the wrong reason, not to stop it failing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tannevaled added a commit to go-ruby-set/docs that referenced this pull request Sep 30, 2026
Three pages here state in the present tense that `rbgo` binds this module. It
did, from rbgo#56 on 2026-06-29 until rbgo#330 on 2026-08-08, which
reimplemented `Set` inside the interpreter for MRI 4.0 conformance with the bulk
of the Ruby-observable API in its prelude. rbgo#333 dropped the now-unused
requirement the next day. `github.com/go-ruby-set/set` has not been in rbgo's
build list since.

That makes the rbgo row on the performance page a historical figure rather than a
measurement of this library. It also moved underneath the claim: re-taken on
2026-09-30 with a clock finer than the 10 ms one that produced it, that row reads
8212 ms and 38.2x rather than 2220 ms and 10.09x — because it now times prelude
Ruby rather than this library. MRI reproduces at 215 ms against the published
220, which is what says the machine is not the explanation.

The library-level benchmark further down the page is untouched and unaffected: it
drives this library through its own Go API and never went through rbgo.

Refs go-embedded-ruby/ruby#747, go-embedded-ruby/ruby#748.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 17ba54b into main Oct 1, 2026
9 checks passed
@tannevaled
tannevaled deleted the bench/a-clock-that-can-see-the-rows branch October 1, 2026 13:35
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