Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/10666-collection-poll-stride.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Faster

- A global regular-expression replace spends 1.4-3.5% fewer instructions. The loop that collects the matches asked the collector whether it was due to run once per match, which costs about 436 instructions and could never do anything there — that loop writes its matches into a native buffer and creates nothing the collector can free. It now asks once per 64 matches, which is the same bound each search was already keeping. Peak memory is unchanged, measured over nine interleaved rounds of a replace at n=1,000,000 (#10165).
32 changes: 31 additions & 1 deletion crates/perry-runtime/src/regex/perex_replace_direct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ pub(super) fn admissible(receiver: &RuntimeHandle<'_>, reuse: &Reuse<'_, '_>) ->
&& reuse.name_count(re) == Some(0)
}

/// One collection-loop round in this many runs the GC safepoint poll.
///
/// The value matches `PRE_SEARCH_POLL_STRIDE`, and deliberately so: both count
/// one search, so they are parallel on the same unit rather than nested. Note
/// that 64 is a chosen margin in #10494, not a derived one -- the evidence
/// there (removing the poll left `cycle_starts`, `completions` and `steps`
/// identical across 48,000,000 calls) argues for removal and does not pick a
/// stride. Nothing here relies on 64 being the right number; see the call site.
const COLLECT_POLL_STRIDE: usize = 64;

/// One piece of a template: a span of the template itself, or a part of the
/// current match. Parsed once; the capture count is the program's.
#[derive(Clone, Copy)]
Expand Down Expand Up @@ -237,7 +247,27 @@ pub(super) fn replace(
let next = advance(bound, index, input_length, unicode, budget)?;
super::perex_dispatch::set_last_index(receiver, next)?;
}
host::poll()?;
// One collection-loop round in COLLECT_POLL_STRIDE runs the safepoint.
//
// This loop writes each match's spans into a native buffer and creates
// no JS garbage, so its poll enables no collection: nulling it moves
// peak RSS by +0.0% median over nine interleaved rounds of an
// allocating replace at n=1,000,000. (For contrast, polling 8x less
// often in `Pieces::finish`, which does produce garbage, moved the same
// figure +13.2%.)
//
// Worst-case work between executed polls does not grow. Every search
// this loop performs goes through `find_near`, which either polls
// unconditionally (the owned path, and any lent fallback) or ticks
// `PRE_SEARCH_POLL_TICK` and polls on one search in 64 (#10494). That
// tick advances once per search, which is once per iteration of this
// loop, so the two strides run in parallel on the same unit rather than
// composing: the bound stays 64 searches whether this poll is strided
// or not. Striding a site whose own counter advanced on a *different*
// unit would not be safe on this argument.
if searches % COLLECT_POLL_STRIDE == 0 {
host::poll()?;
}
}
if spans.values.is_empty() {
return Ok(boxed(input));
Expand Down
Loading