Skip to content

perf: number-to-string is 6.6x-15.9x bun, and four spellings of it differ by 2.6x inside perry while bun spans 30-34 instructions #10762

Description

@proggeramlug

Summary

Converting a number to a string costs perry 6.6× to 15.9× what bun costs, and four different spellings of the same operation differ by 2.6× inside perry while bun does all of them in ~30 instructions.

Fitted warm across N=500k→5M (perry is flat across fit ranges; the JITs are not — see the measurement note).

spelling perry node bun perry vs best
String(n), small int 206 106 31 6.6×
"" + n 290 49 32 9.0×
`${n}` template 435 51 30 14.5×
n.toString() 542 103 34 15.9×
String(n), up to 10⁶ 570 339 410 1.6×
String(n + 0.5) — float 1205 780 604 1.9×

Two things the spread says

Floats are nearly fine; integers are not. Float formatting is 1.9× off — that is a real algorithm (shortest round-trip) and perry is in the same league. Small-integer formatting is 6.6× to 15.9× off, and bun does it in ~30 instructions, which is about what a fast itoa or a small-value cache costs.

Four spellings of one operation span 2.6× inside perry — 206 for String(n) against 542 for n.toString() — while bun spans 30 to 34. That means perry has multiple conversion paths and the better ones are not reached by the common spellings. A template literal, which is the most idiomatic way to interpolate a number, is the second-worst at 435.

This is the third instance of this exact pattern on this campaign:

In each case a fast path exists and a common spelling cannot reach it. #10743 proved that shape can be tractable without touching semantics.

Why it matters

Number-to-string is ubiquitous: logging, serialisation, every ${n} interpolation, CSV and report generation, JSON output, key construction. It was the single largest item in text, the string-heavy program in #10695, at 431 instructions against bun's 30.

Measurement note

Fit at two widely separated ranges and check the value is stable. perry's figures are identical across ranges because it is AOT; node's and bun's fall sharply once warm, so a small-range fit understates every gap here. This campaign published three wrong tables before adopting that rule — one comparing totals, one fitting inside node's warmup, and one using a benchmark shape real code does not use.

Related: #10761 (property access, same campaign), #10695 (where perry stands on real programs).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions