Skip to content

gc: Perry-owned memory for GC arena blocks (2 MiB OS regions, huge pages by complete units) - #12132

Merged
proggeramlug merged 6 commits into
mainfrom
perf/gc-region-manager
Oct 7, 2026
Merged

proggeramlug merged 6 commits into
mainfrom
perf/gc-region-manager

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Perry now owns the memory under its GC heap on Linux. Arena blocks come straight from the OS in 2 MiB-aligned regions instead of from the Rust global allocator (mimalloc), and page size is chosen by how much of a 2 MiB unit is used, not by object lifetime.

What changes

  • OS regions for arena blocks. Each agent maps 2 MiB-aligned regions and pools its blocks. mimalloc keeps ordinary native allocations only. Other platforms keep std::alloc and the 1 MiB geometry.
  • 2 MiB GC blocks on Linux. One usable block per aligned region, so a block and a huge page are the same unit.
  • One page-size rule. Every generation gets MADV_HUGEPAGE on complete 2 MiB units, and sub-unit extents and rounded tails stay on base pages. A nursery is refilled every cycle, so it is dense, and it gets huge pages like mature memory. There is no prefault, no collapse request and no tuning constant.
  • The defrag selector decides from the allocation kind stored in the block's existing metadata (it fits in padding), not from a size threshold.
  • Census bitmap capacity follows the block size.
  • Separate fix in the GC trigger: a parse bump compared against the nursery-capped effective trigger, so it could lower an already armed whole-arena threshold. With bigger blocks this re-armed a minor on almost every small parse (fastify: 379 minors instead of 17). Now the bump compares with the whole-arena threshold itself, and it never lowers it.

What it removes: GC memory no longer goes through a general allocator that can't know GC lifetimes or density, and the page-size decision is one rule instead of a nursery/mature split.

Measurements

These were measured on c85c2db (before #12103), on qb6 CPUs 56–63 under the shared measurement lock. n=5 interleaved runs, default auto-optimized builds, and every output matches Node. The rebase onto current main only crossed Buffer/stream test files.

Program RSS MiB main → this sys ms main → this wall s main → this
tsc 206.1 → 176.9 23 → 32 (noise 49) 1.010 → 1.022
fastify inject 262.2 → 226.5 86 → 77 (noise 26) 11.63 → 11.75
Effect 181.4 → 153.1 26 → 35 (noise 17) 2.324 → 2.306
Zod ×5000 48.8 → 32.6 1.129 → 1.128
qs parse 50.9 → 34.5 2.116 → 2.109
qs stringify 52.8 → 36.9 4.533 → 4.530
commander 46.6 → 30.2 0.745 → 0.743
buffer_heavy 91.6 → 73.2 0.800 → 0.834
worker_heavy 209.9 → 116.0 0.205 → 0.218
upm lock 211.7 → 148.6 0.453 → 0.445
upm cold 361.8 → 251.7 0.540 → 0.627
hello 1.6 → 1.6
  • System CPU on tsc, fastify and Effect is within the same-binary noise of main. Wall medians are within the host's noise, and no speedup is claimed.
  • Most RSS saving comes from not backing sparse GC memory with huge pages inside mimalloc. A THP-off control on main reaches similar RSS, but costs page faults; this change keeps huge pages where memory is dense.
  • An earlier variant with base pages for the nursery saved about 5 MiB more on small programs (Zod, qs, commander) but cost +40–60 ms system CPU on tsc, fastify and Effect.

Known limits:

  • Some programs show instruction differences of about ±0.1% against main that are not fully attributed. Part of it comes from the measured binaries' path length changing startup allocation.
  • A whole nursery unit gets huge pages even when a small program only partly fills it. That costs about 5–6 MiB on small programs, and an advise-after-fill step would be a follow-up.

Tests

  • cargo test --release -p perry-runtime -- --test-threads=1: 5206 passed, 0 failed. Plain main fails only the known z8 RSS flake (Expire retired receiver shapes in the class read cache #12111). The build is clean under -D warnings.
  • The z8 stream churn test now restores the payload-prototype, runtime-handle, shape-rekey, promise and timer scanners after its copying-GC isolation guard. Without that, the guard dropped roots the workload uses and the test crashed. Its assertions are unchanged.
  • Five parity subsets pass with none skipped.
  • New tests: region mapping and alignment, census capacity at both geometries, the allocation-kind defrag selector, page advice (complete units huge, tails base), a fastify-like churn that keeps the minor count equal at 1 MiB and 2 MiB, and worker transfer with sender exit.
  • Each was checked by reverting its fix: the census capacity, the non-moving extent filter, the pool charge, the promotion advice, nursery advice and the trigger comparison. Every test fails without its fix.

Open question for review

A diagnostic control (z8 without the isolation guard, at the larger batch count) hit the young-log verifier at gc/young_log.rs:173: young log for shapes.families+indices does not name …, reached through scan_shape_table_rekey_mut during a moving minor. Main at a matched larger count passed that control, so this is not shown to be pre-existing. It does not reproduce with the guard, and the full suite passes. Reviewers who own the shape table: please check whether the larger blocks expose a missing young-log entry there. The backtrace is available on request.

Ralph Küpper and others added 6 commits October 7, 2026 02:09
Linux arena blocks now come straight from mmap in aligned regions, pooled per agent, instead of from the Rust global allocator. mimalloc keeps native allocations only. Other platforms keep std::alloc.
The bump compared against the effective trigger, which is clamped to the nursery cap, so it could lower an armed whole-arena threshold and re-arm a minor on every small parse. Compare with the whole-arena threshold itself.
Linux GC blocks are 2 MiB in 2 MiB-aligned regions. Page size follows how much of a unit is used, not object lifetime: complete 2 MiB units get MADV_HUGEPAGE in every generation, sub-unit extents and rounded tails stay on base pages. The defrag selector decides from the allocation kind stored in the block, not a size threshold.
@coderabbitai

coderabbitai Bot commented Oct 7, 2026

Copy link
Copy Markdown

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 58db139c-e9e1-41e7-a1df-3197d2c3e4d8
📥 Commits

Reviewing files that changed from the base of the PR and between 68e41fa and 7961e37.

📒 Files selected for processing (29)
  • changelog.d/gc-region-manager-2mib.md
  • crates/perry-runtime/src/arena/block.rs
  • crates/perry-runtime/src/arena/block/reuse_window_tests.rs
  • crates/perry-runtime/src/arena/mod.rs
  • crates/perry-runtime/src/arena/page_meta/mod.rs
  • crates/perry-runtime/src/arena/promote.rs
  • crates/perry-runtime/src/arena/quarantine.rs
  • crates/perry-runtime/src/arena/region.rs
  • crates/perry-runtime/src/arena/tests.rs
  • crates/perry-runtime/src/ffi/perry_memory_profile.c
  • crates/perry-runtime/src/gc/large_extent_selection_tests.rs
  • crates/perry-runtime/src/gc/oldgen_defrag.rs
  • crates/perry-runtime/src/gc/policy.rs
  • crates/perry-runtime/src/gc/tests/adopt_census.rs
  • crates/perry-runtime/src/gc/tests/census_block_windows.rs
  • crates/perry-runtime/src/gc/tests/census_two_mib.rs
  • crates/perry-runtime/src/gc/tests/debt_pacer.rs
  • crates/perry-runtime/src/gc/tests/evacuation.rs
  • crates/perry-runtime/src/gc/tests/incremental_sweep_reclaim.rs
  • crates/perry-runtime/src/gc/tests/mod.rs
  • crates/perry-runtime/src/gc/tests/native_payload_streams.rs
  • crates/perry-runtime/src/gc/tests/oldgen.rs
  • crates/perry-runtime/src/gc/tests/runtime_roots.rs
  • crates/perry-runtime/src/gc/tests/sweep_window_births.rs
  • crates/perry-runtime/src/gc/tests/triggers.rs
  • crates/perry-runtime/src/gc/trace.rs
  • crates/perry-runtime/src/startup_memory_profile.rs
  • test-files/_helpers/region_transfer_sender_exit.ts
  • test-files/test_gap_region_transfer_sender_exit.ts
 ____________________________________________________________________________________________
< If you don't start with a spec, every piece of code you write is a patch. - Leslie Lamport >
 --------------------------------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant