Skip to content

perf: string-keyed Map is 4.2x node with repeated constant keys (integer-keyed Map wins); template literals ~1.9x #10697

Description

@proggeramlug

Context

#10695 shows regex is 63% of a realistic program, and that removing it moves perry from 0.18× to 0.54× against node. This issue is the residue — what remains after regex, which still leaves perry ~1.9× behind node at 100k lines. Two items account for most of it.

Per-operation, fitted across N=10,000→100,000 so startup drops out. perf stat -e instructions:u, perrymaster, perry from main 68a545439 + #10611, node v26.8.1, bun 1.3.14. Output checked equal to node on every row.

1. Map with string keys — 4.2× node in the commonest shape

shape perry node bun vs node
integer keys, set+get 285 504 350 1.76× — perry wins
string keys, 1,024 distinct ("k"+i) 1,184 958 1,006 0.80×
4 constant string keys, get-or-default 1,298 320 289 0.24×

The third row is the ordinary "count things by category" pattern — a handful of repeated constant string keys — and it is the worst case, at 4.2× node and 4.5× bun.

The interesting part is that it is worst where it should be easiest. node gets faster with repeated constant keys (958 → 320) because they intern and the lookup becomes pointer comparison. perry gets slightly slower (1,184 → 1,298). perry appears to derive no benefit from key repetition, which suggests hashing and comparing the string contents on every lookup rather than exploiting identity.

Integer-keyed Map is genuinely good and needs nothing.

2. Template literals — ~1.9× node

shape perry node bun
4-interpolation template, content consumed 2,640 1,416 1,657

Modest, but it is the base layer of any string-producing program, so it multiplies through.

A measurement warning, because I nearly filed a wrong number

Measured with h = (h*31 + t.length) | 0 — reading only the template's length — the same program gives perry 1,619 vs node 173, which looks like 9.4×. That is an artifact: node's JIT can satisfy .length without materialising the string, so it is not building one at all. Forcing the content to be consumed (charCodeAt across the string) gives node 1,416 and the honest ratio 1.9×.

Any benchmark that builds a string and then only measures its length is measuring nothing in node or bun. Consume the content.

Priority

Both are second-tranche: worth doing after regex (#10166, #10518, #10519), which is 63% of a real program against these two together being a fraction of the remaining 37%. The string-keyed Map is the larger of the two and has the clearer mechanism.

Related: #10695 (crossover map and the real-program decomposition), #10696 (JSON per-object cost).

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