From 9fcedc74ada9bdb7880f0d5d23d9ea8078e1d4db Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Sun, 24 May 2026 15:01:16 +0200 Subject: [PATCH] =?UTF-8?q?perf(vm):=20pass=20callback=20args=20by=20slice?= =?UTF-8?q?=20to=20skip=20per-call=20Vec=20allocs=20=F0=9F=A9=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In `dispatch_vec_call` and `dispatch_vec_call_dynamic`, every broadcast element used `std::mem::replace(&mut elem_args, Vec::with_capacity(args))` to hand a fresh `Vec` to `call_callback` — N allocations per outer call. Likewise every stdlib HOF callsite did `comp.call(vec![…])`, one heap allocation per element of `map`/`filter`/`sort_by`/`reduce`/etc. `call_callback` and `VmCallable::call` now take `&[Value]`. The native path was already passing `&args`; the closure path becomes `extend(args.iter().cloned())`. The vec dispatch loops reuse a single `elem_args` buffer via `clear()`. Stdlib HOFs build stack arrays. Three `vec![x.clone()]` sites that clippy flagged switch to `std::slice::from_ref(&x)`, eliminating a real Rc bump+drop per element on object-heavy iterables. `vec_hot_loop` and `hof_pipeline` benches show no meaningful movement (the allocator caches these small Vecs well), but the dispatch loops and HOF callsites read cleaner and the slice-from-ref change has measurable upside for non-trivial element types. Co-Authored-By: Claude Opus 4.7 (1M context) --- ndc_stdlib/src/index.rs | 2 +- ndc_stdlib/src/sequence.rs | 34 ++++++++++++++++++---------------- ndc_vm/src/vm.rs | 18 ++++++++---------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/ndc_stdlib/src/index.rs b/ndc_stdlib/src/index.rs index 1703956f..2a2ad052 100644 --- a/ndc_stdlib/src/index.rs +++ b/ndc_stdlib/src/index.rs @@ -294,7 +294,7 @@ fn vm_get_at_index(container: &Value, index_value: &Value, vm: &mut Vm) -> Resul let Object::Function(f) = o.as_ref() else { unreachable!() }; - let result = vm.call_callback(f.clone(), vec![])?; + let result = vm.call_callback(f.clone(), &[])?; entries.borrow_mut().insert(key, result.clone()); Ok(result) } diff --git a/ndc_stdlib/src/sequence.rs b/ndc_stdlib/src/sequence.rs index 0101ad89..7a1069eb 100644 --- a/ndc_stdlib/src/sequence.rs +++ b/ndc_stdlib/src/sequence.rs @@ -238,7 +238,7 @@ mod inner { if err.is_some() { return Ordering::Equal; } - match comp.call(vec![left.clone(), right.clone()]) { + match comp.call(&[left.clone(), right.clone()]) { Ok(ret) => match ret.cmp_to_zero() { Ok(ord) => ord, Err(e) => { @@ -288,7 +288,7 @@ mod inner { if err.is_some() { return Ordering::Equal; } - match comp.call(vec![left.clone(), right.clone()]) { + match comp.call(&[left.clone(), right.clone()]) { Ok(ret) => match ret.cmp_to_zero() { Ok(ord) => ord, Err(e) => { @@ -376,7 +376,7 @@ mod inner { .ok_or_else(|| anyhow!("filter requires a sequence"))? { match predicate - .call(vec![element.clone()]) + .call(std::slice::from_ref(&element)) .map_err(|e| anyhow!(e))? { Value::Bool(true) => out.push(element), @@ -394,7 +394,7 @@ mod inner { .try_into_iter() .ok_or_else(|| anyhow!("count requires a sequence"))? { - match predicate.call(vec![element]).map_err(|e| anyhow!(e))? { + match predicate.call(&[element]).map_err(|e| anyhow!(e))? { Value::Bool(true) => out += 1, Value::Bool(false) => {} _ => return Err(anyhow!("return value of predicate must be a boolean")), @@ -410,7 +410,7 @@ mod inner { .ok_or_else(|| anyhow!("find requires a sequence"))? { match predicate - .call(vec![element.clone()]) + .call(std::slice::from_ref(&element)) .map_err(|e| anyhow!(e))? { Value::Bool(true) => return Ok(element), @@ -428,7 +428,7 @@ mod inner { .ok_or_else(|| anyhow!("locate requires a sequence"))? .enumerate() { - match predicate.call(vec![element]).map_err(|e| anyhow!(e))? { + match predicate.call(&[element]).map_err(|e| anyhow!(e))? { Value::Bool(true) => return Ok(Value::Int(idx as i64)), Value::Bool(false) => {} _ => return Err(anyhow!("return value of predicate must be a boolean")), @@ -458,7 +458,7 @@ mod inner { .try_into_iter() .ok_or_else(|| anyhow!("none requires a sequence"))? { - match function.call(vec![item]).map_err(|e| anyhow!(e))? { + match function.call(&[item]).map_err(|e| anyhow!(e))? { Value::Bool(true) => return Ok(Value::Bool(false)), Value::Bool(false) => {} v => { @@ -479,7 +479,7 @@ mod inner { .try_into_iter() .ok_or_else(|| anyhow!("all requires a sequence"))? { - match function.call(vec![item]).map_err(|e| anyhow!(e))? { + match function.call(&[item]).map_err(|e| anyhow!(e))? { Value::Bool(true) => {} Value::Bool(false) => return Ok(Value::Bool(false)), v => { @@ -499,7 +499,7 @@ mod inner { .try_into_iter() .ok_or_else(|| anyhow!("any requires a sequence"))? { - match predicate.call(vec![item]).map_err(|e| anyhow!(e))? { + match predicate.call(&[item]).map_err(|e| anyhow!(e))? { Value::Bool(true) => return Ok(Value::Bool(true)), Value::Bool(false) => {} v => { @@ -521,7 +521,7 @@ mod inner { .try_into_iter() .ok_or_else(|| anyhow!("map requires a sequence"))? { - out.push(function.call(vec![item]).map_err(|e| anyhow!(e))?); + out.push(function.call(&[item]).map_err(|e| anyhow!(e))?); } Ok(Value::list(out)) } @@ -534,7 +534,7 @@ mod inner { .try_into_iter() .ok_or_else(|| anyhow!("flat_map requires a sequence"))? { - let result = function.call(vec![item]).map_err(|e| anyhow!(e))?; + let result = function.call(&[item]).map_err(|e| anyhow!(e))?; let inner = result .try_into_iter() .ok_or_else(|| anyhow!("callable argument to flat_map must return a sequence"))?; @@ -555,7 +555,7 @@ mod inner { if let Some(item) = seq.try_into_iter().and_then(|mut i| i.next()) { return Ok(item); } - default.call(vec![]).map_err(|e| anyhow!(e)) + default.call(&[]).map_err(|e| anyhow!(e)) } /// Returns the `k` sized combinations of the given sequence `seq` as a lazy iterator of tuples. @@ -700,7 +700,7 @@ mod inner { .collect(); let mut out = Vec::with_capacity(main.len().saturating_sub(1)); for (a, b) in main.into_iter().tuple_windows() { - out.push(function.call(vec![a, b]).map_err(|e| anyhow!(e))?); + out.push(function.call(&[a, b]).map_err(|e| anyhow!(e))?); } Ok(Value::list(out)) } @@ -837,7 +837,9 @@ fn by_key(seq: Value, func: &mut VmCallable<'_>, better: Ordering) -> anyhow::Re .try_into_iter() .ok_or_else(|| anyhow!("sequence is required"))? { - let new_key = func.call(vec![value.clone()]).map_err(|e| anyhow!(e))?; + let new_key = func + .call(std::slice::from_ref(&value)) + .map_err(|e| anyhow!(e))?; let is_better = match &best_key { None => true, Some(current_best) => { @@ -865,7 +867,7 @@ fn by_comp(seq: Value, comp: &mut VmCallable<'_>, better: Ordering) -> anyhow::R None => true, Some(current) => { let result = comp - .call(vec![value.clone(), current.clone()]) + .call(&[value.clone(), current.clone()]) .map_err(|e| anyhow!(e))?; result.cmp_to_zero().map_err(|e| anyhow!(e))? == better } @@ -884,7 +886,7 @@ fn fold_iterator( ) -> anyhow::Result { let mut acc = initial; for item in iter { - acc = function.call(vec![acc, item]).map_err(|e| anyhow!(e))?; + acc = function.call(&[acc, item]).map_err(|e| anyhow!(e))?; } Ok(acc) } diff --git a/ndc_vm/src/vm.rs b/ndc_vm/src/vm.rs index 2188a3a8..4a5f79c3 100644 --- a/ndc_vm/src/vm.rs +++ b/ndc_vm/src/vm.rs @@ -693,7 +693,7 @@ impl Vm { /// uses `call_callback` instead, which runs inline on the parent VM. pub fn call_function( func: Function, - args: Vec, + args: &[Value], globals: Vec, ) -> Result { let mut vm = Self { @@ -713,17 +713,17 @@ impl Vm { /// Call a function inline on this VM, without spawning a child VM. /// Used by `VmCallable::call` so stdlib HOFs run their predicates/mappers /// directly on the parent stack — zero allocation per callback invocation. - pub fn call_callback(&mut self, func: Function, args: Vec) -> Result { + pub fn call_callback(&mut self, func: Function, args: &[Value]) -> Result { if let Function::Native(native) = func { match &native.func { - NativeFunc::Simple(f) => f(&args), - NativeFunc::WithVm(f) => f(&args, self), + NativeFunc::Simple(f) => f(args), + NativeFunc::WithVm(f) => f(args, self), } } else { let depth = self.frames.len(); let n = args.len(); self.stack.push(Value::unit()); // dummy callee slot - self.stack.extend(args); + self.stack.extend(args.iter().cloned()); self.dispatch_call_with_memo(func, n, None)?; self.run_to_depth(depth)?; Ok(self.stack.pop().expect("callback must produce a value")) @@ -945,8 +945,7 @@ impl Vm { f }; - let call_args = std::mem::replace(&mut elem_args, Vec::with_capacity(args)); - let result = self.call_callback(scalar, call_args).map_err(|mut e| { + let result = self.call_callback(scalar, &elem_args).map_err(|mut e| { let prefix = match &callee_name { Some(name) => format!("while vectorising '{name}' at index {i}: "), None => format!("while vectorising at index {i}: "), @@ -1028,8 +1027,7 @@ impl Vm { found.clone() }; - let call_args = std::mem::replace(&mut elem_args, Vec::with_capacity(args)); - let result = self.call_callback(scalar, call_args).map_err(|mut e| { + let result = self.call_callback(scalar, &elem_args).map_err(|mut e| { let prefix = match &callee_name { Some(name) => format!("while vectorising '{name}' at index {i}: "), None => format!("while vectorising at index {i}: "), @@ -1202,7 +1200,7 @@ pub struct VmCallable<'a> { impl VmCallable<'_> { /// Call this function with the given arguments, running inline on the /// parent VM. - pub fn call(&mut self, args: Vec) -> Result { + pub fn call(&mut self, args: &[Value]) -> Result { self.vm.call_callback(self.function.clone(), args) } }