Skip to content

perf(gc): gc_check_trigger runs the 436-instruction ladder once per gc_malloc — collapse to a watermark, using the allocator's existing limit word for the young arm #10698

Description

@proggeramlug

Summary

gc_check_trigger runs the budgeted due-ladder — 436 instructions, measured exactly under callgrindonce per gc_malloc, in every workload. This is the residue left after #10659 was closed won't-fix: the epoch-memo approach there is structurally refuted (the ladder's inputs are the allocation counters, so an epoch keyed on them bumps on every allocation and the memo's hit rate is zero), and the poll-frequency lever is regex-local and handled at its own call sites.

This issue is for the remaining, general cost, and for the one design shape that looks viable.

It is now costed, on a workload that matters

A profile of RegExp.prototype.exec (2M calls, control-subtracted, by the session that owns perex) attributes:

  • gc_budgeted_due_trigger_eval — 3.68%
  • gc_budgeted_step_work_units_inner_with_progress — 1.58%
  • copying_from_space_in_use_bytes — 1.48%

≈6.7% of every exec call. And it is not the safepoint poll: at 2M calls with #10494's 1-in-64 stride the poll cannot account for it. It is gc_check_trigger firing once per gc_malloc, and exec allocates several objects per call, so it pays the 436 several times over.

Since #10695 measures regex as 63% of a realistic program's runtime, ~6.7% of exec is roughly 4% of whole-program time on that workload — a concrete figure rather than "everywhere, forever".

Why the cost is what it is

The 436 is not arithmetic. It is ~13 thread-local lookups (enumerated in trigger_path_hot_slot_indices, policy.rs:3362-3422), plus a RefCell borrow for malloc_object_count, plus the from-space memo read. The arms themselves are already counter >= watermark comparisons — total >= next_arena_trigger_base(), malloc_count >= GC_NEXT_MALLOC_TRIGGER.

So the ladder re-gathers a dozen quantities in order to evaluate a handful of comparisons that are mostly not close to firing.

The shape that looks viable: a watermark, not a memo

Collapse the arms into a single "next check at" scalar plus one counter, so the common case is one load and one compare.

For the young-generation arm specifically there is an existing precedent that costs nothing: alloc_sample::inline_limit (arena/alloc_sample.rs:162-171) already clamps the inline allocator's limit word to force a return to the runtime at a chosen byte count. That limit is compared on every allocation already. Setting state.size = min(block.size, offset + (cap − sealed)) at each sync/reset point makes "is the nursery cap due" a branch the allocator is already taking — zero added instructions on the compiled fast path, which is the constraint that killed every other approach (the inline bump sequence is seven instructions, emitted directly into LLVM IR at perry-codegen/src/lower_call/new_alloc.rs:528, with no call into the runtime).

Known complication: js_inline_arena_slow_alloc would need an extra arm to distinguish a trip-wire from a genuine block-full, since try_alloc_current would succeed and skip gc_check_trigger. That arm is off the inline path.

Not costed

I have not costed the redesign itself, and the per-arm work of collapsing the ladder is unknown. What is measured is the prize (436 per gc_malloc; ~6.7% of exec; ~4% of a regex-heavy real program) and the existence of a zero-cost mechanism for the hottest arm.

Anything built here must not add instructions to the compiled inline bump sequence. That is #10377's lesson twice over, and the reason the obvious incremental-counter approaches were rejected in #10659.

Related: #10659 (closed won't-fix, with the full refutation of the memo approach), #10695, #10494, #10377.

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