Recycle value stacks to avoid allocation costs (#184)
This commit is contained in:
parent
2520bfc5a8
commit
284c907b29
34
src/func.rs
34
src/func.rs
|
@ -6,7 +6,7 @@ use host::Externals;
|
|||
use isa;
|
||||
use module::ModuleInstance;
|
||||
use parity_wasm::elements::Local;
|
||||
use runner::{check_function_args, Interpreter, InterpreterState};
|
||||
use runner::{check_function_args, Interpreter, InterpreterState, StackRecycler};
|
||||
use types::ValueType;
|
||||
use value::RuntimeValue;
|
||||
use {Signature, Trap};
|
||||
|
@ -140,7 +140,7 @@ impl FuncInstance {
|
|||
check_function_args(func.signature(), &args)?;
|
||||
match *func.as_internal() {
|
||||
FuncInstanceInternal::Internal { .. } => {
|
||||
let mut interpreter = Interpreter::new(func, args)?;
|
||||
let mut interpreter = Interpreter::new(func, args, None)?;
|
||||
interpreter.start_execution(externals)
|
||||
}
|
||||
FuncInstanceInternal::Host {
|
||||
|
@ -150,6 +150,34 @@ impl FuncInstance {
|
|||
}
|
||||
}
|
||||
|
||||
/// Invoke this function using recycled stacks.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Same as [`invoke`].
|
||||
///
|
||||
/// [`invoke`]: #method.invoke
|
||||
pub fn invoke_with_stack<E: Externals>(
|
||||
func: &FuncRef,
|
||||
args: &[RuntimeValue],
|
||||
externals: &mut E,
|
||||
stack_recycler: &mut StackRecycler,
|
||||
) -> Result<Option<RuntimeValue>, Trap> {
|
||||
check_function_args(func.signature(), &args)?;
|
||||
match *func.as_internal() {
|
||||
FuncInstanceInternal::Internal { .. } => {
|
||||
let mut interpreter = Interpreter::new(func, args, Some(stack_recycler))?;
|
||||
let return_value = interpreter.start_execution(externals);
|
||||
stack_recycler.recycle(interpreter);
|
||||
return_value
|
||||
}
|
||||
FuncInstanceInternal::Host {
|
||||
ref host_func_index,
|
||||
..
|
||||
} => externals.invoke_index(*host_func_index, args.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Invoke the function, get a resumable handle. This handle can then be used to [`start_execution`]. If a
|
||||
/// Host trap happens, caller can use [`resume_execution`] to feed the expected return value back in, and then
|
||||
/// continue the execution.
|
||||
|
@ -171,7 +199,7 @@ impl FuncInstance {
|
|||
check_function_args(func.signature(), &args)?;
|
||||
match *func.as_internal() {
|
||||
FuncInstanceInternal::Internal { .. } => {
|
||||
let interpreter = Interpreter::new(func, args)?;
|
||||
let interpreter = Interpreter::new(func, args, None)?;
|
||||
Ok(FuncInvocation {
|
||||
kind: FuncInvocationKind::Internal(interpreter),
|
||||
})
|
||||
|
|
|
@ -404,6 +404,7 @@ pub use self::host::{Externals, HostError, NopExternals, RuntimeArgs};
|
|||
pub use self::imports::{ImportResolver, ImportsBuilder, ModuleImportResolver};
|
||||
pub use self::memory::{MemoryInstance, MemoryRef, LINEAR_MEMORY_PAGE_SIZE};
|
||||
pub use self::module::{ExternVal, ModuleInstance, ModuleRef, NotStartedModuleRef};
|
||||
pub use self::runner::{StackRecycler, DEFAULT_CALL_STACK_LIMIT, DEFAULT_VALUE_STACK_LIMIT};
|
||||
pub use self::table::{TableInstance, TableRef};
|
||||
pub use self::types::{GlobalDescriptor, MemoryDescriptor, Signature, TableDescriptor, ValueType};
|
||||
pub use self::value::{Error as ValueError, FromRuntimeValue, LittleEndianConvert, RuntimeValue};
|
||||
|
|
|
@ -18,6 +18,7 @@ use imports::ImportResolver;
|
|||
use memory::MemoryRef;
|
||||
use memory_units::Pages;
|
||||
use parity_wasm::elements::{External, InitExpr, Instruction, Internal, ResizableLimits, Type};
|
||||
use runner::StackRecycler;
|
||||
use table::TableRef;
|
||||
use types::{GlobalDescriptor, MemoryDescriptor, TableDescriptor};
|
||||
use validation::{DEFAULT_MEMORY_INDEX, DEFAULT_TABLE_INDEX};
|
||||
|
@ -625,21 +626,43 @@ impl ModuleInstance {
|
|||
args: &[RuntimeValue],
|
||||
externals: &mut E,
|
||||
) -> Result<Option<RuntimeValue>, Error> {
|
||||
let func_instance = self.func_by_name(func_name)?;
|
||||
|
||||
FuncInstance::invoke(&func_instance, args, externals).map_err(|t| Error::Trap(t))
|
||||
}
|
||||
|
||||
/// Invoke exported function by a name using recycled stacks.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Same as [`invoke_export`].
|
||||
///
|
||||
/// [`invoke_export`]: #method.invoke_export
|
||||
pub fn invoke_export_with_stack<E: Externals>(
|
||||
&self,
|
||||
func_name: &str,
|
||||
args: &[RuntimeValue],
|
||||
externals: &mut E,
|
||||
stack_recycler: &mut StackRecycler,
|
||||
) -> Result<Option<RuntimeValue>, Error> {
|
||||
let func_instance = self.func_by_name(func_name)?;
|
||||
|
||||
FuncInstance::invoke_with_stack(&func_instance, args, externals, stack_recycler)
|
||||
.map_err(|t| Error::Trap(t))
|
||||
}
|
||||
|
||||
fn func_by_name(&self, func_name: &str) -> Result<FuncRef, Error> {
|
||||
let extern_val = self
|
||||
.export_by_name(func_name)
|
||||
.ok_or_else(|| Error::Function(format!("Module doesn't have export {}", func_name)))?;
|
||||
|
||||
let func_instance = match extern_val {
|
||||
ExternVal::Func(func_instance) => func_instance,
|
||||
unexpected => {
|
||||
return Err(Error::Function(format!(
|
||||
"Export {} is not a function, but {:?}",
|
||||
func_name, unexpected
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
FuncInstance::invoke(&func_instance, args, externals).map_err(|t| Error::Trap(t))
|
||||
match extern_val {
|
||||
ExternVal::Func(func_instance) => Ok(func_instance),
|
||||
unexpected => Err(Error::Function(format!(
|
||||
"Export {} is not a function, but {:?}",
|
||||
func_name, unexpected
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Find export by a name.
|
||||
|
|
126
src/runner.rs
126
src/runner.rs
|
@ -18,10 +18,10 @@ use value::{
|
|||
};
|
||||
use {Signature, Trap, TrapKind, ValueType};
|
||||
|
||||
/// Maximum number of entries in value stack.
|
||||
pub const DEFAULT_VALUE_STACK_LIMIT: usize = (1024 * 1024) / ::core::mem::size_of::<RuntimeValue>();
|
||||
/// Maximum number of bytes on the value stack.
|
||||
pub const DEFAULT_VALUE_STACK_LIMIT: usize = 1024 * 1024;
|
||||
|
||||
// TODO: Make these parameters changeble.
|
||||
/// Maximum number of levels on the call stack.
|
||||
pub const DEFAULT_CALL_STACK_LIMIT: usize = 64 * 1024;
|
||||
|
||||
/// This is a wrapper around u64 to allow us to treat runtime values as a tag-free `u64`
|
||||
|
@ -166,14 +166,18 @@ enum RunResult {
|
|||
/// Function interpreter.
|
||||
pub struct Interpreter {
|
||||
value_stack: ValueStack,
|
||||
call_stack: Vec<FunctionContext>,
|
||||
call_stack: CallStack,
|
||||
return_type: Option<ValueType>,
|
||||
state: InterpreterState,
|
||||
}
|
||||
|
||||
impl Interpreter {
|
||||
pub fn new(func: &FuncRef, args: &[RuntimeValue]) -> Result<Interpreter, Trap> {
|
||||
let mut value_stack = ValueStack::with_limit(DEFAULT_VALUE_STACK_LIMIT);
|
||||
pub fn new(
|
||||
func: &FuncRef,
|
||||
args: &[RuntimeValue],
|
||||
mut stack_recycler: Option<&mut StackRecycler>,
|
||||
) -> Result<Interpreter, Trap> {
|
||||
let mut value_stack = StackRecycler::recreate_value_stack(&mut stack_recycler);
|
||||
for &arg in args {
|
||||
let arg = arg.into();
|
||||
value_stack.push(arg).map_err(
|
||||
|
@ -183,7 +187,7 @@ impl Interpreter {
|
|||
)?;
|
||||
}
|
||||
|
||||
let mut call_stack = Vec::new();
|
||||
let mut call_stack = StackRecycler::recreate_call_stack(&mut stack_recycler);
|
||||
let initial_frame = FunctionContext::new(func.clone());
|
||||
call_stack.push(initial_frame);
|
||||
|
||||
|
@ -278,14 +282,14 @@ impl Interpreter {
|
|||
|
||||
match function_return {
|
||||
RunResult::Return => {
|
||||
if self.call_stack.last().is_none() {
|
||||
if self.call_stack.is_empty() {
|
||||
// This was the last frame in the call stack. This means we
|
||||
// are done executing.
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
RunResult::NestedCall(nested_func) => {
|
||||
if self.call_stack.len() + 1 >= DEFAULT_CALL_STACK_LIMIT {
|
||||
if self.call_stack.is_full() {
|
||||
return Err(TrapKind::StackOverflow.into());
|
||||
}
|
||||
|
||||
|
@ -1363,16 +1367,6 @@ struct ValueStack {
|
|||
}
|
||||
|
||||
impl ValueStack {
|
||||
fn with_limit(limit: usize) -> ValueStack {
|
||||
let mut buf = Vec::new();
|
||||
buf.resize(limit, RuntimeValueInternal(0));
|
||||
|
||||
ValueStack {
|
||||
buf: buf.into_boxed_slice(),
|
||||
sp: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn drop_keep(&mut self, drop_keep: isa::DropKeep) {
|
||||
if drop_keep.keep == isa::Keep::Single {
|
||||
|
@ -1454,3 +1448,97 @@ impl ValueStack {
|
|||
self.sp
|
||||
}
|
||||
}
|
||||
|
||||
struct CallStack {
|
||||
buf: Vec<FunctionContext>,
|
||||
limit: usize,
|
||||
}
|
||||
|
||||
impl CallStack {
|
||||
fn push(&mut self, ctx: FunctionContext) {
|
||||
self.buf.push(ctx);
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> Option<FunctionContext> {
|
||||
self.buf.pop()
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.buf.is_empty()
|
||||
}
|
||||
|
||||
fn is_full(&self) -> bool {
|
||||
self.buf.len() + 1 >= self.limit
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to recycle stacks instead of allocating them repeatedly.
|
||||
pub struct StackRecycler {
|
||||
value_stack_buf: Option<Box<[RuntimeValueInternal]>>,
|
||||
value_stack_limit: usize,
|
||||
call_stack_buf: Option<Vec<FunctionContext>>,
|
||||
call_stack_limit: usize,
|
||||
}
|
||||
|
||||
impl StackRecycler {
|
||||
/// Limit stacks created by this recycler to
|
||||
/// - `value_stack_limit` bytes for values and
|
||||
/// - `call_stack_limit` levels for calls.
|
||||
pub fn with_limits(value_stack_limit: usize, call_stack_limit: usize) -> Self {
|
||||
Self {
|
||||
value_stack_buf: None,
|
||||
value_stack_limit,
|
||||
call_stack_buf: None,
|
||||
call_stack_limit,
|
||||
}
|
||||
}
|
||||
|
||||
fn recreate_value_stack(this: &mut Option<&mut Self>) -> ValueStack {
|
||||
let limit = this
|
||||
.as_ref()
|
||||
.map_or(DEFAULT_VALUE_STACK_LIMIT, |this| this.value_stack_limit)
|
||||
/ ::core::mem::size_of::<RuntimeValueInternal>();
|
||||
|
||||
let buf = this
|
||||
.as_mut()
|
||||
.and_then(|this| this.value_stack_buf.take())
|
||||
.unwrap_or_else(|| {
|
||||
let mut buf = Vec::new();
|
||||
buf.reserve_exact(limit);
|
||||
buf.resize(limit, RuntimeValueInternal(0));
|
||||
buf.into_boxed_slice()
|
||||
});
|
||||
|
||||
ValueStack { buf, sp: 0 }
|
||||
}
|
||||
|
||||
fn recreate_call_stack(this: &mut Option<&mut Self>) -> CallStack {
|
||||
let limit = this
|
||||
.as_ref()
|
||||
.map_or(DEFAULT_CALL_STACK_LIMIT, |this| this.call_stack_limit);
|
||||
|
||||
let buf = this
|
||||
.as_mut()
|
||||
.and_then(|this| this.call_stack_buf.take())
|
||||
.unwrap_or_default();
|
||||
|
||||
CallStack { buf, limit }
|
||||
}
|
||||
|
||||
pub(crate) fn recycle(&mut self, mut interpreter: Interpreter) {
|
||||
for cell in interpreter.value_stack.buf.iter_mut() {
|
||||
*cell = RuntimeValueInternal(0);
|
||||
}
|
||||
|
||||
interpreter.call_stack.buf.clear();
|
||||
|
||||
self.value_stack_buf = Some(interpreter.value_stack.buf);
|
||||
self.call_stack_buf = Some(interpreter.call_stack.buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StackRecycler {
|
||||
fn default() -> Self {
|
||||
Self::with_limits(DEFAULT_VALUE_STACK_LIMIT, DEFAULT_CALL_STACK_LIMIT)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue