Skip to content

perf(vm): borrow callee name in vec dispatch instead of allocating 🪢#155

Merged
timfennis merged 1 commit into
masterfrom
perf/vec-callee-name-no-alloc
May 24, 2026
Merged

perf(vm): borrow callee name in vec dispatch instead of allocating 🪢#155
timfennis merged 1 commit into
masterfrom
perf/vec-callee-name-no-alloc

Conversation

@timfennis
Copy link
Copy Markdown
Owner

Summary

dispatch_vec_call and dispatch_vec_call_dynamic both eagerly built an Option<String> for the callee's name on every call, then only read it from rarely-taken error branches (the overload-not-found Err and the call_callback map_err closure). The success path threw the String away. Same shape of bug as the GetIterator fix in #147.

Changes

  • dispatch_vec_call: borrow &str directly from scalars.first().and_then(|f| f.name()). The slice is a caller-owned parameter, so the borrow lifetime is independent of &mut self and the map_err closure can capture it freely.
  • dispatch_vec_call_dynamic: resolve the first vec candidate once into a held Rc<Object>, then borrow &str out of it. The resolve_var call already happened inside the old callee_name(); the .to_string() is what's gone.
  • Vm::callee_name() itself is kept — it's still used from the regular Call opcode's "no function found" error path, where the allocation is fine because we're already on an error path.

Caveat — perf impact is barely measurable

vec_hot_loop (200k–2M (int,int) + (int,int) calls):

Iters Baseline This PR
200k 39.0 ± 3.4 ms 38.6 ± 3.1 ms
2M 336.2 ± 3.8 ms 334.5 ± 4.1 ms

≈1.01× — within noise. perf confirms ~13% of total time goes to malloc/free, but the eliminated allocation is one small String (operator name like "+") per outer vec call, dwarfed by Function::clone, the per-call Vec allocations for arg_values/elem_args/results, and the final Rc::new(Object::Tuple(...)). Unlike the GetIterator case, there's no deep recursive walk being saved here.

So this is more of a code-cleanliness/correctness fix (no wasted allocation on the hot path; &str reads more naturally than Option<String>) than a real perf win. Happy to drop it if you'd rather not carry the churn.

🤖 PR description generated by Claude.

Both `dispatch_vec_call` and `dispatch_vec_call_dynamic` eagerly built
an `Option<String>` for the callee's name on every call, but only read
it inside the overload-not-found error branch and the `map_err` closure
for `call_callback`. The success path threw it away.

Same shape as the GetIterator fix in 87e2e1b. Borrow the name as
`&str` from a stable source whose lifetime is independent of `&mut self`:
- `dispatch_vec_call`: `scalars.first().and_then(|f| f.name())` — the
  slice is caller-owned, so the &str doesn't conflict with &mut self.
- `dispatch_vec_call_dynamic`: resolve the first vec candidate once
  into a held `Rc<Object>`, then borrow its name. The resolve_var was
  already happening inside `callee_name()`; only the `.to_string()` is
  gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@timfennis timfennis merged commit b8d1c05 into master May 24, 2026
1 check passed
@timfennis timfennis deleted the perf/vec-callee-name-no-alloc branch May 24, 2026 13:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant