Skip to content

perf: crossover map — perry beats node/bun on 7 of 9 primitives at 100k iterations, and loses on regex and JSON #10695

Description

@proggeramlug

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).

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