go-ruby-pathname/pathname is the pure-Go library that
rbgo binds for Ruby's Pathname path
algebra. This page records the methodology for the comparative benchmark of that
module against the reference Ruby runtimes — part of the ecosystem-wide per-module parity
suite. No numbers are quoted here until they are measured on a pinned host and committed
to the benchmark harness.
What is measured
The same Ruby script — a representative mix of Pathname lexical operations
(cleanpath, relative_path_from, basename/dirname/extname, join/split,
ascend) over a corpus of paths — is run under every runtime. rbgo's number reflects
this pure-Go library doing the work; every other column is that interpreter's own
pathname stdlib. So the comparison is the Ruby-visible operation, apples-to-apples
across interpreters. The script prints a deterministic checksum and its output is checked
byte-identical to MRI before any timing is taken.
Method
- Best-of-N wall time (best, not mean, to suppress scheduler noise); single-shot
processes, no warm-up beyond the script's own loop.
- Runtimes: MRI (the oracle) and MRI
--yjit; JRuby (on OpenJDK); TruffleRuby
(GraalVM CE Native) — each timed cold, single-shot, so JVM / Graal startup is on every
run; read those as one-shot ruby file.rb costs, not steady-state JIT numbers.
- The benchmark script and harness live in rbgo's repo under
bench/modules/.
Reproduce with RBGO=./rbgo bash bench/modules/run.sh N.
Result (best of 5, ms)
| Runtime |
time |
vs MRI |
| rbgo (go-ruby-pathname) |
260 |
0.59× |
| MRI (ruby 4.0.5) |
440 |
1.00× |
| MRI + YJIT |
420 |
0.95× |
| JRuby 10.1.0.0 |
1890 |
4.30× |
| TruffleRuby 34.0.1 |
740 |
1.68× |
rbgo runs on go-ruby-pathname and is faster than MRI here (0.59x) on this lexical path-manipulation workload (join/cleanpath/relative_path_from/split, no filesystem I/O).
Honest framing
JRuby and TruffleRuby are timed cold, single-shot, so they carry JVM /
Graal startup on every run — read them as one-shot ruby file.rb costs, the
same way rbgo and MRI are measured, not as steady-state JIT numbers. Rows
that complete in well under ~200 ms carry the most relative noise; treat
their ratios as order-of-magnitude. These are real measured numbers from
the 2026-06-30 run (Apple M-series; ruby 4.0.5 +PRISM, jruby 10.1.0.0,
truffleruby 34.0.1) — nothing is fabricated or cherry-picked.
Library-level benchmark (Go API vs runtimes) — 2026-07-03
This section measures the pure-Go library directly, through its Go API — not
the rbgo interpreter path recorded above. It isolates the library primitive
from Ruby-interpreter dispatch, answering the parity question head-on: is the
pure-Go implementation as fast as the reference runtime's own Pathname? The
same workload, same inputs, same iteration counts run through the Go library
and through each reference runtime's stdlib; the Go driver's output was checked
byte-identical to MRI (the resulting path strings for every op) before any
timing.
- Host: Apple M4 Max (
Mac16,5, arm64), macOS 26.5 — date 2026-07-03.
All runtimes ran natively on the host (no VM).
- Runtimes: Go 1.26.4 · MRI
ruby 4.0.5 +PRISM · MRI + YJIT · JRuby 10.1.0.0
(OpenJDK 25) · TruffleRuby 34.0.1 (GraalVM CE Native).
- Ops: all pure-path (lexical) — no filesystem access, so the workload is
deterministic and reproducible:
Pathname#join, #+ (plus), #basename,
#dirname, #extname, #cleanpath, #relative_path_from, #split, and
#each_filename. Ruby's Pathname is pure-Ruby stdlib wrapping File path
methods, so parity is algorithmically reachable — and, being compiled, the
pure-Go library realizes it with room to spare. No op was unmeasurable: every
one of the nine has a direct, output-equal Go counterpart.
- Method: each process runs 3 untimed warm-up passes, then 25 timed passes of
a fixed inner loop (1000 ops each), timed with a monotonic clock; the best
pass is reported as ns/op (lower is better).
vs MRI < 1.00× means
faster than MRI. Interpreter start-up is outside the timed region, so these
are operation costs, not ruby file.rb process costs. Harness + workload:
benchmarks/
(go/ pins the published library by pseudo-version; ruby/pathname.rb);
reproduce with bash benchmarks/run.sh.
join
| Runtime |
ns/op |
vs MRI |
| go-ruby (pure Go) |
112.1 |
0.01× |
| MRI |
11018.0 |
1.00× |
| MRI + YJIT |
10161.0 |
0.92× |
| JRuby |
8448.9 |
0.77× |
| TruffleRuby |
6322.3 |
0.57× |
plus
| Runtime |
ns/op |
vs MRI |
| go-ruby (pure Go) |
25.9 |
0.01× |
| MRI |
2822.0 |
1.00× |
| MRI + YJIT |
2688.0 |
0.95× |
| JRuby |
2011.2 |
0.71× |
| TruffleRuby |
1524.3 |
0.54× |
basename
| Runtime |
ns/op |
vs MRI |
| go-ruby (pure Go) |
84.7 |
0.25× |
| MRI |
336.0 |
1.00× |
| MRI + YJIT |
277.0 |
0.82× |
| JRuby |
210.6 |
0.63× |
| TruffleRuby |
442.7 |
1.32× |
dirname
| Runtime |
ns/op |
vs MRI |
| go-ruby (pure Go) |
14.3 |
0.05× |
| MRI |
298.0 |
1.00× |
| MRI + YJIT |
227.0 |
0.76× |
| JRuby |
265.7 |
0.89× |
| TruffleRuby |
1973.8 |
6.62× |
extname
| Runtime |
ns/op |
vs MRI |
| go-ruby (pure Go) |
105.9 |
0.52× |
| MRI |
203.0 |
1.00× |
| MRI + YJIT |
163.0 |
0.80× |
| JRuby |
313.0 |
1.54× |
| TruffleRuby |
493.3 |
2.43× |
cleanpath
| Runtime |
ns/op |
vs MRI |
| go-ruby (pure Go) |
250.0 |
0.04× |
| MRI |
6044.0 |
1.00× |
| MRI + YJIT |
6058.0 |
1.00× |
| JRuby |
4995.4 |
0.83× |
| TruffleRuby |
3353.3 |
0.55× |
relative_path_from
| Runtime |
ns/op |
vs MRI |
| go-ruby (pure Go) |
578.2 |
0.05× |
| MRI |
12150.0 |
1.00× |
| MRI + YJIT |
11913.0 |
0.98× |
| JRuby |
9125.1 |
0.75× |
| TruffleRuby |
3648.9 |
0.30× |
split
| Runtime |
ns/op |
vs MRI |
| go-ruby (pure Go) |
132.5 |
0.19× |
| MRI |
703.0 |
1.00× |
| MRI + YJIT |
544.0 |
0.77× |
| JRuby |
389.7 |
0.55× |
| TruffleRuby |
881.9 |
1.25× |
each_filename
| Runtime |
ns/op |
vs MRI |
| go-ruby (pure Go) |
166.1 |
0.06× |
| MRI |
2885.0 |
1.00× |
| MRI + YJIT |
2711.0 |
0.94× |
| JRuby |
2173.6 |
0.75× |
| TruffleRuby |
4788.2 |
1.66× |
The pure-Go library is faster than MRI on every one of the nine ops. The
widest margins are on the allocation-heavy path builders — join and +
(~100× and ~110× faster than MRI, since MRI's Pathname#join chains #+, each
allocating a fresh Pathname and string) and cleanpath /
relative_path_from (~24× and ~21×, which split, normalise and re-join). The
narrowest are the already-tiny single-scan ops extname (~1.9×) and basename
(~4×), where MRI has little Ruby overhead to shed. MRI's Pathname is pure-Ruby
stdlib, so each op pays interpreter dispatch the compiled Go code does not.
Parity is not just reached, it is beaten across the board. Outputs were checked
byte-identical to MRI before timing, so the speed is not bought with divergent
behavior.
Cold-JIT caveat
JRuby (JVM) and TruffleRuby (GraalVM) get only the harness's 3 warm-up
passes, so they are effectively cold — TruffleRuby's dirname row
(1973.8 ns, 6.62×) and extname row (2.43×) are dominated by Graal
compilation kicking in on just-touched paths, not steady-state throughput;
on the heavier relative_path_from/cleanpath/join ops, where it has more
work to amortise over, TruffleRuby is already the fastest Ruby column. JRuby
is closer to warm but still not fully JITed. Read the JVM/Graal columns as
cold single-shot costs, the same footing as the top-of-page table. These are
real measured numbers from the 2026-07-03 run (Apple M4 Max, arm64;
ruby 4.0.5 +PRISM, jruby 10.1.0.0, truffleruby 34.0.1) — nothing is
fabricated or cherry-picked.