-
-
Notifications
You must be signed in to change notification settings - Fork 161
perf(regex): two per-match overheads in the replace path #10605
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ### Faster | ||
|
|
||
| - `String.prototype.replace` with a function replacement spends about 27% fewer instructions, and roughly a quarter less on non-ASCII subjects. Each match used to build a JS array to carry the replacer's arguments, growing it one push at a time and then reading it straight back out into the native slots the call actually uses; the arguments now go directly into those slots, which were already the collector's roots for them. A proxy replacer, whose `apply` trap really does receive an arguments array, is unaffected (#10165). |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -130,6 +130,90 @@ pub(super) fn call( | |||||||||
| result | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /// Arguments for a replacer call, produced straight into shadow-stack slots. | ||||||||||
| /// | ||||||||||
| /// `call` builds its slots by copying a JS array the caller filled one push at | ||||||||||
| /// a time. For a replacer invoked once per match that array is pure overhead: | ||||||||||
| /// it is allocated, grown as each argument is pushed, read back out, and | ||||||||||
| /// dropped, and no user code can observe it. Here the slots are bound to the | ||||||||||
| /// shadow stack *before* any argument is produced, so a value is a traced root | ||||||||||
| /// from the moment it is written and producing the next one may allocate and | ||||||||||
| /// collect freely. The buffer is sized once and outlives the match loop. | ||||||||||
| /// | ||||||||||
| /// Not usable for a proxy replacer: `js_proxy_apply` takes the arguments as a | ||||||||||
| /// JS array, which the `apply` trap observes, so that path keeps `List`. | ||||||||||
| pub(super) struct NativeArgs { | ||||||||||
| slots: Vec<std::cell::UnsafeCell<f64>>, | ||||||||||
| } | ||||||||||
|
|
||||||||||
| impl NativeArgs { | ||||||||||
| pub(super) fn new(count: usize) -> Result<Self, EngineError> { | ||||||||||
| let mut slots = Vec::new(); | ||||||||||
| slots | ||||||||||
| .try_reserve_exact(count) | ||||||||||
| .map_err(|_| StorageError::Allocation)?; | ||||||||||
| for _ in 0..count { | ||||||||||
| slots.push(std::cell::UnsafeCell::new(f64::from_bits( | ||||||||||
| crate::value::TAG_UNDEFINED, | ||||||||||
| ))); | ||||||||||
| } | ||||||||||
| Ok(Self { slots }) | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /// `call` for an ordinary replacer, with the arguments produced by `fill` | ||||||||||
| /// directly into `args`. | ||||||||||
| /// | ||||||||||
| /// Every slot is reset to `undefined` and bound before `fill` runs, so an | ||||||||||
| /// argument `fill` does not write stays `undefined` (an unset capture), and one | ||||||||||
| /// it does write is rooted immediately. `fill` may therefore allocate between | ||||||||||
| /// arguments, which is what producing a match's capture strings does. | ||||||||||
| pub(super) fn call_native( | ||||||||||
| method: &RuntimeHandle<'_>, | ||||||||||
| receiver: &RuntimeHandle<'_>, | ||||||||||
| args: &mut NativeArgs, | ||||||||||
| memory: &MemoryBudget, | ||||||||||
| fill: impl FnOnce(&mut dyn FnMut(usize, f64)) -> Result<(), EngineError>, | ||||||||||
| ) -> Result<f64, EngineError> { | ||||||||||
| struct Frame(u64); | ||||||||||
| impl Drop for Frame { | ||||||||||
| fn drop(&mut self) { | ||||||||||
| crate::gc::js_shadow_frame_pop(self.0); | ||||||||||
| } | ||||||||||
| } | ||||||||||
| let scope = RuntimeHandleScope::new(); | ||||||||||
| let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); | ||||||||||
| let slots = &args.slots; | ||||||||||
| for slot in slots { | ||||||||||
| // SAFETY: nothing else holds a reference to these cells, and the | ||||||||||
| // shadow stack is not yet bound to them. | ||||||||||
| unsafe { *slot.get() = f64::from_bits(crate::value::TAG_UNDEFINED) }; | ||||||||||
| } | ||||||||||
| let frame = Frame(crate::gc::js_shadow_frame_push(slots.len() as u32)); | ||||||||||
| for (i, slot) in slots.iter().enumerate() { | ||||||||||
| crate::gc::js_shadow_slot_bind(i as u32, slot.get().cast()); | ||||||||||
| } | ||||||||||
| // SAFETY as above; the slots are bound, so a write publishes a root. | ||||||||||
| let mut set = |i: usize, value: f64| unsafe { *slots[i].get() = value }; | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '560,700p' crates/perry-runtime/src/gc/roots/shadow_stack.rs
rg -n 'shadow_slot_bind|slot_addr|slots\[|fn trace|shadow' crates/perry-runtime/src/gc/roots/shadow_stack.rs | head -80
sed -n '120,225p' crates/perry-runtime/src/regex/perex_replace_storage.rsRepository: PerryTS/perry Length of output: 13229 🏁 Script executed: sed -n '1,130p' crates/perry-runtime/src/gc/roots/shadow_stack.rs
sed -n '340,425p' crates/perry-runtime/src/gc/roots/shadow_stack.rs
rg -n -C 8 'visit_shadow_stack_root_slots|ShadowEntry|bound_ptr|root_shading_barrier|write_barrier_root_nanbox|js_shadow_slot_set' crates/perry-runtime crates/perry-codegenRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: sed -n '1,125p' crates/perry-runtime/src/gc/roots/shadow_stack.rs
sed -n '360,415p' crates/perry-runtime/src/gc/roots/shadow_stack.rs
rg -n 'visit_shadow_stack_root_slots' crates/perry-runtime/src
rg -n 'fn root_shading_barrier|root_shading_barrier\(' crates/perry-runtime/src/gc
rg -n 'fn js_write_barrier_root_nanbox|js_write_barrier_root_nanbox\(' crates/perry-runtime/src crates/perry-codegen/srcRepository: PerryTS/perry Length of output: 11536 🏁 Script executed: sed -n '1380,1435p' crates/perry-runtime/src/gc/roots.rs
sed -n '1925,1975p' crates/perry-runtime/src/gc/barrier/mod.rs
sed -n '400,425p' crates/perry-runtime/src/gc/roots/shadow_stack.rs
sed -n '590,665p' crates/perry-runtime/src/gc/roots/shadow_stack.rsRepository: PerryTS/perry Length of output: 7799 🏁 Script executed: rg -n -C 12 'runtime_write_barrier_root_nanbox|incremental_mark_barrier_value' crates/perry-runtime/src/gcRepository: PerryTS/perry Length of output: 50369 Publish each slot value through the shadow-slot setter.
- let mut set = |i: usize, value: f64| unsafe { *slots[i].get() = value };
+ let mut set = |i: usize, value: f64| {
+ crate::gc::js_shadow_slot_set(i as u32, value.to_bits());
+ };📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| fill(&mut set)?; | ||||||||||
| let reservation = Reservation::new( | ||||||||||
| memory, | ||||||||||
| slots.len().checked_mul(8).ok_or(StorageError::Limit)?, | ||||||||||
| )?; | ||||||||||
| let result = api::caught(|| unsafe { | ||||||||||
| crate::object::js_implicit_this_set(receiver.get_nanbox_f64()); | ||||||||||
| crate::closure::js_native_call_value( | ||||||||||
| method.get_nanbox_f64(), | ||||||||||
| slots.as_ptr().cast(), | ||||||||||
| slots.len(), | ||||||||||
| ) | ||||||||||
| }); | ||||||||||
| crate::object::js_implicit_this_set(previous.get_nanbox_f64()); | ||||||||||
| drop(reservation); | ||||||||||
| drop(frame); | ||||||||||
| result | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /// A reusable original-input reader. A read retains only Perex offsets across | ||||||||||
| /// collection, and adjacent reads do not repeat the initial Unicode seek. | ||||||||||
| pub(super) struct Units<'a, 's> { | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 9057
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 45933
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 14823
Account the reusable
NativeArgsallocation for its full lifetime.NativeArgs::newallocatesslotswithtry_reserve_exact(count), andperex_replace_direct.rsretains it across the match loop.call_nativecreates a localReservationonly afterfilland drops it after each callback.Reservation::Dropremoves the charge fromMemoryBudget, so the retainedVecis uncharged between callbacks and duringfill. This can let the operation exceed its hard scratch limit.Pass
memorytoNativeArgs::new, store aReservationinNativeArgs, initialize it withslots.capacity().checked_mul(8), and remove the per-call reservation for the same allocation.🤖 Prompt for AI Agents