Skip to content

Commit 6dbbada

Browse files
Robbepopathei
andauthored
Add fuel consumption modes (#706)
* add fuel consumption mode to Config * make Config::wasm_features crate private This API was never intened to be public. * add doc note to Config::fuel_consumption_mode * implement both fuel consumption modes in executor * fix internal doc link * add tests for fuel consumption modes * Update crates/wasmi/src/engine/config.rs Co-authored-by: Alexander Theißen <[email protected]> * try fix performance regressions * add some inline annotations in executor * deduplicate some code * Revert "deduplicate some code" This reverts commit 165d94d. * try to fix perf regressions (take 2) * remove some inline annotations * refactor code * refactor code (split into more funcs) * apply #[cold] attribute * Revert "apply #[cold] attribute" This reverts commit 13e017e. * try fix regression (take 3) * replace inlines * remove cold attribute again * replace some inline(always) with inline * Revert "replace some inline(always) with inline" This reverts commit 482abc1. * add cold attribute again ... * Revert "add cold attribute again ..." This reverts commit 925cc22. * put inline on all UntypedValue public methods * Revert "put inline on all UntypedValue public methods" This reverts commit df19822. * put inline(always) on ret method * refactor code * apply rustfmt * try to fix Wasm regressions * Revert "try to fix Wasm regressions" This reverts commit 267666f. * deduplicate some control flow code in executor * apply rustfmt * use Default impl * try to mutate inline annotations a bit * adjust inline * use inline * remove one inline * next try * go back to stage 1 ... --------- Co-authored-by: Alexander Theißen <[email protected]>
1 parent cf7736f commit 6dbbada

File tree

6 files changed

+275
-66
lines changed

6 files changed

+275
-66
lines changed

crates/wasmi/src/engine/config.rs

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,60 @@ pub struct Config {
3333
floats: bool,
3434
/// Is `true` if `wasmi` executions shall consume fuel.
3535
consume_fuel: bool,
36+
/// The fuel consumption mode of the `wasmi` [`Engine`](crate::Engine).
37+
fuel_consumption_mode: FuelConsumptionMode,
3638
/// The configured fuel costs of all `wasmi` bytecode instructions.
3739
fuel_costs: FuelCosts,
3840
}
3941

42+
/// The fuel consumption mode of the `wasmi` [`Engine`].
43+
///
44+
/// This mode affects when fuel is charged for Wasm bulk-operations.
45+
/// Affected Wasm instructions are:
46+
///
47+
/// - `memory.{grow, copy, fill}`
48+
/// - `data.init`
49+
/// - `table.{grow, copy, fill}`
50+
/// - `element.init`
51+
///
52+
/// The default fuel consumption mode is [`FuelConsumptionMode::Lazy`].
53+
///
54+
/// [`Engine`]: crate::Engine
55+
#[derive(Debug, Default, Copy, Clone)]
56+
pub enum FuelConsumptionMode {
57+
/// Fuel consumption for bulk-operations is lazy.
58+
///
59+
/// Lazy fuel consumption means that fuel for bulk-operations
60+
/// is checked before executing the instruction but only consumed
61+
/// if the executed instruction suceeded. The reason for this is
62+
/// that bulk-operations fail fast and therefore do not cost
63+
/// a lot of compute power in case of failure.
64+
///
65+
/// # Note
66+
///
67+
/// Lazy fuel consumption makes sense as default mode since the
68+
/// affected bulk-operations usually are very costly if they are
69+
/// successful. Therefore users generally want to avoid having to
70+
/// using more fuel than what was actually used, especially if there
71+
/// is an underlying cost model associated to the used fuel.
72+
#[default]
73+
Lazy,
74+
/// Fuel consumption for bulk-operations is eager.
75+
///
76+
/// Eager fuel consumption means that fuel for bulk-operations
77+
/// is always consumed before executing the instruction independent
78+
/// of it suceeding or failing.
79+
///
80+
/// # Note
81+
///
82+
/// A use case for when a user might prefer eager fuel consumption
83+
/// is when the fuel **required** to perform an execution should be identical
84+
/// to the actual fuel **consumed** by an execution. Otherwise it can be confusing
85+
/// that the execution consumed `x` gas while it needs `x + gas_for_bulk_op` to
86+
/// not run out of fuel.
87+
Eager,
88+
}
89+
4090
/// Type storing all kinds of fuel costs of instructions.
4191
#[derive(Debug, Copy, Clone)]
4292
pub struct FuelCosts {
@@ -154,6 +204,7 @@ impl Default for Config {
154204
floats: true,
155205
consume_fuel: false,
156206
fuel_costs: FuelCosts::default(),
207+
fuel_consumption_mode: FuelConsumptionMode::default(),
157208
}
158209
}
159210
}
@@ -312,8 +363,30 @@ impl Config {
312363
&self.fuel_costs
313364
}
314365

366+
/// Configures the [`FuelConsumptionMode`] for the [`Engine`].
367+
///
368+
/// # Note
369+
///
370+
/// This has no effect if fuel metering is disabled for the [`Engine`].
371+
///
372+
/// [`Engine`]: crate::Engine
373+
pub fn fuel_consumption_mode(&mut self, mode: FuelConsumptionMode) -> &mut Self {
374+
self.fuel_consumption_mode = mode;
375+
self
376+
}
377+
378+
/// Returns the [`FuelConsumptionMode`] for the [`Engine`].
379+
///
380+
/// Returns `None` if fuel metering is disabled for the [`Engine`].
381+
///
382+
/// [`Engine`]: crate::Engine
383+
pub(crate) fn get_fuel_consumption_mode(&self) -> Option<FuelConsumptionMode> {
384+
self.get_consume_fuel()
385+
.then_some(self.fuel_consumption_mode)
386+
}
387+
315388
/// Returns the [`WasmFeatures`] represented by the [`Config`].
316-
pub fn wasm_features(&self) -> WasmFeatures {
389+
pub(crate) fn wasm_features(&self) -> WasmFeatures {
317390
WasmFeatures {
318391
multi_value: self.multi_value,
319392
mutable_global: self.mutable_global,

crates/wasmi/src/engine/executor.rs

Lines changed: 106 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::{
2323
},
2424
func::FuncEntity,
2525
table::TableEntity,
26+
FuelConsumptionMode,
2627
Func,
2728
FuncRef,
2829
Instance,
@@ -164,6 +165,21 @@ struct Executor<'ctx, 'engine> {
164165
code_map: &'engine CodeMap,
165166
}
166167

168+
macro_rules! forward_call {
169+
($expr:expr) => {{
170+
if let CallOutcome::Call {
171+
host_func,
172+
instance,
173+
} = $expr?
174+
{
175+
return Ok(WasmOutcome::Call {
176+
host_func,
177+
instance,
178+
});
179+
}
180+
}};
181+
}
182+
167183
impl<'ctx, 'engine> Executor<'ctx, 'engine> {
168184
/// Creates a new [`Executor`] for executing a `wasmi` function frame.
169185
#[inline(always)]
@@ -214,56 +230,18 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
214230
}
215231
}
216232
Instr::ReturnCall { drop_keep, func } => {
217-
if let CallOutcome::Call {
218-
host_func,
219-
instance,
220-
} = self.visit_return_call(drop_keep, func)?
221-
{
222-
return Ok(WasmOutcome::Call {
223-
host_func,
224-
instance,
225-
});
226-
}
233+
forward_call!(self.visit_return_call(drop_keep, func))
227234
}
228235
Instr::ReturnCallIndirect {
229236
drop_keep,
230237
table,
231238
func_type,
232239
} => {
233-
if let CallOutcome::Call {
234-
host_func,
235-
instance,
236-
} = self.visit_return_call_indirect(drop_keep, table, func_type)?
237-
{
238-
return Ok(WasmOutcome::Call {
239-
host_func,
240-
instance,
241-
});
242-
}
243-
}
244-
Instr::Call(func) => {
245-
if let CallOutcome::Call {
246-
host_func,
247-
instance,
248-
} = self.visit_call(func)?
249-
{
250-
return Ok(WasmOutcome::Call {
251-
host_func,
252-
instance,
253-
});
254-
}
240+
forward_call!(self.visit_return_call_indirect(drop_keep, table, func_type))
255241
}
242+
Instr::Call(func) => forward_call!(self.visit_call(func)),
256243
Instr::CallIndirect { table, func_type } => {
257-
if let CallOutcome::Call {
258-
host_func,
259-
instance,
260-
} = self.visit_call_indirect(table, func_type)?
261-
{
262-
return Ok(WasmOutcome::Call {
263-
host_func,
264-
instance,
265-
});
266-
}
244+
forward_call!(self.visit_call_indirect(table, func_type))
267245
}
268246
Instr::Drop => self.visit_drop(),
269247
Instr::Select => self.visit_select(),
@@ -599,7 +577,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
599577
///
600578
/// This also modifies the stack as the caller would expect it
601579
/// and synchronizes the execution state with the outer structures.
602-
#[inline]
580+
#[inline(always)]
603581
fn ret(&mut self, drop_keep: DropKeep) -> ReturnOutcome {
604582
self.sp.drop_keep(drop_keep);
605583
self.sync_stack_ptr();
@@ -622,49 +600,113 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
622600
/// for amount of required fuel determined by `delta` or if
623601
/// fuel metering is disabled.
624602
///
625-
/// Only if `exec` runs successfully and fuel metering
626-
/// is enabled the fuel determined by `delta` is charged.
627-
///
628603
/// # Errors
629604
///
630605
/// - If the [`StoreInner`] ran out of fuel.
631606
/// - If the `exec` closure traps.
632-
fn consume_fuel_on_success<T, E>(
607+
#[inline(always)]
608+
fn consume_fuel_with<T, E>(
633609
&mut self,
634610
delta: impl FnOnce(&FuelCosts) -> u64,
635611
exec: impl FnOnce(&mut Self) -> Result<T, E>,
636612
) -> Result<T, E>
637613
where
638614
E: From<TrapCode>,
639615
{
640-
if !self.is_fuel_metering_enabled() {
641-
return exec(self);
616+
match self.get_fuel_consumption_mode() {
617+
None => exec(self),
618+
Some(mode) => self.consume_fuel_with_mode(mode, delta, exec),
642619
}
643-
// At this point we know that fuel metering is enabled.
620+
}
621+
622+
/// Consume an amount of fuel specified by `delta` and executes `exec`.
623+
///
624+
/// The `mode` determines when and if the fuel determined by `delta` is charged.
625+
///
626+
/// # Errors
627+
///
628+
/// - If the [`StoreInner`] ran out of fuel.
629+
/// - If the `exec` closure traps.
630+
#[inline(always)]
631+
fn consume_fuel_with_mode<T, E>(
632+
&mut self,
633+
mode: FuelConsumptionMode,
634+
delta: impl FnOnce(&FuelCosts) -> u64,
635+
exec: impl FnOnce(&mut Self) -> Result<T, E>,
636+
) -> Result<T, E>
637+
where
638+
E: From<TrapCode>,
639+
{
644640
let delta = delta(self.fuel_costs());
641+
match mode {
642+
FuelConsumptionMode::Lazy => self.consume_fuel_with_lazy(delta, exec),
643+
FuelConsumptionMode::Eager => self.consume_fuel_with_eager(delta, exec),
644+
}
645+
}
646+
647+
/// Consume an amount of fuel specified by `delta` if `exec` succeeds.
648+
///
649+
/// Prior to executing `exec` it is checked if enough fuel is remaining
650+
/// determined by `delta`. The fuel is charged only after `exec` has been
651+
/// finished successfully.
652+
///
653+
/// # Errors
654+
///
655+
/// - If the [`StoreInner`] ran out of fuel.
656+
/// - If the `exec` closure traps.
657+
#[inline(always)]
658+
fn consume_fuel_with_lazy<T, E>(
659+
&mut self,
660+
delta: u64,
661+
exec: impl FnOnce(&mut Self) -> Result<T, E>,
662+
) -> Result<T, E>
663+
where
664+
E: From<TrapCode>,
665+
{
645666
self.ctx.fuel().sufficient_fuel(delta)?;
646667
let result = exec(self)?;
647668
self.ctx
648669
.fuel_mut()
649670
.consume_fuel(delta)
650-
.unwrap_or_else(|error| {
651-
panic!("remaining fuel has already been approved prior but encountered: {error}")
652-
});
671+
.expect("remaining fuel has already been approved prior");
653672
Ok(result)
654673
}
655674

656-
/// Returns `true` if fuel metering is enabled.
657-
fn is_fuel_metering_enabled(&self) -> bool {
658-
self.ctx.engine().config().get_consume_fuel()
675+
/// Consume an amount of fuel specified by `delta` and executes `exec`.
676+
///
677+
/// # Errors
678+
///
679+
/// - If the [`StoreInner`] ran out of fuel.
680+
/// - If the `exec` closure traps.
681+
#[inline(always)]
682+
fn consume_fuel_with_eager<T, E>(
683+
&mut self,
684+
delta: u64,
685+
exec: impl FnOnce(&mut Self) -> Result<T, E>,
686+
) -> Result<T, E>
687+
where
688+
E: From<TrapCode>,
689+
{
690+
self.ctx.fuel_mut().consume_fuel(delta)?;
691+
exec(self)
659692
}
660693

661694
/// Returns a shared reference to the [`FuelCosts`] of the [`Engine`].
662695
///
663696
/// [`Engine`]: crate::Engine
697+
#[inline]
664698
fn fuel_costs(&self) -> &FuelCosts {
665699
self.ctx.engine().config().fuel_costs()
666700
}
667701

702+
/// Returns the [`FuelConsumptionMode`] of the [`Engine`].
703+
///
704+
/// [`Engine`]: crate::Engine
705+
#[inline]
706+
fn get_fuel_consumption_mode(&self) -> Option<FuelConsumptionMode> {
707+
self.ctx.engine().config().get_fuel_consumption_mode()
708+
}
709+
668710
/// Executes a `call` or `return_call` instruction.
669711
#[inline(always)]
670712
fn execute_call(
@@ -892,7 +934,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
892934
return self.try_next_instr();
893935
}
894936
};
895-
let result = self.consume_fuel_on_success(
937+
let result = self.consume_fuel_with(
896938
|costs| {
897939
let delta_in_bytes = delta.to_bytes().unwrap_or(0) as u64;
898940
costs.fuel_for_bytes(delta_in_bytes)
@@ -928,7 +970,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
928970
let n = i32::from(n) as usize;
929971
let offset = i32::from(d) as usize;
930972
let byte = u8::from(val);
931-
self.consume_fuel_on_success(
973+
self.consume_fuel_with(
932974
|costs| costs.fuel_for_bytes(n as u64),
933975
|this| {
934976
let memory = this
@@ -951,7 +993,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
951993
let n = i32::from(n) as usize;
952994
let src_offset = i32::from(s) as usize;
953995
let dst_offset = i32::from(d) as usize;
954-
self.consume_fuel_on_success(
996+
self.consume_fuel_with(
955997
|costs| costs.fuel_for_bytes(n as u64),
956998
|this| {
957999
let data = this.cache.default_memory_bytes(this.ctx);
@@ -976,7 +1018,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
9761018
let n = i32::from(n) as usize;
9771019
let src_offset = i32::from(s) as usize;
9781020
let dst_offset = i32::from(d) as usize;
979-
self.consume_fuel_on_success(
1021+
self.consume_fuel_with(
9801022
|costs| costs.fuel_for_bytes(n as u64),
9811023
|this| {
9821024
let (memory, data) = this
@@ -1018,7 +1060,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
10181060
fn visit_table_grow(&mut self, table_index: TableIdx) -> Result<(), TrapCode> {
10191061
let (init, delta) = self.sp.pop2();
10201062
let delta: u32 = delta.into();
1021-
let result = self.consume_fuel_on_success(
1063+
let result = self.consume_fuel_with(
10221064
|costs| costs.fuel_for_elements(u64::from(delta)),
10231065
|this| {
10241066
let table = this.cache.get_table(this.ctx, table_index);
@@ -1043,7 +1085,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
10431085
let (i, val, n) = self.sp.pop3();
10441086
let dst: u32 = i.into();
10451087
let len: u32 = n.into();
1046-
self.consume_fuel_on_success(
1088+
self.consume_fuel_with(
10471089
|costs| costs.fuel_for_elements(u64::from(len)),
10481090
|this| {
10491091
let table = this.cache.get_table(this.ctx, table_index);
@@ -1088,7 +1130,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
10881130
let len = u32::from(n);
10891131
let src_index = u32::from(s);
10901132
let dst_index = u32::from(d);
1091-
self.consume_fuel_on_success(
1133+
self.consume_fuel_with(
10921134
|costs| costs.fuel_for_elements(u64::from(len)),
10931135
|this| {
10941136
// Query both tables and check if they are the same:
@@ -1120,7 +1162,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
11201162
let len = u32::from(n);
11211163
let src_index = u32::from(s);
11221164
let dst_index = u32::from(d);
1123-
self.consume_fuel_on_success(
1165+
self.consume_fuel_with(
11241166
|costs| costs.fuel_for_elements(u64::from(len)),
11251167
|this| {
11261168
let (instance, table, element) = this

0 commit comments

Comments
 (0)