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).
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).
String(n), small int"" + n`${n}`templaten.toString()String(n), up to 10⁶String(n + 0.5)— floatTwo 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 forn.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:
a[i] += 1cost 11.5×a[i] = a[i] + 1. Fixed in the matcher; 277 → 25.5.O[K]with a hoisted const key costs 7.6× the identical read spelledO.a.n.toString()costs 2.6×String(n).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 intext, 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).