Skip to content

Performance

go-ruby-scanf/scanf is the pure-Go library that rbgo binds for Ruby's scanf. This page records the methodology for a comparative benchmark of that module against the reference Ruby runtimes, part of the ecosystem-wide per-module parity suite.

What is measured

The same Ruby script — a String#scanf parse of a representative formatted-data string — is run under every runtime. rbgo's number reflects this pure-Go library doing the work; every other column is that interpreter's own 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 timing.

  • Method: best-of-5 wall time (best, not mean, to suppress scheduler noise); single-shot processes, no warm-up beyond the script's own loop.
  • Runtimes: ruby (MRI, the oracle) and ruby --yjit; jruby (OpenJDK); truffleruby (GraalVM CE Native).
  • The benchmark script and harness live in rbgo's repo under bench/modules/ (scanf.rb + run.sh). Reproduce: RBGO=./rbgo TRUFFLE=truffleruby bash bench/modules/run.sh 5.

Result

Result (best of 5, ms)

Runtime time vs MRI
rbgo (go-ruby-scanf) 150 0.26×
MRI (ruby 4.0.5) 570 1.00×
MRI + YJIT 550 0.96×
JRuby 10.1.0.0 n/a*
TruffleRuby 34.0.1 n/a*

* scanf is not bundled in JRuby 10.1 or TruffleRuby 34 (both LoadError on require — it was removed as a default gem in those distributions), so no JRuby/TruffleRuby number could be measured. The row runs on MRI, MRI+YJIT and rbgo.

rbgo runs on go-ruby-scanf and is ~4x faster than MRI here (0.26x): MRI's scanf format engine is Ruby-coded. scanf is not bundled in JRuby 10.1 or TruffleRuby 34 (both LoadError on require — it was removed as a default gem), so those two columns have no number.

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 and answers the parity question head-on: is the pure-Go implementation as fast as the reference runtime's own scanf? Because MRI's scanf is a pure-Ruby stdlib (the format engine is Ruby-coded), parity is reachable — and, as the tables show, beaten. The same workload, same inputs, same iteration counts run through the Go library and through each reference runtime's scanf; outputs were checked identical to MRI before any timing.

  • Host: Apple M4 Max (Mac16,5, arm64), macOS 26.5.1 — date 2026-07-03.
  • 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). scanf (v1.0.0) is a default gem JRuby 10.1 and TruffleRuby 34 no longer bundle; it was restored on both with gem install scanf — the same version MRI ships — so all five columns carry a real number (the interpreter-level table above predates that install).
  • Method: each process runs 3 untimed warm-up passes, then 25 timed passes of a fixed inner loop, 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.

scan-mixed — String#scanf("%s %d %f %x"), one mixed record

Runtime ns/op vs MRI
go-ruby (pure Go) 7110.1 0.26×
MRI 27858.0 1.00×
MRI + YJIT 26709.0 0.96×
JRuby 10254.4 0.37×
TruffleRuby 10186.2 0.37×

A single sscanf-style parse of "lorem 42 3.14159 deadbeef" with %s %d %f %x (string, decimal int, float, hex int — the inverse of one sprintf). The pure-Go scanf.Scan runs at 0.26× MRI (~3.9× faster) and 0.27× YJIT (7110.1 / 26709.0 = ~3.8× faster than YJIT) — MRI's Ruby-coded format engine and YJIT's JIT of it are both well behind a compiled directive walker. Go also leads the JVM/GraalVM Rubies (0.37× each). Beats YJIT.

scanall-pairs — String#block_scanf("%d %d"), 30 matches per call

Runtime ns/op vs MRI
go-ruby (pure Go) 17058.6 0.10×
MRI 166096.0 1.00×
MRI + YJIT 149184.0 0.90×
JRuby 34813.6 0.21×
TruffleRuby 30946.9 0.19×

The multi-match / streaming path: "1 2 … 60" matched two integers at a time, so scanf.ScanAll / block_scanf iterate the format 30 times per call. This is scanf's heaviest mode and the widest margin — pure-Go ScanAll runs at 0.10× MRI (~9.7× faster) and 0.11× YJIT (17058.6 / 149184.0 = ~8.7× faster than YJIT), because MRI re-drives its Ruby-level format loop and block dispatch on every one of the 30 sub-matches while the Go path amortises a single compiled scan. It also comfortably beats JRuby (0.21×) and TruffleRuby (0.19×). Beats YJIT.

Go-vs-YJIT verdict: the pure-Go library beats MRI + YJIT on both ops — 0.27× YJIT on scan-mixed (~3.8× faster) and 0.11× YJIT on scanall-pairs (~8.7× faster). scanf is a pure-Ruby stdlib, so this is genuine parity-and-beyond, not a C-extension gap; the margin widens on the multi-match path where the interpreter pays its per-iteration dispatch 30× over.

Reproduce

The harness is committed under benchmarks/: a self-contained Go driver (go/, pins the published library via go.mod), the equivalent ruby/scanf.rb workload, and run.sh. Run bash benchmarks/run.sh; env OUTER/WARM tune the pass budget and RUBY/JRUBY/TRUFFLERUBY select the runtime binaries. Restore scanf on JRuby/TruffleRuby first with gem install scanf.

Warm-up budget & noise — honest framing

Numbers reflect a fixed warm-process budget (3 warm-up + 25 timed passes in one process). The JVM/GraalVM JITs (JRuby, TruffleRuby) may need a larger warm-up to reach steady state, so their columns can understate peak throughput. Every number here is a real measured value from the dated run above (stable across three back-to-back runs) — nothing is fabricated, estimated, or cherry-picked. The go-ruby column is the pure-Go library; every other column is that interpreter's own scanf stdlib doing the equivalent work, output checked byte-identical to MRI before timing.