Skip to content

Commit 9fcedc7

Browse files
timfennisclaude
andcommitted
perf(vm): pass callback args by slice to skip per-call Vec allocs 🩻
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<Value>` 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) <[email protected]>
1 parent 87e2e1b commit 9fcedc7

3 files changed

Lines changed: 27 additions & 27 deletions

File tree

‎ndc_stdlib/src/index.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ fn vm_get_at_index(container: &Value, index_value: &Value, vm: &mut Vm) -> Resul
294294
let Object::Function(f) = o.as_ref() else {
295295
unreachable!()
296296
};
297-
let result = vm.call_callback(f.clone(), vec![])?;
297+
let result = vm.call_callback(f.clone(), &[])?;
298298
entries.borrow_mut().insert(key, result.clone());
299299
Ok(result)
300300
}

‎ndc_stdlib/src/sequence.rs‎

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ mod inner {
238238
if err.is_some() {
239239
return Ordering::Equal;
240240
}
241-
match comp.call(vec![left.clone(), right.clone()]) {
241+
match comp.call(&[left.clone(), right.clone()]) {
242242
Ok(ret) => match ret.cmp_to_zero() {
243243
Ok(ord) => ord,
244244
Err(e) => {
@@ -288,7 +288,7 @@ mod inner {
288288
if err.is_some() {
289289
return Ordering::Equal;
290290
}
291-
match comp.call(vec![left.clone(), right.clone()]) {
291+
match comp.call(&[left.clone(), right.clone()]) {
292292
Ok(ret) => match ret.cmp_to_zero() {
293293
Ok(ord) => ord,
294294
Err(e) => {
@@ -376,7 +376,7 @@ mod inner {
376376
.ok_or_else(|| anyhow!("filter requires a sequence"))?
377377
{
378378
match predicate
379-
.call(vec![element.clone()])
379+
.call(std::slice::from_ref(&element))
380380
.map_err(|e| anyhow!(e))?
381381
{
382382
Value::Bool(true) => out.push(element),
@@ -394,7 +394,7 @@ mod inner {
394394
.try_into_iter()
395395
.ok_or_else(|| anyhow!("count requires a sequence"))?
396396
{
397-
match predicate.call(vec![element]).map_err(|e| anyhow!(e))? {
397+
match predicate.call(&[element]).map_err(|e| anyhow!(e))? {
398398
Value::Bool(true) => out += 1,
399399
Value::Bool(false) => {}
400400
_ => return Err(anyhow!("return value of predicate must be a boolean")),
@@ -410,7 +410,7 @@ mod inner {
410410
.ok_or_else(|| anyhow!("find requires a sequence"))?
411411
{
412412
match predicate
413-
.call(vec![element.clone()])
413+
.call(std::slice::from_ref(&element))
414414
.map_err(|e| anyhow!(e))?
415415
{
416416
Value::Bool(true) => return Ok(element),
@@ -428,7 +428,7 @@ mod inner {
428428
.ok_or_else(|| anyhow!("locate requires a sequence"))?
429429
.enumerate()
430430
{
431-
match predicate.call(vec![element]).map_err(|e| anyhow!(e))? {
431+
match predicate.call(&[element]).map_err(|e| anyhow!(e))? {
432432
Value::Bool(true) => return Ok(Value::Int(idx as i64)),
433433
Value::Bool(false) => {}
434434
_ => return Err(anyhow!("return value of predicate must be a boolean")),
@@ -458,7 +458,7 @@ mod inner {
458458
.try_into_iter()
459459
.ok_or_else(|| anyhow!("none requires a sequence"))?
460460
{
461-
match function.call(vec![item]).map_err(|e| anyhow!(e))? {
461+
match function.call(&[item]).map_err(|e| anyhow!(e))? {
462462
Value::Bool(true) => return Ok(Value::Bool(false)),
463463
Value::Bool(false) => {}
464464
v => {
@@ -479,7 +479,7 @@ mod inner {
479479
.try_into_iter()
480480
.ok_or_else(|| anyhow!("all requires a sequence"))?
481481
{
482-
match function.call(vec![item]).map_err(|e| anyhow!(e))? {
482+
match function.call(&[item]).map_err(|e| anyhow!(e))? {
483483
Value::Bool(true) => {}
484484
Value::Bool(false) => return Ok(Value::Bool(false)),
485485
v => {
@@ -499,7 +499,7 @@ mod inner {
499499
.try_into_iter()
500500
.ok_or_else(|| anyhow!("any requires a sequence"))?
501501
{
502-
match predicate.call(vec![item]).map_err(|e| anyhow!(e))? {
502+
match predicate.call(&[item]).map_err(|e| anyhow!(e))? {
503503
Value::Bool(true) => return Ok(Value::Bool(true)),
504504
Value::Bool(false) => {}
505505
v => {
@@ -521,7 +521,7 @@ mod inner {
521521
.try_into_iter()
522522
.ok_or_else(|| anyhow!("map requires a sequence"))?
523523
{
524-
out.push(function.call(vec![item]).map_err(|e| anyhow!(e))?);
524+
out.push(function.call(&[item]).map_err(|e| anyhow!(e))?);
525525
}
526526
Ok(Value::list(out))
527527
}
@@ -534,7 +534,7 @@ mod inner {
534534
.try_into_iter()
535535
.ok_or_else(|| anyhow!("flat_map requires a sequence"))?
536536
{
537-
let result = function.call(vec![item]).map_err(|e| anyhow!(e))?;
537+
let result = function.call(&[item]).map_err(|e| anyhow!(e))?;
538538
let inner = result
539539
.try_into_iter()
540540
.ok_or_else(|| anyhow!("callable argument to flat_map must return a sequence"))?;
@@ -555,7 +555,7 @@ mod inner {
555555
if let Some(item) = seq.try_into_iter().and_then(|mut i| i.next()) {
556556
return Ok(item);
557557
}
558-
default.call(vec![]).map_err(|e| anyhow!(e))
558+
default.call(&[]).map_err(|e| anyhow!(e))
559559
}
560560

561561
/// Returns the `k` sized combinations of the given sequence `seq` as a lazy iterator of tuples.
@@ -700,7 +700,7 @@ mod inner {
700700
.collect();
701701
let mut out = Vec::with_capacity(main.len().saturating_sub(1));
702702
for (a, b) in main.into_iter().tuple_windows() {
703-
out.push(function.call(vec![a, b]).map_err(|e| anyhow!(e))?);
703+
out.push(function.call(&[a, b]).map_err(|e| anyhow!(e))?);
704704
}
705705
Ok(Value::list(out))
706706
}
@@ -837,7 +837,9 @@ fn by_key(seq: Value, func: &mut VmCallable<'_>, better: Ordering) -> anyhow::Re
837837
.try_into_iter()
838838
.ok_or_else(|| anyhow!("sequence is required"))?
839839
{
840-
let new_key = func.call(vec![value.clone()]).map_err(|e| anyhow!(e))?;
840+
let new_key = func
841+
.call(std::slice::from_ref(&value))
842+
.map_err(|e| anyhow!(e))?;
841843
let is_better = match &best_key {
842844
None => true,
843845
Some(current_best) => {
@@ -865,7 +867,7 @@ fn by_comp(seq: Value, comp: &mut VmCallable<'_>, better: Ordering) -> anyhow::R
865867
None => true,
866868
Some(current) => {
867869
let result = comp
868-
.call(vec![value.clone(), current.clone()])
870+
.call(&[value.clone(), current.clone()])
869871
.map_err(|e| anyhow!(e))?;
870872
result.cmp_to_zero().map_err(|e| anyhow!(e))? == better
871873
}
@@ -884,7 +886,7 @@ fn fold_iterator(
884886
) -> anyhow::Result<Value> {
885887
let mut acc = initial;
886888
for item in iter {
887-
acc = function.call(vec![acc, item]).map_err(|e| anyhow!(e))?;
889+
acc = function.call(&[acc, item]).map_err(|e| anyhow!(e))?;
888890
}
889891
Ok(acc)
890892
}

‎ndc_vm/src/vm.rs‎

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ impl Vm {
693693
/// uses `call_callback` instead, which runs inline on the parent VM.
694694
pub fn call_function(
695695
func: Function,
696-
args: Vec<Value>,
696+
args: &[Value],
697697
globals: Vec<Value>,
698698
) -> Result<Value, VmError> {
699699
let mut vm = Self {
@@ -713,17 +713,17 @@ impl Vm {
713713
/// Call a function inline on this VM, without spawning a child VM.
714714
/// Used by `VmCallable::call` so stdlib HOFs run their predicates/mappers
715715
/// directly on the parent stack — zero allocation per callback invocation.
716-
pub fn call_callback(&mut self, func: Function, args: Vec<Value>) -> Result<Value, VmError> {
716+
pub fn call_callback(&mut self, func: Function, args: &[Value]) -> Result<Value, VmError> {
717717
if let Function::Native(native) = func {
718718
match &native.func {
719-
NativeFunc::Simple(f) => f(&args),
720-
NativeFunc::WithVm(f) => f(&args, self),
719+
NativeFunc::Simple(f) => f(args),
720+
NativeFunc::WithVm(f) => f(args, self),
721721
}
722722
} else {
723723
let depth = self.frames.len();
724724
let n = args.len();
725725
self.stack.push(Value::unit()); // dummy callee slot
726-
self.stack.extend(args);
726+
self.stack.extend(args.iter().cloned());
727727
self.dispatch_call_with_memo(func, n, None)?;
728728
self.run_to_depth(depth)?;
729729
Ok(self.stack.pop().expect("callback must produce a value"))
@@ -945,8 +945,7 @@ impl Vm {
945945
f
946946
};
947947

948-
let call_args = std::mem::replace(&mut elem_args, Vec::with_capacity(args));
949-
let result = self.call_callback(scalar, call_args).map_err(|mut e| {
948+
let result = self.call_callback(scalar, &elem_args).map_err(|mut e| {
950949
let prefix = match &callee_name {
951950
Some(name) => format!("while vectorising '{name}' at index {i}: "),
952951
None => format!("while vectorising at index {i}: "),
@@ -1028,8 +1027,7 @@ impl Vm {
10281027
found.clone()
10291028
};
10301029

1031-
let call_args = std::mem::replace(&mut elem_args, Vec::with_capacity(args));
1032-
let result = self.call_callback(scalar, call_args).map_err(|mut e| {
1030+
let result = self.call_callback(scalar, &elem_args).map_err(|mut e| {
10331031
let prefix = match &callee_name {
10341032
Some(name) => format!("while vectorising '{name}' at index {i}: "),
10351033
None => format!("while vectorising at index {i}: "),
@@ -1202,7 +1200,7 @@ pub struct VmCallable<'a> {
12021200
impl VmCallable<'_> {
12031201
/// Call this function with the given arguments, running inline on the
12041202
/// parent VM.
1205-
pub fn call(&mut self, args: Vec<Value>) -> Result<Value, VmError> {
1203+
pub fn call(&mut self, args: &[Value]) -> Result<Value, VmError> {
12061204
self.vm.call_callback(self.function.clone(), args)
12071205
}
12081206
}

0 commit comments

Comments
 (0)