Skip to content

Commit 22a2bb3

Browse files
authored
Re-use sp in Wasm calls (#2014)
* optimize ValueStack::grow_if_needed - this introduces a `usable` field to drop one of the two checks on the hot-path. the trade-off is that we have to keep this field in sync. however, there is only one place for that so the costs for that are okay. - also this splits `grow_if_needed` into a hot-path and cold-path: `grow_cold` - with both above improvements we can inline(always) `grow_if_needed`. * make return_call[_indirect] re-use the caller's sp we can re-use the caller's sp since the caller's call frame is replaced, thus the sp or the caller matches the sp of the callee. this allows to drop re-computation of the callee's sp on the hot-path. only if the stack needs to grow do we need to re-compute the sp. * remove ValueStack::sp_or_dangling utillity * add Stack::base_ptr * add missing commit for return call improvement * add missing usable init in ValueStack::empty * simplify computing callee's sp in nested calls the callee frame always starts at its first local variable or parameter slot. thus we can infer its sp from the caller's sp and the known parameter slot span. * use base_sp to initialize wasm calls * remove call to non-existing debug check * add note to sp method * remove debug_check_sp reference from comment
1 parent 9a0e85b commit 22a2bb3

4 files changed

Lines changed: 156 additions & 46 deletions

File tree

crates/wasmi/src/engine/executor/handler/args.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,8 @@ impl Args {
216216
params: BoundedSlotSpan,
217217
instance: Option<Inst>,
218218
) -> Control<(), Break> {
219-
(self.ip, self.sp) = utils::call_func_entry(state, self.ip, params, func, instance)?;
219+
(self.ip, self.sp) =
220+
utils::call_func_entry(state, self.ip, self.sp, params, func, instance)?;
220221
Control::Continue(())
221222
}
222223

@@ -229,7 +230,7 @@ impl Args {
229230
params: BoundedSlotSpan,
230231
instance: Option<Inst>,
231232
) -> Control<(), Break> {
232-
(self.ip, self.sp) = utils::return_call_func_entry(state, params, func, instance)?;
233+
(self.ip, self.sp) = utils::return_call_func_entry(state, self.sp, params, func, instance)?;
233234
Control::Continue(())
234235
}
235236

@@ -266,6 +267,7 @@ impl Args {
266267
) = utils::call_wasm_or_host(
267268
state,
268269
self.ip,
270+
self.sp,
269271
func,
270272
func_entity,
271273
params,
@@ -293,6 +295,7 @@ impl Args {
293295
self.instance,
294296
) = utils::return_call_wasm_or_host(
295297
state,
298+
self.sp,
296299
func,
297300
func_entity,
298301
params,

crates/wasmi/src/engine/executor/handler/func.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,10 @@ pub fn init_wasm_func_call<'a, T>(
174174
// so we simply default to 0.
175175
let callee_params = BoundedSlotSpan::new(SlotSpan::new(Slot::from(0)), 0);
176176
let instance = resolve_instance(store.prune(), &instance).into();
177+
// Note: the call stack is empty here, so the first frame starts at the value stack base.
178+
let caller_sp = stack.base_sp();
177179
let callee_sp = stack.push_frame(
180+
caller_sp,
178181
None,
179182
callee_ip,
180183
callee_params,

crates/wasmi/src/engine/executor/handler/state.rs

Lines changed: 133 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,7 @@ impl Stack {
863863
let Some((ip, start, instance)) = self.frames.restore_frame() else {
864864
panic!("restore_frame: missing top-frame")
865865
};
866-
let sp = self.values.sp_or_dangling(start);
866+
let sp = self.values.sp(start);
867867
(ip, sp, instance, self.ireg, self.freg32, self.freg64)
868868
}
869869

@@ -891,22 +891,47 @@ impl Stack {
891891
.prepare_host_frame(caller_start, callee_params, results_len)
892892
}
893893

894+
/// Returns an [`Sp`] pointing at the base of the value stack.
895+
///
896+
/// # Note
897+
///
898+
/// Only valid as the `caller_sp` of the very first frame pushed onto an empty [`Stack`].
899+
pub fn base_sp(&mut self) -> Sp {
900+
self.values.base_sp()
901+
}
902+
894903
/// Adjusts `self` for a normal function call.
904+
///
905+
/// # Note
906+
///
907+
/// `caller_sp` must be the [`Sp`] of the currently executing frame. The callee's [`Sp`]
908+
/// is derived from it by pointer arithmetic instead of being re-loaded from the frame
909+
/// table, which keeps it off the call's dependent-load chain.
895910
#[inline(always)]
911+
#[expect(clippy::too_many_arguments)]
896912
pub fn push_frame(
897913
&mut self,
914+
caller_sp: Sp,
898915
caller_ip: Option<Ip>,
899916
callee_ip: Ip,
900917
callee_params: BoundedSlotSpan,
901918
callee_locals: u16,
902919
callee_slots: u16,
903920
callee_instance: Option<Inst>,
904921
) -> Result<Sp, TrapCode> {
922+
// Note: the callee's frame starts with its first parameter and its stack pointer
923+
// can be inferred from its parameter slot span and its caller's stack pointer.
924+
let callee_sp = caller_sp.offset(callee_params.span().head());
905925
let start = self
906926
.frames
907927
.push(caller_ip, callee_ip, callee_params, callee_instance)?;
908-
self.values
909-
.push(start, callee_locals, callee_slots, callee_params.len())
928+
self.values.push(
929+
callee_sp,
930+
start,
931+
callee_locals,
932+
callee_slots,
933+
callee_params.len(),
934+
)
910935
}
911936

912937
/// Adjusts `self` after returning from a function.
@@ -926,7 +951,7 @@ impl Stack {
926951
instance: Inst,
927952
) -> Option<(Ip, Sp, Mem0Ptr, Mem0Len, Inst)> {
928953
let (ip, start, changed_instance) = self.frames.pop()?;
929-
let sp = self.values.sp_or_dangling(start);
954+
let sp = self.values.sp(start);
930955
let (mem0, mem0_len, instance) = match changed_instance {
931956
Some(instance) => {
932957
let (mem0, mem0_len) = extract_mem0(store, instance);
@@ -938,18 +963,24 @@ impl Stack {
938963
}
939964

940965
/// Adjusts `self` for a function tail call.
966+
///
967+
/// # Note
968+
///
969+
/// A tail call reuses the caller's frame, so the callee's [`Sp`] is `caller_sp` itself.
970+
/// See [`Stack::push_frame`] for why it is threaded through instead of re-loaded.
941971
#[inline(always)]
942972
pub fn replace_frame(
943973
&mut self,
974+
caller_sp: Sp,
944975
callee_ip: Ip,
945976
callee_params: BoundedSlotSpan,
946977
callee_locals: u16,
947-
callee_size: u16,
978+
callee_slots: u16,
948979
callee_instance: Option<Inst>,
949980
) -> Result<Sp, TrapCode> {
950981
let start = self.frames.replace(callee_ip, callee_instance)?;
951982
self.values
952-
.replace(start, callee_locals, callee_size, callee_params)
983+
.replace(caller_sp, start, callee_locals, callee_slots, callee_params)
953984
}
954985
}
955986

@@ -968,6 +999,14 @@ impl Stack {
968999
pub struct ValueStack {
9691000
/// The cells of the value stack.
9701001
cells: Vec<Cell>,
1002+
/// The number of cells that fit without reallocating, capped by [`Self::max_height`].
1003+
///
1004+
/// # Note
1005+
///
1006+
/// Kept in sync with `cells` so that the hot path of [`ValueStack::grow_if_needed`]
1007+
/// is a single comparison: `Vec::try_reserve` may over-allocate past `max_height`,
1008+
/// so `cells.capacity()` alone is not a sound bound to test against.
1009+
usable: usize,
9711010
/// The maximum height of the value stack.
9721011
max_height: usize,
9731012
}
@@ -981,13 +1020,19 @@ impl ValueStack {
9811020
let min_height = min_height / sizeof_cell;
9821021
let max_height = max_height / sizeof_cell;
9831022
let cells = Vec::with_capacity(min_height);
984-
Self { cells, max_height }
1023+
let usable = cmp::min(cells.capacity(), max_height);
1024+
Self {
1025+
cells,
1026+
usable,
1027+
max_height,
1028+
}
9851029
}
9861030

9871031
/// Create an empty [`ValueStack`] which uses no heap allocations.
9881032
fn empty() -> Self {
9891033
Self {
9901034
cells: Vec::new(),
1035+
usable: 0,
9911036
max_height: 0,
9921037
}
9931038
}
@@ -997,6 +1042,17 @@ impl ValueStack {
9971042
self.cells.clear();
9981043
}
9991044

1045+
/// Returns an [`Sp`] pointing at the base of the value stack.
1046+
///
1047+
/// # Note
1048+
///
1049+
/// Only used to seed the very first frame, where `start` is 0. If the buffer has
1050+
/// not been allocated yet the returned [`Sp`] is dangling, but pushing that frame
1051+
/// then reallocates and re-derives it.
1052+
fn base_sp(&mut self) -> Sp {
1053+
Sp::new(self.cells.as_mut_ptr())
1054+
}
1055+
10001056
/// Returns the number of heap allocated bytes of `self`.
10011057
///
10021058
/// # Note
@@ -1008,6 +1064,12 @@ impl ValueStack {
10081064
}
10091065

10101066
/// Returns an [`Sp`] pointing to the cell at the `start` index.
1067+
///
1068+
/// # Note
1069+
///
1070+
/// This is the single definition of a frame's [`Sp`]: every one of them equals
1071+
/// `cells.as_ptr().add(start)`, including on an empty stack where `start` is 0 and the
1072+
/// result is the (never dereferenced) base pointer.
10111073
fn sp(&mut self, start: SpOffset) -> Sp {
10121074
let offset = start.into_inner();
10131075
debug_assert!(
@@ -1023,39 +1085,56 @@ impl ValueStack {
10231085
Sp::new(value)
10241086
}
10251087

1026-
/// Returns an [`Sp`] pointing to the cell at the `start` index if `self` is non-empty.
1027-
///
1028-
/// Otherwise returns a dangling [`Sp`] that must not be dereferenced.
1029-
fn sp_or_dangling(&mut self, start: SpOffset) -> Sp {
1030-
match self.cells.is_empty() {
1031-
true => {
1032-
debug_assert_eq!(start.into_inner(), 0);
1033-
Sp::dangling()
1034-
}
1035-
false => self.sp(start),
1036-
}
1037-
}
1038-
10391088
/// Grows the number of cells to `new_len` if the current number is less than `new_len`.
10401089
///
10411090
/// Does nothing if the number of cells is already at least `new_len`.
10421091
///
1092+
/// # Returns
1093+
///
1094+
/// `true` if the underlying buffer was reallocated, which invalidates every [`Sp`]
1095+
/// derived from it before the call.
1096+
///
10431097
/// # Errors
10441098
///
10451099
/// - Returns [`TrapCode::OutOfSystemMemory`] if the machine ran out of memory.
10461100
/// - Returns [`TrapCode::StackOverflow`] if this exceeds the stack's predefined limits.
1047-
fn grow_if_needed(&mut self, new_len: SpOffset) -> Result<(), TrapCode> {
1101+
#[inline(always)]
1102+
fn grow_if_needed(&mut self, new_len: SpOffset) -> Result<bool, TrapCode> {
10481103
let new_len = new_len.into_inner();
1104+
if new_len > self.usable {
1105+
self.grow_cold(new_len)?;
1106+
return Ok(true);
1107+
}
1108+
if new_len > self.cells.len() {
1109+
// Safety: `new_len <= self.usable <= self.cells.capacity()`. There is no need to
1110+
// initialize the cells since we are operating on `Cell` which only has
1111+
// valid bit patterns.
1112+
// Note: non-security related initialization of function parameters
1113+
// and zero-initialization of function locals happens elsewhere.
1114+
unsafe { self.cells.set_len(new_len) };
1115+
}
1116+
Ok(false)
1117+
}
1118+
1119+
/// Reallocates the value stack so that it holds at least `new_len` cells.
1120+
///
1121+
/// # Note
1122+
///
1123+
/// Split out of [`ValueStack::grow_if_needed`] and marked `#[cold]` so that the
1124+
/// common no-growth path stays a single comparison and keeps its caller's [`Sp`]
1125+
/// valid in a register.
1126+
#[cold]
1127+
#[inline(never)]
1128+
fn grow_cold(&mut self, new_len: usize) -> Result<(), TrapCode> {
10491129
if new_len > self.max_height {
10501130
return Err(TrapCode::StackOverflow);
10511131
}
1052-
let capacity = self.cells.capacity();
10531132
let len = self.cells.len();
1054-
if new_len > capacity {
1055-
debug_assert!(
1056-
self.cells.len() <= self.cells.capacity(),
1057-
"capacity must always be larger or equal to the actual number of the cells"
1058-
);
1133+
debug_assert!(
1134+
len <= self.cells.capacity(),
1135+
"capacity must always be larger or equal to the actual number of the cells"
1136+
);
1137+
if new_len > self.cells.capacity() {
10591138
let additional = new_len - len;
10601139
self.cells
10611140
.try_reserve(additional)
@@ -1066,12 +1145,9 @@ impl ValueStack {
10661145
self.cells.capacity()
10671146
);
10681147
}
1069-
let max_len = cmp::max(new_len, len);
1070-
// Safety: there is no need to initialize the cells since we are operating
1071-
// on `RawVal` which only has valid bit patterns.
1072-
// Note: non-security related initialization of function parameters
1073-
// and zero-initialization of function locals happens elsewhere.
1074-
unsafe { self.cells.set_len(max_len) };
1148+
self.usable = cmp::min(self.cells.capacity(), self.max_height);
1149+
// Safety: see `grow_if_needed`.
1150+
unsafe { self.cells.set_len(cmp::max(new_len, len)) };
10751151
Ok(())
10761152
}
10771153

@@ -1171,6 +1247,7 @@ impl ValueStack {
11711247
#[inline(always)]
11721248
fn push(
11731249
&mut self,
1250+
callee_sp: Sp,
11741251
start: SpOffset,
11751252
len_local_slots: u16,
11761253
len_stack_slots: u16,
@@ -1195,40 +1272,55 @@ impl ValueStack {
11951272
let len_stack_slots = usize::from(len_stack_slots);
11961273
let len_params = usize::from(len_params);
11971274
if len_stack_slots == 0 {
1198-
return Ok(Sp::dangling());
1275+
// Note: a callee without stack slots also has no parameters, so the translator
1276+
// encodes a zero `params` head and `callee_sp == caller_sp`. Propagate it
1277+
// rather than a fresh dangling pointer, so that any call this frame goes on
1278+
// to make can still derive its callee's `Sp` from this one.
1279+
return Ok(callee_sp);
11991280
}
12001281
let end = start.add(len_stack_slots)?;
1201-
self.grow_if_needed(end)?;
1282+
// Note: `callee_sp` was derived from the caller's `Sp` register, so it only survives
1283+
// if the buffer was not reallocated. Re-deriving is confined to the cold path.
1284+
let sp = match self.grow_if_needed(end)? {
1285+
true => self.sp(start),
1286+
false => callee_sp,
1287+
};
12021288
let start_locals = start.into_inner().wrapping_add(len_params);
12031289
let end_locals = start.into_inner().wrapping_add(len_local_slots);
12041290
let Some(local_cells) = self.cells.get_mut(start_locals..end_locals) else {
12051291
unsafe { unreachable_unchecked!() }
12061292
};
12071293
local_cells.fill_with(Cell::default);
1208-
let sp = self.sp(start);
12091294
Ok(sp)
12101295
}
12111296

12121297
/// Adjusts `self` for a function tail call.
12131298
#[inline(always)]
12141299
fn replace(
12151300
&mut self,
1301+
callee_sp: Sp,
12161302
callee_start: SpOffset,
12171303
callee_locals: u16,
1218-
callee_size: u16,
1304+
callee_slots: u16,
12191305
callee_params: BoundedSlotSpan,
12201306
) -> Result<Sp, TrapCode> {
1221-
debug_assert!(callee_locals <= callee_size);
1307+
debug_assert!(callee_locals <= callee_slots);
12221308
let callee_locals = usize::from(callee_locals);
1223-
let callee_size = usize::from(callee_size);
1309+
let callee_size = usize::from(callee_slots);
12241310
let params_len = usize::from(callee_params.len());
12251311
let params_start = usize::from(u16::from(callee_params.span().head()));
12261312
let params_end = params_start.wrapping_add(params_len);
12271313
if callee_size == 0 {
1228-
return Ok(Sp::dangling());
1314+
// Note: see `ValueStack::push` — the frame is reused, so this is the caller's `Sp`.
1315+
return Ok(callee_sp);
12291316
}
12301317
let callee_end = callee_start.add(callee_size)?;
1231-
self.grow_if_needed(callee_end)?;
1318+
// Note: a tail call reuses the caller's frame, so `callee_sp` is the caller's `Sp`
1319+
// unchanged and only needs re-deriving if the buffer was reallocated.
1320+
let sp = match self.grow_if_needed(callee_end)? {
1321+
true => self.sp(callee_start),
1322+
false => callee_sp,
1323+
};
12321324
let Some(callee_cells) = self.cells_from(callee_start) else {
12331325
unsafe { unreachable_unchecked!("ValueStack::replace: out of bounds callee cells") }
12341326
};
@@ -1237,7 +1329,6 @@ impl ValueStack {
12371329
unsafe { unreachable_unchecked!() }
12381330
};
12391331
local_cells.fill_with(Cell::default);
1240-
let sp = self.sp(callee_start);
12411332
Ok(sp)
12421333
}
12431334

0 commit comments

Comments
 (0)