Skip to content

perf/gc: regex scratch counted as external GC pressure forces full collections; growing it wins −16..−31% instr but +13..+28% RSS #11549

Open

Description

@proggeramlug

Found by the regex performance lane (#11543).

The regex slow path reports its per-call scratch buffers to the GC as external allocations. For patterns needing more than 32 match registers (for example dotenv's line regex, which has 42), that phantom external pressure keeps hot loops in frequent full collections.

Measured: letting the per-thread match scratch grow beyond 32 registers gives dotenv/parse −31% and moment/parse_format −16% instructions. But peak RSS rises +27.6% / +13.4%, because once the phantom pressure is gone the young generation grows to its cap before collecting. So it was not shipped, under the "minimize RSS AND keep best compute" rule.

We want both wins. Directions:

  1. Stop counting short-lived, immediately-freed scratch as external GC pressure, or count it as transient so it doesn't drive full collections.
  2. Separately, look at the young-generation pacing that lets RSS climb to the cap when allocation pressure is low. An RSS-aware nursery cap or earlier minor GC would keep RSS flat with the faster regex path.

Measurements are on branch wip/perf-regex-matcher and in #11543's description.

Activity

  1. proggeramlug commented on Sep 28, 2026

    @proggeramlug
    ContributorAuthor

    Progress: #11634 landed. A copying minor's fixed cost drops ~87% (alloc loop: 753k -> 98k instructions per minor) via a young-only intern-table log, skipping empty array-tail tables, a young-only closure-metadata prune and skipping pinned string caches on minors; each skip has a completeness check and a sabotage test. No default changed. Leaving this open for the remaining per-minor cost.

  2. 6 remaining items

  3. proggeramlug commented on Sep 30, 2026

    @proggeramlug
    ContributorAuthor

    Status: the regex scratch is no longer counted as GC pressure. That landed with #11645 (merged 9e29f59, carrying #11612), together with #11634, #11668 and #11676 (the minor's fixed cost and per-object trace cost) and survival-aware nursery pacing. The regressions the owner accepted are tracked in #11699. Keeping this open until the post-fix package matrix confirms the dotenv/moment numbers.

  4. proggeramlug commented on Oct 23, 2026

    @proggeramlug
    ContributorAuthor

    Verified fixed on current main — suggest closing

    Both directions landed. The accounting change is in regex/perex_memory.rs, which now cites this issue in its header:

    This scratch is NOT reported to the collector as external side bytes (#11549). Everything here is freed by the operation that allocated it, when that operation returns or unwinds; no collection can ever reclaim a byte of it, and no collection is needed for it to be released.

    Direction 2 landed separately as 9e29f59d4 (survival-aware nursery pacing, 4 MB floor on the influx ladder, includes #11612).

    Measured on the shape this issue names

    A pattern with 42 match registers — more than the 32 the lent cell holds, so every call takes the owned scratch path — exec in a hot loop, 300,000 calls, on f7725820a:

    trigger kinds over the whole run nursery ×43, ArenaBytes ×43 — no OldReclaim at all
    external_side 0, every trigger line
    copying minors 87 (one per ~3,450 calls)

    The symptom this issue describes — per-call buffer releases accumulating in GC_EXTERNAL_SIDE_DRAINED_SINCE_FULL and pacing the loop "into a budgeted full mark-sweep every few hundred calls" — produced OldReclaim triggers. There are none.

    The RSS objection no longer applies

    This was held back because removing the phantom pressure cost +27.6% / +13.4% peak RSS. On that 42-register workload now:

    peak RSS
    perry 16.0 MB
    node 26.5.1 53.3 MB

    Perry is 3.3× under node. Even the full +27.6% from the original measurement would leave it at ~20 MB, still less than half of node's. Whatever the "minimize RSS AND keep best compute" rule was protecting, it is not at risk at this size.

    What is not fixed, and is a different issue

    Instructions per call on that workload are still 40,983 against node's 2,576 — 15.9× (control-subtracted, 300k calls, outputs identical). That is the owned-scratch path's own cost, not the GC pacing this issue is about, and it is the largest per-call regex ratio I can currently measure. Worth its own issue rather than keeping this one open for it.

  5. added
    performanceRuntime, compile-time, build-size, or memory performance
    on Oct 26, 2026
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

    performanceRuntime, compile-time, build-size, or memory performance

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions