wasm_interp: Make ValueStack a dumb Vec<Value> => Zig test 146.3->124.6ms

This commit is contained in:
Brian Carroll 2022-12-07 08:41:27 +00:00
parent 1f90286d64
commit 8c7d9dbff8
No known key found for this signature in database
GPG key ID: 5C7B2EC4101703C0
5 changed files with 156 additions and 304 deletions

View file

@ -6,7 +6,11 @@ set -euxo pipefail
# Test failures will always point at the _start function
# Make sure to look at the rest of the stack trace!
# Zig will try to run the test binary it produced, but it is a wasm object and hence your OS won't
# know how to run it. In the error message, it prints the binary it tried to run. We use some fun
# unix tools to get that path, then feed it to wasmer
zig test -target wasm32-wasi-musl -O ReleaseFast src/main.zig --test-cmd wasmer --test-cmd-bin
# Zig will try to run the test binary it produced, but since your OS doesn't know how to
# run Wasm binaries natively, we need to provide a Wasm interpreter as a "test command".
zig test -target wasm32-wasi-musl -O ReleaseFast src/main.zig --test-cmd ~/roc/target/release/roc_wasm_interp --test-cmd-bin
hyperfine --warmup 1 \
'zig test -target wasm32-wasi-musl -O ReleaseFast src/main.zig --test-cmd wasmer --test-cmd-bin' \
'zig test -target wasm32-wasi-musl -O ReleaseFast src/main.zig --test-cmd wasm3 --test-cmd-bin' \
'zig test -target wasm32-wasi-musl -O ReleaseFast src/main.zig --test-cmd ~/roc/target/release/roc_wasm_interp --test-cmd-bin'

View file

@ -197,93 +197,93 @@ impl<'a> CallStack<'a> {
pc: usize,
buffer: &mut String,
) -> fmt::Result {
let divider = "-------------------";
writeln!(buffer, "{}", divider)?;
// let divider = "-------------------";
// writeln!(buffer, "{}", divider)?;
let mut value_stack_iter = value_stack.iter();
// let mut value_stack_iter = value_stack.iter();
for frame in 0..self.frame_offsets.len() {
let next_frame = frame + 1;
let op_offset = if next_frame < self.frame_offsets.len() {
// return address of next frame = next op in this frame
let next_op = self.return_addrs_and_block_depths[next_frame].0 as usize;
// Call address is more intuitive than the return address when debugging. Search backward for it.
// Skip last byte of function index to avoid a false match with CALL/CALLINDIRECT.
// The more significant bytes won't match because of LEB-128 encoding.
let mut call_op = next_op - 2;
loop {
let byte = module.code.bytes[call_op];
if byte == OpCode::CALL as u8 || byte == OpCode::CALLINDIRECT as u8 {
break;
} else {
call_op -= 1;
}
}
call_op
} else {
pc
};
// for frame in 0..self.frame_offsets.len() {
// let next_frame = frame + 1;
// let op_offset = if next_frame < self.frame_offsets.len() {
// // return address of next frame = next op in this frame
// let next_op = self.return_addrs_and_block_depths[next_frame].0 as usize;
// // Call address is more intuitive than the return address when debugging. Search backward for it.
// // Skip last byte of function index to avoid a false match with CALL/CALLINDIRECT.
// // The more significant bytes won't match because of LEB-128 encoding.
// let mut call_op = next_op - 2;
// loop {
// let byte = module.code.bytes[call_op];
// if byte == OpCode::CALL as u8 || byte == OpCode::CALLINDIRECT as u8 {
// break;
// } else {
// call_op -= 1;
// }
// }
// call_op
// } else {
// pc
// };
let fn_index = pc_to_fn_index(op_offset, module);
let address = op_offset + module.code.section_offset as usize;
writeln!(buffer, "function {}", fn_index)?;
writeln!(buffer, " address {:06x}", address)?; // format matches wasm-objdump, for easy search
// let fn_index = pc_to_fn_index(op_offset, module);
// let address = op_offset + module.code.section_offset as usize;
// writeln!(buffer, "function {}", fn_index)?;
// writeln!(buffer, " address {:06x}", address)?; // format matches wasm-objdump, for easy search
write!(buffer, " args ")?;
let arg_count = {
let n_import_fns = module.import.imports.len();
let signature_index = if fn_index < n_import_fns {
match module.import.imports[fn_index].description {
ImportDesc::Func { signature_index } => signature_index,
_ => unreachable!(),
}
} else {
module.function.signatures[fn_index - n_import_fns]
};
module.types.look_up_arg_type_bytes(signature_index).len()
};
let args_and_locals_count = {
let frame_offset = self.frame_offsets[frame] as usize;
let next_frame_offset = if frame == self.frame_offsets.len() - 1 {
self.locals_data.len()
} else {
self.frame_offsets[frame + 1] as usize
};
next_frame_offset - frame_offset
};
for index in 0..args_and_locals_count {
let value = self.get_local_help(frame, index as u32);
if index != 0 {
write!(buffer, ", ")?;
}
if index == arg_count {
write!(buffer, "\n locals ")?;
}
write!(buffer, "{}: {:?}", index, value)?;
}
write!(buffer, "\n stack [")?;
// write!(buffer, " args ")?;
// let arg_count = {
// let n_import_fns = module.import.imports.len();
// let signature_index = if fn_index < n_import_fns {
// match module.import.imports[fn_index].description {
// ImportDesc::Func { signature_index } => signature_index,
// _ => unreachable!(),
// }
// } else {
// module.function.signatures[fn_index - n_import_fns]
// };
// module.types.look_up_arg_type_bytes(signature_index).len()
// };
// let args_and_locals_count = {
// let frame_offset = self.frame_offsets[frame] as usize;
// let next_frame_offset = if frame == self.frame_offsets.len() - 1 {
// self.locals_data.len()
// } else {
// self.frame_offsets[frame + 1] as usize
// };
// next_frame_offset - frame_offset
// };
// for index in 0..args_and_locals_count {
// let value = self.get_local_help(frame, index as u32);
// if index != 0 {
// write!(buffer, ", ")?;
// }
// if index == arg_count {
// write!(buffer, "\n locals ")?;
// }
// write!(buffer, "{}: {:?}", index, value)?;
// }
// write!(buffer, "\n stack [")?;
let frame_value_count = {
let value_stack_base = self.value_stack_bases[frame];
let next_value_stack_base = if frame == self.frame_offsets.len() - 1 {
value_stack.len() as u32
} else {
self.value_stack_bases[frame + 1]
};
next_value_stack_base - value_stack_base
};
for i in 0..frame_value_count {
if i != 0 {
write!(buffer, ", ")?;
}
if let Some(value) = value_stack_iter.next() {
write!(buffer, "{:?}", value)?;
}
}
// let frame_value_count = {
// let value_stack_base = self.value_stack_bases[frame];
// let next_value_stack_base = if frame == self.frame_offsets.len() - 1 {
// value_stack.len() as u32
// } else {
// self.value_stack_bases[frame + 1]
// };
// next_value_stack_base - value_stack_base
// };
// for i in 0..frame_value_count {
// if i != 0 {
// write!(buffer, ", ")?;
// }
// if let Some(value) = value_stack_iter.next() {
// write!(buffer, "{:?}", value)?;
// }
// }
writeln!(buffer, "]")?;
writeln!(buffer, "{}", divider)?;
}
// writeln!(buffer, "]")?;
// writeln!(buffer, "{}", divider)?;
// }
Ok(())
}

View file

@ -10,7 +10,7 @@ use roc_wasm_module::{Value, ValueType};
use crate::call_stack::CallStack;
use crate::value_stack::ValueStack;
use crate::{pc_to_fn_index, Error, ImportDispatcher};
use crate::{Error, ImportDispatcher};
pub enum Action {
Continue,
@ -451,7 +451,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
}
let mut action = Action::Continue;
let mut implicit_return = false;
// let mut implicit_return = false;
match op_code {
UNREACHABLE => {
@ -503,7 +503,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
if self.block_loop_addrs.len() == self.outermost_block as usize {
// implicit RETURN at end of function
action = self.do_return();
implicit_return = true;
// implicit_return = true;
} else {
self.block_loop_addrs.pop().unwrap();
}
@ -1504,17 +1504,17 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
}
}
if let Some(debug_string) = &self.debug_string {
let base = self.call_stack.value_stack_base();
let slice = self.value_stack.get_slice(base as usize);
eprintln!("{:06x} {:17} {:?}", file_offset, debug_string, slice);
if op_code == RETURN || (op_code == END && implicit_return) {
let fn_index = pc_to_fn_index(self.program_counter, module);
eprintln!("returning to function {}\n", fn_index);
} else if op_code == CALL || op_code == CALLINDIRECT {
eprintln!();
}
}
// if let Some(debug_string) = &self.debug_string {
// let base = self.call_stack.value_stack_base();
// let slice = self.value_stack.get_slice(base as usize);
// eprintln!("{:06x} {:17} {:?}", file_offset, debug_string, slice);
// if op_code == RETURN || (op_code == END && implicit_return) {
// let fn_index = pc_to_fn_index(self.program_counter, module);
// eprintln!("returning to function {}\n", fn_index);
// } else if op_code == CALL || op_code == CALLINDIRECT {
// eprintln!();
// }
// }
Ok(action)
}

View file

@ -94,23 +94,26 @@ fn main() -> io::Result<()> {
});
// Run
// let end= 10000;
let end = 1;
for _ in 0..end {
let result = inst.call_export_from_cli(&module, start_fn_name, &wasi_argv);
let result = inst.call_export_from_cli(&module, start_fn_name, &wasi_argv);
// Print out return value, if any
// Print out return value, if any
match result {
Ok(Some(val)) => {
if is_hex_format {
println!("{:#x?}", val)
} else {
println!("{:?}", val)
match result {
Ok(Some(val)) => {
if is_hex_format {
println!("{:#x?}", val)
} else {
println!("{:?}", val)
}
}
Ok(None) => {}
Err(e) => {
eprintln!("{}", e);
process::exit(3);
}
}
Ok(None) => {}
Err(e) => {
eprintln!("{}", e);
process::exit(3);
}
}

View file

@ -1,251 +1,96 @@
use bitvec::vec::BitVec;
use bumpalo::{collections::Vec, Bump};
use roc_wasm_module::{Value, ValueType};
use std::{fmt::Debug, mem::size_of};
use std::fmt::Debug;
use crate::Error;
/// Memory-efficient Struct-of-Arrays storage for the value stack.
/// Pack the values and their types as densely as possible,
/// to get better cache usage, at the expense of some extra logic.
// Very simple and easy-to-debug storage for the Wasm stack machine
// It wastes a lot of memory but we tried more complex schemes with packed bytes
// and it made no measurable difference to performance.
pub struct ValueStack<'a> {
bytes: Vec<'a, u8>,
is_float: BitVec,
is_64: BitVec,
}
macro_rules! pop_bytes {
($ty: ty, $bytes: expr) => {{
const SIZE: usize = size_of::<$ty>();
if $bytes.len() < SIZE {
Err(Error::ValueStackEmpty)
} else {
let bytes_idx = $bytes.len() - SIZE;
let mut b = [0; SIZE];
b.copy_from_slice(&$bytes[bytes_idx..][..SIZE]);
$bytes.truncate(bytes_idx);
Ok(<$ty>::from_ne_bytes(b))
}
}};
values: Vec<'a, Value>,
}
impl<'a> ValueStack<'a> {
pub(crate) fn new(arena: &'a Bump) -> Self {
ValueStack {
bytes: Vec::with_capacity_in(1024, arena),
is_float: BitVec::with_capacity(1024),
is_64: BitVec::with_capacity(1024),
values: Vec::with_capacity_in(1024, arena),
}
}
pub(crate) fn len(&self) -> usize {
self.is_64.len()
self.values.len()
}
pub(crate) fn is_empty(&self) -> bool {
self.is_64.is_empty()
self.values.is_empty()
}
pub(crate) fn push(&mut self, value: Value) {
match value {
Value::I32(x) => {
self.bytes.extend_from_slice(&x.to_ne_bytes());
self.is_float.push(false);
self.is_64.push(false);
}
Value::I64(x) => {
self.bytes.extend_from_slice(&x.to_ne_bytes());
self.is_float.push(false);
self.is_64.push(true);
}
Value::F32(x) => {
self.bytes.extend_from_slice(&x.to_ne_bytes());
self.is_float.push(true);
self.is_64.push(false);
}
Value::F64(x) => {
self.bytes.extend_from_slice(&x.to_ne_bytes());
self.is_float.push(true);
self.is_64.push(true);
}
}
self.values.push(value);
}
pub(crate) fn pop(&mut self) -> Value {
let is_64 = self.is_64.pop().unwrap();
let is_float = self.is_float.pop().unwrap();
let size = if is_64 { 8 } else { 4 };
let bytes_idx = self.bytes.len() - size;
let value = self.get(is_64, is_float, bytes_idx);
self.bytes.truncate(bytes_idx);
value
self.values.pop().unwrap()
}
pub(crate) fn peek(&self) -> Value {
let is_64 = *self.is_64.last().unwrap();
let is_float = *self.is_float.last().unwrap();
let size = if is_64 { 8 } else { 4 };
let bytes_idx = self.bytes.len() - size;
self.get(is_64, is_float, bytes_idx)
}
fn get(&self, is_64: bool, is_float: bool, bytes_idx: usize) -> Value {
if is_64 {
let mut b = [0; 8];
b.copy_from_slice(&self.bytes[bytes_idx..][..8]);
if is_float {
Value::F64(f64::from_ne_bytes(b))
} else {
Value::I64(i64::from_ne_bytes(b))
}
} else {
let mut b = [0; 4];
b.copy_from_slice(&self.bytes[bytes_idx..][..4]);
if is_float {
Value::F32(f32::from_ne_bytes(b))
} else {
Value::I32(i32::from_ne_bytes(b))
}
}
*self.values.last().unwrap()
}
/// Memory addresses etc
pub(crate) fn pop_u32(&mut self) -> Result<u32, Error> {
match (self.is_float.pop(), self.is_64.pop()) {
(Some(false), Some(false)) => pop_bytes!(u32, self.bytes),
(Some(is_float), Some(is_64)) => {
Err(Error::value_stack_type(ValueType::I32, is_float, is_64))
}
_ => Err(Error::ValueStackEmpty),
match self.values.pop() {
Some(Value::I32(x)) => Ok(u32::from_ne_bytes(x.to_ne_bytes())),
Some(bad) => Err(Error::ValueStackType(ValueType::I32, ValueType::from(bad))),
None => Err(Error::ValueStackEmpty),
}
}
pub(crate) fn pop_i32(&mut self) -> Result<i32, Error> {
match (self.is_float.pop(), self.is_64.pop()) {
(Some(false), Some(false)) => pop_bytes!(i32, self.bytes),
(Some(is_float), Some(is_64)) => {
Err(Error::value_stack_type(ValueType::I32, is_float, is_64))
}
_ => Err(Error::ValueStackEmpty),
match self.values.pop() {
Some(Value::I32(x)) => Ok(x),
Some(bad) => Err(Error::ValueStackType(ValueType::I32, ValueType::from(bad))),
None => Err(Error::ValueStackEmpty),
}
}
pub(crate) fn pop_u64(&mut self) -> Result<u64, Error> {
match (self.is_float.pop(), self.is_64.pop()) {
(Some(false), Some(true)) => pop_bytes!(u64, self.bytes),
(Some(is_float), Some(is_64)) => {
Err(Error::value_stack_type(ValueType::I64, is_float, is_64))
}
_ => Err(Error::ValueStackEmpty),
match self.values.pop() {
Some(Value::I64(x)) => Ok(u64::from_ne_bytes(x.to_ne_bytes())),
Some(bad) => Err(Error::ValueStackType(ValueType::I64, ValueType::from(bad))),
None => Err(Error::ValueStackEmpty),
}
}
pub(crate) fn pop_i64(&mut self) -> Result<i64, Error> {
match (self.is_float.pop(), self.is_64.pop()) {
(Some(false), Some(true)) => pop_bytes!(i64, self.bytes),
(Some(is_float), Some(is_64)) => {
Err(Error::value_stack_type(ValueType::I64, is_float, is_64))
}
_ => Err(Error::ValueStackEmpty),
match self.values.pop() {
Some(Value::I64(x)) => Ok(x),
Some(bad) => Err(Error::ValueStackType(ValueType::I64, ValueType::from(bad))),
None => Err(Error::ValueStackEmpty),
}
}
pub(crate) fn pop_f32(&mut self) -> Result<f32, Error> {
match (self.is_float.pop(), self.is_64.pop()) {
(Some(true), Some(false)) => pop_bytes!(f32, self.bytes),
(Some(is_float), Some(is_64)) => {
Err(Error::value_stack_type(ValueType::F32, is_float, is_64))
}
_ => Err(Error::ValueStackEmpty),
match self.values.pop() {
Some(Value::F32(x)) => Ok(x),
Some(bad) => Err(Error::ValueStackType(ValueType::F32, ValueType::from(bad))),
None => Err(Error::ValueStackEmpty),
}
}
pub(crate) fn pop_f64(&mut self) -> Result<f64, Error> {
match (self.is_float.pop(), self.is_64.pop()) {
(Some(true), Some(true)) => pop_bytes!(f64, self.bytes),
(Some(is_float), Some(is_64)) => {
Err(Error::value_stack_type(ValueType::F64, is_float, is_64))
}
_ => Err(Error::ValueStackEmpty),
}
}
fn fmt_from_index(
&self,
f: &mut std::fmt::Formatter<'_>,
from_index: usize,
) -> std::fmt::Result {
write!(f, "[")?;
let mut bytes_index = 0;
assert_eq!(self.is_64.len(), self.is_float.len());
if from_index < self.is_64.len() {
let iter_64 = self.is_64.iter().by_vals();
let iter_float = self.is_float.iter().by_vals();
for (i, (is_64, is_float)) in iter_64.zip(iter_float).enumerate() {
if i < from_index {
continue;
}
let value = self.get(is_64, is_float, bytes_index);
bytes_index += if is_64 { 8 } else { 4 };
value.fmt(f)?;
if i < self.is_64.len() - 1 {
write!(f, ", ")?;
}
}
}
write!(f, "]")
}
pub(crate) fn get_slice<'b>(&'b self, index: usize) -> ValueStackSlice<'a, 'b> {
ValueStackSlice { stack: self, index }
}
pub(crate) fn iter<'b>(&'b self) -> ValueStackIter<'a, 'b> {
ValueStackIter {
stack: self,
index: 0,
bytes_index: 0,
match self.values.pop() {
Some(Value::F64(x)) => Ok(x),
Some(bad) => Err(Error::ValueStackType(ValueType::F64, ValueType::from(bad))),
None => Err(Error::ValueStackEmpty),
}
}
}
impl Debug for ValueStack<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.fmt_from_index(f, 0)
}
}
pub struct ValueStackSlice<'a, 'b> {
stack: &'b ValueStack<'a>,
index: usize,
}
impl Debug for ValueStackSlice<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.stack.fmt_from_index(f, self.index)
}
}
pub struct ValueStackIter<'a, 'b> {
stack: &'b ValueStack<'a>,
index: usize,
bytes_index: usize,
}
impl Iterator for ValueStackIter<'_, '_> {
type Item = Value;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.stack.is_64.len() {
None
} else {
let is_64 = self.stack.is_64[self.index];
let is_float = self.stack.is_float[self.index];
let value = self.stack.get(is_64, is_float, self.bytes_index);
self.index += 1;
self.bytes_index += if is_64 { 8 } else { 4 };
Some(value)
}
write!(f, "{:?}", &self.values)
}
}