diff --git a/Cargo.lock b/Cargo.lock index 480657380e..7b0c5752b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5617,9 +5617,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perex" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc61f41aef38c94e922057977bcb33bf185ab42242188719991ecfdc0fa1fe6b" +checksum = "1473d470930ed48574515a25df34900f3af89c6fa422d903e019121312a9f13e" [[package]] name = "perry" diff --git a/Cargo.toml b/Cargo.toml index b4f5891706..203089dde0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -399,7 +399,7 @@ chrono = "0.4" regex = "1.12" aho-corasick = "1.1" # The single regular-expression engine, through crates/perry-perex. -perex = "0.1.7" +perex = "0.1.9" hex = "0.4" tempfile = "3" itoa = "1.0" diff --git a/changelog.d/10580-search-run-one-entry.md b/changelog.d/10580-search-run-one-entry.md new file mode 100644 index 0000000000..426a6db087 --- /dev/null +++ b/changelog.d/10580-search-run-one-entry.md @@ -0,0 +1,3 @@ +### Faster + +- Regular expression calls spend 305 to 332 fewer instructions each, 4.3% to 12.5% depending on the pattern. A `test` or `exec` call used to acquire the engine's program and subject views twice — once to learn the shape of the search and again to run it — and move the search between them; it now does both in one entry, and a search that its first quantum decides is never built as a resumable one at all (#10166). diff --git a/crates/perry-runtime/src/regex/perex_runtime.rs b/crates/perry-runtime/src/regex/perex_runtime.rs index a4853ba871..fccd00fe3d 100644 --- a/crates/perry-runtime/src/regex/perex_runtime.rs +++ b/crates/perry-runtime/src/regex/perex_runtime.rs @@ -11,8 +11,8 @@ use perex::binding::{ }; use perex::compiler::{self, CompileError, Node, Range}; use perex::executor::{ - ExecError, Frame, Progress, Scratch, ScratchOwner, ScratchRequirements, Search, SearchError, - Undo, + ExecError, Frame, Progress, Run, Scratch, ScratchOwner, ScratchRequirements, Search, + SearchError, Undo, }; use perex::input::Position; use perex::span::Span; @@ -382,11 +382,46 @@ fn find_near_lent<'mem, S: ImmutableSubject>( undo: &mut cell.undo[..], }; poll_on_stride(poll)?; - let mut search = match near { - Some(near) => Search::new_near(resources, start, near, scratch, *budget), - None => Search::new(resources, start, scratch, *budget), - } - .map_err(search_error)?; + // Both views are acquired once for the quantum that decides nearly + // every per-call search; a `Search` is built and moved only if this + // one pauses or asks for more scratch. + // On an error that is not a capacity request -- WorkLimit, + // InvalidProgram, ChangedResources, Cancelled -- `run` returns Err and + // drops the scratch and the remaining budget, where `new` + `advance` + // left a Search to read `remaining_work()` from. So `*budget` keeps its + // entry value and under-counts what the failed call spent. WORK is + // usize::MAX on every path here, so nothing observes it today; if a + // finite execution budget is ever reintroduced (see #10164/#10165), + // this stops being free and wants the engine's failure arm to report + // remaining work. + let mut search = match Search::run(resources, start, near, scratch, *budget, quantum) + .map_err(search_error)? + { + Run::Finished(mut finished) => { + *budget = Budget::new(finished.remaining_work()); + let position = finished.position(); + if !finished.matched() { + return Ok(Lent::Done(None, position)); + } + let full = finished + .capture(0) + .map_err(EngineError::Execution)? + .ok_or(EngineError::Execution(ExecError::InvalidProgram))?; + let captures = match mode { + CaptureMode::Full => None, + CaptureMode::All => { + poll()?; + let mut output = Slots::new(memory, finished.capture_count())?; + finished + .copy_captures(&mut output) + .map_err(EngineError::Execution)?; + Some(output) + } + }; + return Ok(Lent::Done(Some(Match { full, captures }), position)); + } + Run::Paused(search) => search, + }; loop { let result = search.advance(quantum); *budget = Budget::new(search.remaining_work()); @@ -482,11 +517,43 @@ pub(crate) fn find_near<'mem, S: ImmutableSubject>( poll()?; let buffers = MatchBuffers::new(memory, size)?; - let mut search = match near { - Some(near) => Search::new_near(&resources, start, near, buffers, *budget), - None => Search::new(&resources, start, buffers, *budget), - } - .map_err(search_error)?; + // On an error that is not a capacity request -- WorkLimit, + // InvalidProgram, ChangedResources, Cancelled -- `run` returns Err and + // drops the scratch and the remaining budget, where `new` + `advance` + // left a Search to read `remaining_work()` from. So `*budget` keeps its + // entry value and under-counts what the failed call spent. WORK is + // usize::MAX on every path here, so nothing observes it today; if a + // finite execution budget is ever reintroduced (see #10164/#10165), + // this stops being free and wants the engine's failure arm to report + // remaining work. + let mut search = match Search::run(&resources, start, near, buffers, *budget, quantum) + .map_err(search_error)? + { + Run::Finished(mut finished) => { + *budget = Budget::new(finished.remaining_work()); + let position = finished.position(); + if !finished.matched() { + return Ok((None, position)); + } + let full = finished + .capture(0) + .map_err(EngineError::Execution)? + .ok_or(EngineError::Execution(ExecError::InvalidProgram))?; + let captures = match mode { + CaptureMode::Full => None, + CaptureMode::All => { + poll()?; + let mut output = Slots::new(memory, finished.capture_count())?; + finished + .copy_captures(&mut output) + .map_err(EngineError::Execution)?; + Some(output) + } + }; + return Ok((Some(Match { full, captures }), position)); + } + Run::Paused(search) => search, + }; loop { let result = search.advance(quantum); // Preserve consumed work even when the following poll cancels/throws,