Skip to content

perf: int64 fast path for integer conversions (1.65x, math/big eliminated) - #1

Merged
tannevaled merged 1 commit into
mainfrom
perf-int64-fast-path
Jul 3, 2026
Merged

tannevaled merged 1 commit into
mainfrom
perf-int64-fast-path

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Summary

A CPU + heap profile of a mixed sprintf workload ("%05d|%-8s|%8.3f|%x|%o|%+d|%e") showed the formatter was GC-bound: runtime.kevent/runtime.madvise accounted for ~65% of CPU, all driven by allocations from math/big. Every integer conversion (%d/%i/%u/%x/%X/%o/%b/%B) routed its operand through a *big.Int — even a small int64 like 12345 — boxing the arg into a *big.Int and then allocating two more big values (Abs, Text) per directive. Parsing the format string was <1% of CPU, so the re-parse was never the bottleneck.

Change

An allocation-free int64 path:

  • intFast — an optional Value fast-path (Int64Fast() (int64, bool)) reporting an int64-range integer without allocating a *big.Int. renderInt uses it, falling back to the *big.Int path only for Bignums, Floats, String operands, and the negative-in-unsigned-base two's-complement case (which genuinely needs arbitrary precision).
  • goValue stores small integers as an int64, building a *big.Int only for magnitudes that exceed int64.
  • Pre-size the output strings.Builder to avoid its early re-growths.

renderIntBig is the former renderInt body verbatim, so arbitrary-precision output is byte-identical; the shared precision/width/pad tail is factored into emitInt.

Correctness

  • The live MRI (CRuby) 4.0.5 differential (TestDifferentialAgainstMRI) and golden suites stay green — small integers now exercise the new fast path and are validated byte-for-byte against CRuby.
  • New TestBignumSlowPath pins the Bignum path to CRuby 4.0.5 output.
  • 100% statement coverage retained; go test -race ./... green; cross-builds green on all 6 64-bit arches (amd64/arm64/riscv64/loong64/ppc64le/s390x) + windows.

Benchmark

Mixed "%05d|%-8s|%8.3f|%x|%o|%+d|%e":

ns/op allocs/op
before 865 39
after 543 16

Measured on the rbgo object path (bypassing the interpreter): 688 -> 418 ns/op, 32 -> 11 allocs/op (1.65x).

🤖 Generated with Claude Code

…ated)

Every integer conversion (%d/%i/%u/%x/%X/%o/%b/%B) routed its operand
through math/big.Int, even a small int64 like 12345: the arg was boxed
into a *big.Int, then Abs + Text allocated two more big values per
directive. A CPU/heap profile of a mixed sprintf workload showed the
work was dominated by GC pressure from these allocations, not by parsing
the format string (parsing is <1% of CPU).

Add an allocation-free int64 path:

  * intFast, an optional Value fast-path (Int64Fast) that reports an
    int64-range integer without allocating a *big.Int. renderInt uses it
    and only falls back to the *big.Int path for Bignums, Floats, String
    operands, and the negative-in-unsigned-base two's-complement case
    (which genuinely needs arbitrary precision).
  * goValue stores small integers as an int64, building a *big.Int only
    for magnitudes that exceed int64.
  * Pre-size the output Builder to avoid its early re-growths.

renderIntBig is the former renderInt body verbatim, so the arbitrary-
precision output is byte-identical; the shared precision/width/pad tail
is factored into emitInt. The live MRI 4.0.5 differential and golden
suites stay green, and new coverage cases pin the Bignum slow path to
CRuby byte-for-byte.

Benchmark (mixed "%05d|%-8s|%8.3f|%x|%o|%+d|%e"):
  before 865 ns/op, 39 allocs/op
  after  543 ns/op, 16 allocs/op

100% statement coverage retained.

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