perf(regex): capture-heavy exec is 15.9x Node — GC accounting is as large a share as the matcher
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 48/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- node.js, rust, typescript
- Domain
- backend, performance
Research direction
Start by reading the regex execution path around perex::executor::Vm::trial, atom_scan, run, and the host glue that builds the result array; the issue names these profile symbols but no source files or tests. Reproduce with bigreg.ts using the listed perry compile command, then profile the capture-heavy exec workload. Done means a measured reduction in per-call cost with matching output, while isolating which reported subsystem changed.
Written by the indexing model from the issue text.
Description
What happened
RegExp.prototype.exec on a capture-heavy pattern — 18 groups, 42 match registers — costs 40,983 instructions per call against Node 26.5.1's 2,576 — 15.9×, at a fixed 44-character subject.
Found while verifying #11549, which this is not. That issue is fixed: external_side=0 throughout and zero OldReclaim triggers over 300,000 calls, and its lent-cell bound is now 1024 registers, so this workload takes the lent path (find_near_lent is in the profile) and pays no owned-scratch allocation at all. What remains is the per-call cost of that path.
Reproduction
bigreg.ts — a dotenv-shaped line regex, 18 capture groups / 42 registers, with a 64-string rotating subject pool so nothing is loop-invariant:
const re = /^(\s*)(?:(export)\s+)?([\w.-]+)(\s*)=(\s*)(?:(')([^']*)(')|(")([^"]*)(")|(`)([^`]*)(`)|([^#\r\n]*))(\s*)(?:#(.*))?$/;
const lines: string[] = [];
for (let i = 0; i < 64; i++) lines.push(' export KEY_' + i + ' = "value' + i + '" # note');
let h = 0;
for (let k = 0; k < 300000; k++) { const m = re.exec(lines[k & 63]); if (m !== null) h = (h + m.length + (m[3] ? m[3].length : 0)) | 0; }
console.log(h);
A control binary differing only in the exec call is subtracted, so the figures are the call alone. Both runtimes print 7153120.
perry compile bigreg.ts --no-auto-optimize -o bigreg && ./bigreg
Measured on f7725820a, Linux x86_64, perrymaster, against Node 26.5.1. Peak RSS is 16.0 MB against Node's 53.3 MB, so memory is not the problem here.
Where the instructions go
perf record -F 2000 on a --debug-symbols build, aggregated by subsystem:
| subsystem | share |
|---|---|
regex engine (perex::executor) |
34.9% |
| allocation pacing | 13.9% |
| GC, other | 12.4% |
| GC trigger evaluation | 8.5% |
| object model | 7.5% |
| regex host glue | 7.3% |
| result array | 7.3% |
| string/mem | 1.9% |
| unattributed | 6.2% |
GC-related work totals ~34.8% — the same share as the matcher itself. The top symbols:
9.76% <perex::executor::Vm>::trial::<true>
6.60% gc::allocation_pacing::sample_arena
6.28% <perex::executor::Vm>::atom_scan
5.55% <perex::executor::Vm>::run
4.63% gc::policy::gc_budgeted_due_trigger_probe
2.95% arena::inline::sync_inline_arena_state
2.82% array::header_gc_slots::store_array_slot
2.65% <perex::executor::Vm>::byte_run
2.63% gc::allocation_pacing::note_allocation
2.63% gc::barrier_store::runtime_store_jsvalue_slot
Three separable owners
- Allocation pacing, ~13.9% (
sample_arena6.60%,sync_inline_arena_state2.95%,note_allocation2.63%). Per-allocation instrumentation, and this call allocates a result array plus 42 capture slots, so it pays it many times. Not regex-specific — any allocation-heavy call pays it — but this workload makes it measurable. - GC trigger evaluation, 8.5% (
gc_budgeted_due_trigger_probe4.63%). The residue of the once-per-gc_mallocladder; a probe now exists, and it is still 8.5% here. - The matcher, 34.9%, and the regex host boundary, 14.6% (host glue plus result array). The second is the regex lane's; the first is the engine's. 18 groups means the result array carries 19 entries and the captures are copied out per call, so some of that boundary cost scales with the group count rather than being fixed.
What this is not
Not #11549 (fixed, verified). Not #10165 (quadratic scaling — this is a flat per-call cost at a fixed subject length). Not #10518's flags Get, which exec does not perform.
Not established
- How much of this is the capture count rather than the pattern's complexity. A one-group pattern on the same subject would separate them, and I have not run that.
- Whether the engine's 34.9% is reducible, which is a question for the engine rather than for Perry.
- Dominant language
- Rust
- Stars
- 4.9k
- Forks
- 166
- Avg merge
- 1h 52m
- Merged PRs (30d)
- 913
Getting set up
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from PerryTS/perry
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
Maintainers usually reply within 1 day
-
Difficulty 3/5 1-2 days Newbie friendliness 70/100
Maintainers usually reply within 1 day
-
Difficulty 3/5 1-2 days Newbie friendliness 73/100
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 73/100
Maintainers usually reply within 1 day
-
C-bug needs-triage O-UEFI T-libs
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
bug priority: P3 ready-for-agent
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
shep-pm/shep-kelpie#1332 ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
ramensoftware/windhawk#1156 · 2 comments ·
-
area/cli complexity:trivial good first issue Stellar Wave test
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Externalize-Labs/externalize#23 ·
Maintainers usually reply within 1 day