Where perry stops winning, per primitive
perry beats node and bun on every short program, because node pays ~95M instructions of startup and bun ~47M against perry's ~750k. That advantage is a constant, so it is consumed by any program that does enough work. This maps where that happens, per primitive.
Method: one program per primitive, size from process.argv[2], so N varies with no recompile. perf stat -e instructions:u, min of 2, on perrymaster. perry built from main 68a545439 + #10611; node v26.8.1; bun 1.3.14. Every row was checked for output equality against node before its numbers were taken.
Ratios are node÷perry and bun÷perry — above 1.0 perry wins, below 1.0 perry loses.
| primitive |
N=1,000 |
N=10,000 |
N=100,000 |
string += |
134× / 56× |
53× / 29× |
14.1× / 5.1× |
| object property r/w |
108× / 52× |
57× / 28× |
11.7× / 5.3× |
for...of over array |
110× / 52× |
43× / 23× |
9.1× / 5.3× |
| object literal alloc |
111× / 50× |
49× / 23× |
9.0× / 4.2× |
indexed for loop |
112× / 53× |
43× / 24× |
7.5× / 4.6× |
Array.prototype.map |
93× / 46× |
29× / 15× |
4.4× / 2.4× |
Map set+get |
86× / 40× |
22× / 11× |
3.7× / 2.5× |
regex exec |
11.2× / 5.2× |
1.48× / 0.74× |
0.28× / 0.21× |
JSON round-trip |
2.78× / 1.31× |
0.92× / 0.52× |
0.40× / 0.29× |
Reading
Seven of nine primitives are healthy. At 100,000 iterations they still run 3.7–14× fewer instructions than node and 2.4–5.3× fewer than bun, and the decline across sizes is just the startup advantage being amortised — which is expected and fine.
Two lose outright, and they are the two that matter most for real programs.
Per-element cost, fitted across the 10k→100k interval so the startup constant drops out:
|
perry |
node |
ratio |
regex exec |
9,393 instr/call |
1,522 |
6.2× |
JSON stringify+parse |
15,964 instr/round-trip |
5,369 |
3.0× |
regex crosses below node between N=10,000 and 100,000, and below bun before N=10,000. JSON crosses below node just under N=10,000.
Why this is the roadmap and not just a table
Any program that parses configuration, validates input, matches routes, serialises a response or reads JSON off a socket does these two things thousands of times. A program doing 100k regex matches runs 3.6× slower than node and 4.8× slower than bun, regardless of how fast it started.
So the startup win — real and large — does not survive contact with a realistic workload unless these two are fixed. The other seven primitives need nothing; they are already several times ahead at the largest size measured.
Known, already-filed causes
regex has located root causes in flight: #10166 (per-call cost with the regex hoisted), #10518 (spec flags/@@species Gets taken through the generic property path on pristine RegExps), #10519 (KMP reading one UTF-16 unit per bounded-reader call), #10165, #10164.
JSON has no equivalent attribution that I can find, and on this evidence it is the second-biggest obstacle to beating node and bun on real programs. Note that the N=1,000 JSON figure also carries the ~25M one-time constant from #10686, which is a separate defect — the per-element number above is measured above it and is unaffected by fixing it.
Related: #10686 (a ~25M one-time constant), #10689 (its correctness twin).
Where perry stops winning, per primitive
perry beats node and bun on every short program, because node pays ~95M instructions of startup and bun ~47M against perry's ~750k. That advantage is a constant, so it is consumed by any program that does enough work. This maps where that happens, per primitive.
Method: one program per primitive, size from
process.argv[2], so N varies with no recompile.perf stat -e instructions:u, min of 2, on perrymaster. perry built from main68a545439+ #10611; node v26.8.1; bun 1.3.14. Every row was checked for output equality against node before its numbers were taken.Ratios are node÷perry and bun÷perry — above 1.0 perry wins, below 1.0 perry loses.
+=for...ofover arrayforloopArray.prototype.mapMapset+getexecJSONround-tripReading
Seven of nine primitives are healthy. At 100,000 iterations they still run 3.7–14× fewer instructions than node and 2.4–5.3× fewer than bun, and the decline across sizes is just the startup advantage being amortised — which is expected and fine.
Two lose outright, and they are the two that matter most for real programs.
Per-element cost, fitted across the 10k→100k interval so the startup constant drops out:
execJSONstringify+parseregex crosses below node between N=10,000 and 100,000, and below bun before N=10,000. JSON crosses below node just under N=10,000.
Why this is the roadmap and not just a table
Any program that parses configuration, validates input, matches routes, serialises a response or reads JSON off a socket does these two things thousands of times. A program doing 100k regex matches runs 3.6× slower than node and 4.8× slower than bun, regardless of how fast it started.
So the startup win — real and large — does not survive contact with a realistic workload unless these two are fixed. The other seven primitives need nothing; they are already several times ahead at the largest size measured.
Known, already-filed causes
regex has located root causes in flight: #10166 (per-call cost with the regex hoisted), #10518 (spec
flags/@@speciesGets taken through the generic property path on pristine RegExps), #10519 (KMP reading one UTF-16 unit per bounded-reader call), #10165, #10164.JSON has no equivalent attribution that I can find, and on this evidence it is the second-biggest obstacle to beating node and bun on real programs. Note that the N=1,000 JSON figure also carries the ~25M one-time constant from #10686, which is a separate defect — the per-element number above is measured above it and is unaffected by fixing it.
Related: #10686 (a ~25M one-time constant), #10689 (its correctness twin).