Skip to content

perf(vm): int64 fast path + batched arg-boxing for sprintf/String#% - #114

Merged
tannevaled merged 3 commits into
mainfrom
perf-sprintf-int64-fast-path
Jul 3, 2026
Merged

tannevaled merged 3 commits into
mainfrom
perf-sprintf-int64-fast-path

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Summary

A CPU + heap profile of a sprintf-heavy loop showed the format path is GC-bound on allocations, not on re-parsing the format string (parsing is <1% of CPU). The two sources: every operand boxed into the go-ruby-format Value interface one alloc at a time, and every integer routed through math/big even when it fit in an int64.

This PR cuts both on the Kernel#sprintf / Kernel#format / String#% / IO#printf path:

  • formatValue.Int64Fast — implements the library's optional int64 fast-path, so a native Integer (and an int64-range Bignum) formats without allocating a *big.Int. Inert-but-harmless against the current library release; it activates once go.mod consumes the companion library change.
  • Batched arg-boxing — formatString wraps the N operands in a single backing array referenced by pointer, replacing one interface box per argument with one allocation for the whole batch.

Companion library change + required go.mod bump

The int64 fast path lives in go-ruby-format/format#1. After it merges, bump this module to consume it:

GOWORK=off go get github.com/go-ruby-format/format@<merged-main-pseudo-version>

Until then this PR gives the batched-arg-boxing win alone; Int64Fast is dormant (the current library has no fast-path interface to call it).

Results

Isolated format path (rbgo objects, bypassing the interpreter):

ns/op allocs/op
before 688 32
after 418 11

1.65x, once the library bump lands. Note the end-to-end 5-way harness op is dominated by VM interpreter work (checksum reduce, string interpolation) around the sprintf call, so the engine speedup moves the isolated sprintf-only Ruby op ~15% and the full harness op ~1%; the format engine itself is 1.65x.

Correctness

  • Byte-exact MRI (CRuby 4.0.5) output unchanged — the differential/golden sprintf tests stay green.
  • internal/vm keeps 100% coverage (new Int64Fast branch test); go test -race ./internal/vm green under TZ=UTC; cross-builds green on all 6 64-bit arches + windows.

🤖 Generated with Claude Code

tannevaled and others added 3 commits July 3, 2026 13:53
The format binding routed every operand through the go-ruby-format
Value interface, and every integer through math/big: a profile of a
sprintf-heavy loop showed the format path was GC-bound on those
allocations (not on re-parsing the format string).

Two allocation cuts on the Kernel#sprintf / Kernel#format / String#%
path:

  * formatValue implements the library's Int64Fast fast-path, so a
    native Integer (and an int64-range Bignum) formats without
    allocating a *big.Int. This activates once go.mod consumes the
    go-ruby-format/format int64 fast path; it is inert but harmless
    against the current release.
  * formatString wraps the N operands in a single backing array
    referenced by pointer, replacing one interface box per argument
    with one allocation for the whole batch.

Isolated format path (rbgo objects, bypassing the interpreter):
  before 688 ns/op, 32 allocs/op
  after  418 ns/op, 11 allocs/op  (1.65x, once the library bump lands)

Byte-exact MRI output is unchanged (the differential/golden sprintf
tests stay green); internal/vm keeps 100% coverage, with a new
Int64Fast branch test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Activates the two library-side perf fixes end-to-end in rbgo:
- go-ruby-format: math/big-free int64 fast path (format engine 1.65x;
  headline sprintf gap is VM-general per-op alloc, not the formatter).
- go-ruby-zlib: pooled flate engines via Reset (zlib roundtrip
  51554->6653 ns/op = 0.88x MRI, now faster than C zlib; byte-identical
  output, MRI interop preserved). Adds klauspost/compress (indirect).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This branch predated #113 (the go-ruby-prime segmented-sieve bump), so its
go.mod still pinned the pre-fix version. Match main's pin so merging this
PR does not regress Prime.each back to ~12.8x MRI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 518026f into main Jul 3, 2026
9 checks passed
@tannevaled
tannevaled deleted the perf-sprintf-int64-fast-path branch July 3, 2026 12:14
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