Summary
Property access — the most executed operation in JavaScript — costs perry 13× to 112× what node costs, measured with both JITs warm.
Fitted at two widely separated ranges so warmup contamination is visible. perry's figures are identical at both ranges (it is AOT and has no warmup); node's and bun's fall sharply, which is why a small-range fit understates this badly.
| operation |
fit range |
perry |
node |
bun |
O.a + O.b — two static reads |
20k→200k |
162 |
53 |
21 |
|
500k→5M |
162 |
12 |
25 |
O[K] + O[J] — two const-key reads |
20k→200k |
1236 |
41 |
84 |
|
500k→5M |
1236 |
11 |
15 |
O.v = k; h += O.v — write then read |
20k→200k |
182 |
74 |
71 |
|
500k→5M |
183 |
12 |
13 |
String(k % 1000) — number to string |
20k→200k |
433 |
75 |
78 |
|
500k→5M |
435 |
114 |
32 |
The gaps, warm
| operation |
perry vs best of node/bun |
| two static property reads |
13.5× (162 vs 12) |
| two const-key property reads |
112× (1,236 vs 11) |
| property write + read |
15× (183 vs 12) |
String(n) |
13.6× (435 vs 32) |
A single static property read costs perry ~81 instructions. node does it in ~6.
Why the const-key row is the place to start
O[K] with a hoisted const K = "a" costs 1,236 for two reads — 112× node — while the identical reads spelled O.a cost 162. That is a 7.6× penalty for the spelling alone, on a pure hit path with no missing keys, no prototype walk, and no invalidation obligations whatsoever.
It is the same class of defect as #10743 (a[i] += 1 costing 11.5× a[i] = a[i] + 1): a fast path exists and a common spelling cannot reach it. #10743 turned out to be tractable — the fix was in the matcher, not the lowering — and it took that row from 277 to 25.5.
Relationship to existing issues
Measurement notes for anyone reproducing
Fit per-op costs at two widely separated ranges and check the value is stable. This campaign published three wrong tables before adopting that: one comparing totals (crediting perry with node's ~95M startup), one fitting deltas inside node's warmup, and one using a benchmark shape real code does not use. perry's flatness across ranges is the detector — if perry's number does not move and the other runtime's moves 4×, the other runtime is not yet at steady state.
Summary
Property access — the most executed operation in JavaScript — costs perry 13× to 112× what node costs, measured with both JITs warm.
Fitted at two widely separated ranges so warmup contamination is visible. perry's figures are identical at both ranges (it is AOT and has no warmup); node's and bun's fall sharply, which is why a small-range fit understates this badly.
O.a + O.b— two static readsO[K] + O[J]— two const-key readsO.v = k; h += O.v— write then readString(k % 1000)— number to stringThe gaps, warm
String(n)A single static property read costs perry ~81 instructions. node does it in ~6.
Why the const-key row is the place to start
O[K]with a hoistedconst K = "a"costs 1,236 for two reads — 112× node — while the identical reads spelledO.acost 162. That is a 7.6× penalty for the spelling alone, on a pure hit path with no missing keys, no prototype walk, and no invalidation obligations whatsoever.It is the same class of defect as #10743 (
a[i] += 1costing 11.5×a[i] = a[i] + 1): a fast path exists and a common spelling cannot reach it. #10743 turned out to be tractable — the fix was in the matcher, not the lowering — and it took that row from 277 to 25.5.Relationship to existing issues
Map, no benefit from key repetition) and theMapsuperlinearity noted in perf: crossover map — perry beats node/bun on 7 of 9 primitives at 100k iterations, and loses on regex and JSON #10695 are likely the same underlying lookup machinery.Measurement notes for anyone reproducing
Fit per-op costs at two widely separated ranges and check the value is stable. This campaign published three wrong tables before adopting that: one comparing totals (crediting perry with node's ~95M startup), one fitting deltas inside node's warmup, and one using a benchmark shape real code does not use. perry's flatness across ranges is the detector — if perry's number does not move and the other runtime's moves 4×, the other runtime is not yet at steady state.