components: Improve heuristic for splitting adapters (#4827)
This commit is a (second?) attempt at improving the generation of adapter modules to avoid excessively large functions for fuzz-generated inputs. The first iteration of adapters simply translated an entire type inline per-function. This proved problematic however since the size of the adapter function was on the order of the overall size of a type, which can be exponential for a type that is otherwise defined in linear size. The second iteration of adapters performed a split where memory-based types would always be translated with individual functions. The theory here was that once a type was memory-based it was large enough to not warrant inline translation in the original function and a separate outlined function could be shared and otherwise used to deduplicate portions of the original giant function. This again proved problematic, however, since the splitting heuristic was quite naive and didn't take into account large stack-based types. This third iteration in this commit replaces the previous system with a similar but slightly more general one. Each adapter function now has a concept of fuel which is decremented each time a layer of a type is translated. When fuel runs out further translations are deferred to outlined functions. The fuel counter should hopefully provide a sort of reasonable upper bound on the size of a function and the outlined functions should ideally provide the ability to be called from multiple places and therefore deduplicate what would otherwise be a massive function. This final iteration is another attempt at guaranteeing that an adapter module is linear in size with respect to the input type section of the original module. Additionally this iteration uniformly handles stack and memory-based translations which means that stack-based translations can't go wild in their function size and memory-based translations may benefit slightly from having at least a little bit of inlining internally. The immediate impact of this is that the `component_api` fuzzer seems to be running at a faster rate than before. Otherwise #4825 is sufficient to invalidate preexisting fuzz-bugs and this PR is hopefully the final nail in the coffin to prevent further timeouts for small inputs cropping up. Closes #4816
This commit is contained in:
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
use crate::component::dfg::CoreDef;
|
use crate::component::dfg::CoreDef;
|
||||||
use crate::component::{
|
use crate::component::{
|
||||||
Adapter, AdapterOptions as AdapterOptionsDfg, ComponentTypesBuilder, InterfaceType,
|
Adapter, AdapterOptions as AdapterOptionsDfg, ComponentTypesBuilder, FlatType, InterfaceType,
|
||||||
StringEncoding, TypeFuncIndex,
|
StringEncoding, TypeFuncIndex,
|
||||||
};
|
};
|
||||||
use crate::fact::transcode::Transcoder;
|
use crate::fact::transcode::Transcoder;
|
||||||
@@ -65,8 +65,8 @@ pub struct Module<'a> {
|
|||||||
imported_globals: PrimaryMap<GlobalIndex, CoreDef>,
|
imported_globals: PrimaryMap<GlobalIndex, CoreDef>,
|
||||||
|
|
||||||
funcs: PrimaryMap<FunctionId, Function>,
|
funcs: PrimaryMap<FunctionId, Function>,
|
||||||
translate_mem_funcs: HashMap<(InterfaceType, InterfaceType, Options, Options), FunctionId>,
|
helper_funcs: HashMap<Helper, FunctionId>,
|
||||||
translate_mem_worklist: Vec<(FunctionId, InterfaceType, InterfaceType, Options, Options)>,
|
helper_worklist: Vec<(FunctionId, Helper)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AdapterData {
|
struct AdapterData {
|
||||||
@@ -123,6 +123,43 @@ enum Context {
|
|||||||
Lower,
|
Lower,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Representation of a "helper function" which may be generated as part of
|
||||||
|
/// generating an adapter trampoline.
|
||||||
|
///
|
||||||
|
/// Helper functions are created when inlining the translation for a type in its
|
||||||
|
/// entirety would make a function excessively large. This is currently done via
|
||||||
|
/// a simple fuel/cost heuristic based on the type being translated but may get
|
||||||
|
/// fancier over time.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
struct Helper {
|
||||||
|
/// Metadata about the source type of what's being translated.
|
||||||
|
src: HelperType,
|
||||||
|
/// Metadata about the destination type which is being translated to.
|
||||||
|
dst: HelperType,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about a source or destination type in a `Helper` which is
|
||||||
|
/// generated.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
struct HelperType {
|
||||||
|
/// The concrete type being translated.
|
||||||
|
ty: InterfaceType,
|
||||||
|
/// The configuration options (memory, etc) for the adapter.
|
||||||
|
opts: Options,
|
||||||
|
/// Where the type is located (either the stack or in memory)
|
||||||
|
loc: HelperLocation,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where a `HelperType` is located, dictating the signature of the helper
|
||||||
|
/// function.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
enum HelperLocation {
|
||||||
|
/// Located on the stack in wasm locals.
|
||||||
|
Stack,
|
||||||
|
/// Located in linear memory as configured by `opts`.
|
||||||
|
Memory,
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> Module<'a> {
|
impl<'a> Module<'a> {
|
||||||
/// Creates an empty module.
|
/// Creates an empty module.
|
||||||
pub fn new(types: &'a ComponentTypesBuilder, debug: bool) -> Module<'a> {
|
pub fn new(types: &'a ComponentTypesBuilder, debug: bool) -> Module<'a> {
|
||||||
@@ -138,8 +175,8 @@ impl<'a> Module<'a> {
|
|||||||
imported_memories: PrimaryMap::new(),
|
imported_memories: PrimaryMap::new(),
|
||||||
imported_globals: PrimaryMap::new(),
|
imported_globals: PrimaryMap::new(),
|
||||||
funcs: PrimaryMap::new(),
|
funcs: PrimaryMap::new(),
|
||||||
translate_mem_funcs: HashMap::new(),
|
helper_funcs: HashMap::new(),
|
||||||
translate_mem_worklist: Vec::new(),
|
helper_worklist: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,8 +225,8 @@ impl<'a> Module<'a> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
while let Some((result, src, dst, src_opts, dst_opts)) = self.translate_mem_worklist.pop() {
|
while let Some((result, helper)) = self.helper_worklist.pop() {
|
||||||
trampoline::compile_translate_mem(self, result, src, &src_opts, dst, &dst_opts);
|
trampoline::compile_helper(self, result, helper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,27 +358,15 @@ impl<'a> Module<'a> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn translate_mem(
|
fn translate_helper(&mut self, helper: Helper) -> FunctionId {
|
||||||
&mut self,
|
*self.helper_funcs.entry(helper).or_insert_with(|| {
|
||||||
src: InterfaceType,
|
// Generate a fresh `Function` with a unique id for what we're about to
|
||||||
src_opts: &Options,
|
// generate.
|
||||||
dst: InterfaceType,
|
let ty = helper.core_type(self.types, &mut self.core_types);
|
||||||
dst_opts: &Options,
|
let id = self.funcs.push(Function::new(None, ty));
|
||||||
) -> FunctionId {
|
self.helper_worklist.push((id, helper));
|
||||||
*self
|
id
|
||||||
.translate_mem_funcs
|
})
|
||||||
.entry((src, dst, *src_opts, *dst_opts))
|
|
||||||
.or_insert_with(|| {
|
|
||||||
// Generate a fresh `Function` with a unique id for what we're about to
|
|
||||||
// generate.
|
|
||||||
let ty = self
|
|
||||||
.core_types
|
|
||||||
.function(&[src_opts.ptr(), dst_opts.ptr()], &[]);
|
|
||||||
let id = self.funcs.push(Function::new(None, ty));
|
|
||||||
self.translate_mem_worklist
|
|
||||||
.push((id, src, dst, *src_opts, *dst_opts));
|
|
||||||
id
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encodes this module into a WebAssembly binary.
|
/// Encodes this module into a WebAssembly binary.
|
||||||
@@ -462,6 +487,19 @@ impl Options {
|
|||||||
4
|
4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flat_types<'a>(
|
||||||
|
&self,
|
||||||
|
ty: &InterfaceType,
|
||||||
|
types: &'a ComponentTypesBuilder,
|
||||||
|
) -> Option<&'a [FlatType]> {
|
||||||
|
let flat = types.flat_types(ty)?;
|
||||||
|
Some(if self.memory64 {
|
||||||
|
flat.memory64
|
||||||
|
} else {
|
||||||
|
flat.memory32
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Temporary index which is not the same as `FuncIndex`.
|
/// Temporary index which is not the same as `FuncIndex`.
|
||||||
@@ -542,3 +580,43 @@ impl Function {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Helper {
|
||||||
|
fn core_type(
|
||||||
|
&self,
|
||||||
|
types: &ComponentTypesBuilder,
|
||||||
|
core_types: &mut core_types::CoreTypes,
|
||||||
|
) -> u32 {
|
||||||
|
let mut params = Vec::new();
|
||||||
|
let mut results = Vec::new();
|
||||||
|
// The source type being translated is always pushed onto the
|
||||||
|
// parameters first, either a pointer for memory or its flat
|
||||||
|
// representation.
|
||||||
|
self.src.push_flat(&mut params, types);
|
||||||
|
|
||||||
|
// The destination type goes into the parameter list if it's from
|
||||||
|
// memory or otherwise is the result of the function itself for a
|
||||||
|
// stack-based representation.
|
||||||
|
match self.dst.loc {
|
||||||
|
HelperLocation::Stack => self.dst.push_flat(&mut results, types),
|
||||||
|
HelperLocation::Memory => params.push(self.dst.opts.ptr()),
|
||||||
|
}
|
||||||
|
|
||||||
|
core_types.function(¶ms, &results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HelperType {
|
||||||
|
fn push_flat(&self, dst: &mut Vec<ValType>, types: &ComponentTypesBuilder) {
|
||||||
|
match self.loc {
|
||||||
|
HelperLocation::Stack => {
|
||||||
|
for ty in self.opts.flat_types(&self.ty, types).unwrap() {
|
||||||
|
dst.push((*ty).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HelperLocation::Memory => {
|
||||||
|
dst.push(self.opts.ptr());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
//! Size, align, and flattening information about component model types.
|
//! Size, align, and flattening information about component model types.
|
||||||
|
|
||||||
use crate::component::{
|
use crate::component::{ComponentTypesBuilder, InterfaceType, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS};
|
||||||
ComponentTypesBuilder, FlatType, InterfaceType, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS,
|
|
||||||
};
|
|
||||||
use crate::fact::{AdapterOptions, Context, Options};
|
use crate::fact::{AdapterOptions, Context, Options};
|
||||||
use wasm_encoder::ValType;
|
use wasm_encoder::ValType;
|
||||||
|
|
||||||
@@ -90,23 +88,11 @@ impl ComponentTypesBuilder {
|
|||||||
) -> Option<Vec<ValType>> {
|
) -> Option<Vec<ValType>> {
|
||||||
let mut dst = Vec::new();
|
let mut dst = Vec::new();
|
||||||
for ty in tys {
|
for ty in tys {
|
||||||
let flat = self.flat_types(&ty)?;
|
for ty in opts.flat_types(&ty, self)? {
|
||||||
let types = if opts.memory64 {
|
|
||||||
flat.memory64
|
|
||||||
} else {
|
|
||||||
flat.memory32
|
|
||||||
};
|
|
||||||
for ty in types {
|
|
||||||
let ty = match ty {
|
|
||||||
FlatType::I32 => ValType::I32,
|
|
||||||
FlatType::I64 => ValType::I64,
|
|
||||||
FlatType::F32 => ValType::F32,
|
|
||||||
FlatType::F64 => ValType::F64,
|
|
||||||
};
|
|
||||||
if dst.len() == max {
|
if dst.len() == max {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
dst.push(ty);
|
dst.push((*ty).into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(dst)
|
Some(dst)
|
||||||
|
|||||||
@@ -16,15 +16,18 @@
|
|||||||
//! can be somewhat arbitrary, an intentional decision.
|
//! can be somewhat arbitrary, an intentional decision.
|
||||||
|
|
||||||
use crate::component::{
|
use crate::component::{
|
||||||
CanonicalAbiInfo, ComponentTypesBuilder, InterfaceType, StringEncoding, TypeEnumIndex,
|
CanonicalAbiInfo, ComponentTypesBuilder, FlatType, InterfaceType, StringEncoding,
|
||||||
TypeFlagsIndex, TypeInterfaceIndex, TypeOptionIndex, TypeRecordIndex, TypeResultIndex,
|
TypeEnumIndex, TypeFlagsIndex, TypeInterfaceIndex, TypeOptionIndex, TypeRecordIndex,
|
||||||
TypeTupleIndex, TypeUnionIndex, TypeVariantIndex, VariantInfo, FLAG_MAY_ENTER, FLAG_MAY_LEAVE,
|
TypeResultIndex, TypeTupleIndex, TypeUnionIndex, TypeVariantIndex, VariantInfo, FLAG_MAY_ENTER,
|
||||||
MAX_FLAT_PARAMS, MAX_FLAT_RESULTS,
|
FLAG_MAY_LEAVE, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS,
|
||||||
};
|
};
|
||||||
use crate::fact::signature::Signature;
|
use crate::fact::signature::Signature;
|
||||||
use crate::fact::transcode::{FixedEncoding as FE, Transcode, Transcoder};
|
use crate::fact::transcode::{FixedEncoding as FE, Transcode, Transcoder};
|
||||||
use crate::fact::traps::Trap;
|
use crate::fact::traps::Trap;
|
||||||
use crate::fact::{AdapterData, Body, Context, Function, FunctionId, Module, Options};
|
use crate::fact::{
|
||||||
|
AdapterData, Body, Context, Function, FunctionId, Helper, HelperLocation, HelperType, Module,
|
||||||
|
Options,
|
||||||
|
};
|
||||||
use crate::{FuncIndex, GlobalIndex};
|
use crate::{FuncIndex, GlobalIndex};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
@@ -35,6 +38,10 @@ use wasmtime_component_util::{DiscriminantSize, FlagsSize};
|
|||||||
const MAX_STRING_BYTE_LENGTH: u32 = 1 << 31;
|
const MAX_STRING_BYTE_LENGTH: u32 = 1 << 31;
|
||||||
const UTF16_TAG: u32 = 1 << 31;
|
const UTF16_TAG: u32 = 1 << 31;
|
||||||
|
|
||||||
|
/// This value is arbitrarily chosen and should be fine to change at any time,
|
||||||
|
/// it just seemed like a halfway reasonable starting point.
|
||||||
|
const INITIAL_FUEL: usize = 1_000;
|
||||||
|
|
||||||
struct Compiler<'a, 'b> {
|
struct Compiler<'a, 'b> {
|
||||||
types: &'a ComponentTypesBuilder,
|
types: &'a ComponentTypesBuilder,
|
||||||
module: &'b mut Module<'a>,
|
module: &'b mut Module<'a>,
|
||||||
@@ -54,11 +61,15 @@ struct Compiler<'a, 'b> {
|
|||||||
/// well.
|
/// well.
|
||||||
traps: Vec<(usize, Trap)>,
|
traps: Vec<(usize, Trap)>,
|
||||||
|
|
||||||
/// Indicates whether this call to `translate` is a "top level" on where
|
/// A heuristic which is intended to limit the size of a generated function
|
||||||
/// it's the first call from the root of the generated function. This is
|
/// to a certain maximum to avoid generating arbitrarily large functions.
|
||||||
/// used as a heuristic to know when to split helpers out to a separate
|
///
|
||||||
/// function.
|
/// This fuel counter is decremented each time `translate` is called and
|
||||||
top_level_translate: bool,
|
/// when fuel is entirely consumed further translations, if necessary, will
|
||||||
|
/// be done through calls to other functions in the module. This is intended
|
||||||
|
/// to be a heuristic to split up the main function into theoretically
|
||||||
|
/// reusable portions.
|
||||||
|
fuel: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn compile(module: &mut Module<'_>, adapter: &AdapterData) {
|
pub(super) fn compile(module: &mut Module<'_>, adapter: &AdapterData) {
|
||||||
@@ -78,52 +89,84 @@ pub(super) fn compile(module: &mut Module<'_>, adapter: &AdapterData) {
|
|||||||
free_locals: HashMap::new(),
|
free_locals: HashMap::new(),
|
||||||
traps: Vec::new(),
|
traps: Vec::new(),
|
||||||
result,
|
result,
|
||||||
top_level_translate: true,
|
fuel: INITIAL_FUEL,
|
||||||
}
|
}
|
||||||
.compile_adapter(adapter, &lower_sig, &lift_sig)
|
.compile_adapter(adapter, &lower_sig, &lift_sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compiles a helper function which is used to translate `src` to `dst`
|
/// Compiles a helper function as specified by the `Helper` configuration.
|
||||||
/// in-memory.
|
|
||||||
///
|
///
|
||||||
/// The generated function takes two arguments: the source pointer and
|
/// This function is invoked when the translation process runs out of fuel for
|
||||||
/// destination pointer. The conversion operation is configured by the
|
/// some prior function which enqueues a helper to get translated later. This
|
||||||
/// `src_opts` and `dst_opts` specified as well.
|
/// translation function will perform one type translation as specified by
|
||||||
pub(super) fn compile_translate_mem(
|
/// `Helper` which can either be in the stack or memory for each side.
|
||||||
module: &mut Module<'_>,
|
pub(super) fn compile_helper(module: &mut Module<'_>, result: FunctionId, helper: Helper) {
|
||||||
result: FunctionId,
|
let mut nlocals = 0;
|
||||||
src: InterfaceType,
|
let src_flat;
|
||||||
src_opts: &Options,
|
let src = match helper.src.loc {
|
||||||
dst: InterfaceType,
|
// If the source is on the stack then it's specified in the parameters
|
||||||
dst_opts: &Options,
|
// to the function, so this creates the flattened representation and
|
||||||
) {
|
// then lists those as the locals with appropriate types for the source
|
||||||
|
// values.
|
||||||
|
HelperLocation::Stack => {
|
||||||
|
src_flat = module
|
||||||
|
.types
|
||||||
|
.flatten_types(&helper.src.opts, usize::MAX, [helper.src.ty])
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, ty)| (i as u32, *ty))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
nlocals += src_flat.len() as u32;
|
||||||
|
Source::Stack(Stack {
|
||||||
|
locals: &src_flat,
|
||||||
|
opts: &helper.src.opts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// If the source is in memory then that's just propagated here as the
|
||||||
|
// first local is the pointer to the source.
|
||||||
|
HelperLocation::Memory => {
|
||||||
|
nlocals += 1;
|
||||||
|
Source::Memory(Memory {
|
||||||
|
opts: &helper.src.opts,
|
||||||
|
addr: TempLocal::new(0, helper.src.opts.ptr()),
|
||||||
|
offset: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let dst_flat;
|
||||||
|
let dst = match helper.dst.loc {
|
||||||
|
// This is the same as the stack-based source although `Destination` is
|
||||||
|
// configured slightly differently.
|
||||||
|
HelperLocation::Stack => {
|
||||||
|
dst_flat = module
|
||||||
|
.types
|
||||||
|
.flatten_types(&helper.dst.opts, usize::MAX, [helper.dst.ty])
|
||||||
|
.unwrap();
|
||||||
|
Destination::Stack(&dst_flat, &helper.dst.opts)
|
||||||
|
}
|
||||||
|
// This is the same as a memroy-based source but note that the address
|
||||||
|
// of the destination is passed as the final parameter to the function.
|
||||||
|
HelperLocation::Memory => {
|
||||||
|
nlocals += 1;
|
||||||
|
Destination::Memory(Memory {
|
||||||
|
opts: &helper.dst.opts,
|
||||||
|
addr: TempLocal::new(nlocals - 1, helper.dst.opts.ptr()),
|
||||||
|
offset: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
let mut compiler = Compiler {
|
let mut compiler = Compiler {
|
||||||
types: module.types,
|
types: module.types,
|
||||||
module,
|
module,
|
||||||
code: Vec::new(),
|
code: Vec::new(),
|
||||||
nlocals: 2,
|
nlocals,
|
||||||
free_locals: HashMap::new(),
|
free_locals: HashMap::new(),
|
||||||
traps: Vec::new(),
|
traps: Vec::new(),
|
||||||
result,
|
result,
|
||||||
top_level_translate: true,
|
fuel: INITIAL_FUEL,
|
||||||
};
|
};
|
||||||
// This function only does one thing which is to translate between memory,
|
compiler.translate(&helper.src.ty, &src, &helper.dst.ty, &dst);
|
||||||
// so only one call to `translate` is necessary. Note that the `addr_local`
|
|
||||||
// values come from the function arguments.
|
|
||||||
compiler.translate(
|
|
||||||
&src,
|
|
||||||
&Source::Memory(Memory {
|
|
||||||
opts: src_opts,
|
|
||||||
addr: TempLocal::new(0, src_opts.ptr()),
|
|
||||||
offset: 0,
|
|
||||||
}),
|
|
||||||
&dst,
|
|
||||||
&Destination::Memory(Memory {
|
|
||||||
opts: dst_opts,
|
|
||||||
addr: TempLocal::new(1, dst_opts.ptr()),
|
|
||||||
offset: 0,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
compiler.finish();
|
compiler.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,9 +464,39 @@ impl Compiler<'_, '_> {
|
|||||||
self.assert_aligned(dst_ty, mem);
|
self.assert_aligned(dst_ty, mem);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Classify the source type as "primitive" or not as a heuristic to
|
// Calculate a cost heuristic for what the translation of this specific
|
||||||
// whether the translation should be split out into a helper function.
|
// layer of the type is going to incur. The purpose of this cost is that
|
||||||
let src_primitive = match src_ty {
|
// we'll deduct it from `self.fuel` and if no fuel is remaining then
|
||||||
|
// translation is outlined into a separate function rather than being
|
||||||
|
// translated into this function.
|
||||||
|
//
|
||||||
|
// The general goal is to avoid creating an exponentially sized function
|
||||||
|
// for a linearly sized input (the type section). By outlining helper
|
||||||
|
// functions there will ideally be a constant set of helper functions
|
||||||
|
// per type (to accomodate in-memory or on-stack transfers as well as
|
||||||
|
// src/dst options) which means that each function is at most a certain
|
||||||
|
// size and we have a linear number of functions which should guarantee
|
||||||
|
// an overall linear size of the output.
|
||||||
|
//
|
||||||
|
// To implement this the current heuristic is that each layer of
|
||||||
|
// translating a type has a cost associated with it and this cost is
|
||||||
|
// accounted for in `self.fuel`. Some conversions are considered free as
|
||||||
|
// they generate basically as much code as the `call` to the translation
|
||||||
|
// function while other are considered proportionally expensive to the
|
||||||
|
// size of the type. The hope is that some upper layers are of a type's
|
||||||
|
// translation are all inlined into one function but bottom layers end
|
||||||
|
// up getting outlined to separate functions. Theoretically, again this
|
||||||
|
// is built on hopes and dreams, the outlining can be shared amongst
|
||||||
|
// tightly-intertwined type hierarchies which will reduce the size of
|
||||||
|
// the output module due to the helpers being used.
|
||||||
|
//
|
||||||
|
// This heuristic of how to split functions has changed a few times in
|
||||||
|
// the past and this isn't necessarily guaranteed to be the final
|
||||||
|
// iteration.
|
||||||
|
let cost = match src_ty {
|
||||||
|
// These types are all quite simple to load/store and equate to
|
||||||
|
// basically the same cost of the `call` instruction to call an
|
||||||
|
// out-of-line translation function, so give them 0 cost.
|
||||||
InterfaceType::Bool
|
InterfaceType::Bool
|
||||||
| InterfaceType::U8
|
| InterfaceType::U8
|
||||||
| InterfaceType::S8
|
| InterfaceType::S8
|
||||||
@@ -434,119 +507,169 @@ impl Compiler<'_, '_> {
|
|||||||
| InterfaceType::U64
|
| InterfaceType::U64
|
||||||
| InterfaceType::S64
|
| InterfaceType::S64
|
||||||
| InterfaceType::Float32
|
| InterfaceType::Float32
|
||||||
| InterfaceType::Float64
|
| InterfaceType::Float64 => 0,
|
||||||
| InterfaceType::Char
|
|
||||||
| InterfaceType::Flags(_) => true,
|
|
||||||
|
|
||||||
InterfaceType::String
|
// This has a small amount of validation associated with it, so
|
||||||
| InterfaceType::List(_)
|
// give it a cost of 1.
|
||||||
| InterfaceType::Record(_)
|
InterfaceType::Char => 1,
|
||||||
| InterfaceType::Tuple(_)
|
|
||||||
| InterfaceType::Variant(_)
|
// This has a fair bit of code behind it depending on the
|
||||||
| InterfaceType::Union(_)
|
// strings/encodings in play, so arbitrarily assign it a cost a 5.
|
||||||
| InterfaceType::Enum(_)
|
InterfaceType::String => 5,
|
||||||
| InterfaceType::Option(_)
|
|
||||||
| InterfaceType::Result(_) => false,
|
// Iteration of a loop is along the lines of the cost of a string
|
||||||
|
// so give it the same cost
|
||||||
|
InterfaceType::List(_) => 5,
|
||||||
|
|
||||||
|
InterfaceType::Flags(i) => {
|
||||||
|
let count = self.module.types[*i].names.len();
|
||||||
|
match FlagsSize::from_count(count) {
|
||||||
|
FlagsSize::Size0 => 0,
|
||||||
|
FlagsSize::Size1 | FlagsSize::Size2 => 1,
|
||||||
|
FlagsSize::Size4Plus(n) => n.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InterfaceType::Record(i) => self.types[*i].fields.len(),
|
||||||
|
InterfaceType::Tuple(i) => self.types[*i].types.len(),
|
||||||
|
InterfaceType::Variant(i) => self.types[*i].cases.len(),
|
||||||
|
InterfaceType::Union(i) => self.types[*i].types.len(),
|
||||||
|
InterfaceType::Enum(i) => self.types[*i].names.len(),
|
||||||
|
|
||||||
|
// 2 cases to consider for each of these variants.
|
||||||
|
InterfaceType::Option(_) | InterfaceType::Result(_) => 2,
|
||||||
};
|
};
|
||||||
let top_level = mem::replace(&mut self.top_level_translate, false);
|
|
||||||
|
|
||||||
// Use a number of heuristics to determine whether this translation
|
match self.fuel.checked_sub(cost) {
|
||||||
// should be split out into a helper function rather than translated
|
// This function has enough fuel to perform the layer of translation
|
||||||
// inline. The goal of this heuristic is to avoid a function that is
|
// necessary for this type, so the fuel is updated in-place and
|
||||||
// exponential in the size of a type. For example if everything
|
// translation continues. Note that the recursion here is bounded by
|
||||||
// were translated inline then this could get arbitrarily large
|
// the static recursion limit for all interface types as imposed
|
||||||
//
|
// during the translation phase.
|
||||||
// (type $level0 (list u8))
|
Some(n) => {
|
||||||
// (type $level1 (result $level0 $level0))
|
self.fuel = n;
|
||||||
// (type $level2 (result $level1 $level1))
|
match src_ty {
|
||||||
// (type $level3 (result $level2 $level2))
|
InterfaceType::Bool => self.translate_bool(src, dst_ty, dst),
|
||||||
// (type $level4 (result $level3 $level3))
|
InterfaceType::U8 => self.translate_u8(src, dst_ty, dst),
|
||||||
// ;; ...
|
InterfaceType::S8 => self.translate_s8(src, dst_ty, dst),
|
||||||
//
|
InterfaceType::U16 => self.translate_u16(src, dst_ty, dst),
|
||||||
// If everything we inlined then translation of `$level0` would appear
|
InterfaceType::S16 => self.translate_s16(src, dst_ty, dst),
|
||||||
// in 2^n different locations depending on the depth of the type. By
|
InterfaceType::U32 => self.translate_u32(src, dst_ty, dst),
|
||||||
// splitting out the translation to a helper function, though, it
|
InterfaceType::S32 => self.translate_s32(src, dst_ty, dst),
|
||||||
// means there could be one function for each level, keeping the size
|
InterfaceType::U64 => self.translate_u64(src, dst_ty, dst),
|
||||||
// of translation on par with the size of the module itself.
|
InterfaceType::S64 => self.translate_s64(src, dst_ty, dst),
|
||||||
//
|
InterfaceType::Float32 => self.translate_f32(src, dst_ty, dst),
|
||||||
// The heuristics which go into this splitting currently are:
|
InterfaceType::Float64 => self.translate_f64(src, dst_ty, dst),
|
||||||
//
|
InterfaceType::Char => self.translate_char(src, dst_ty, dst),
|
||||||
// * Both the source and destination must be memory. This skips "top
|
InterfaceType::String => self.translate_string(src, dst_ty, dst),
|
||||||
// level" translation for adapters where arguments/results come from
|
InterfaceType::List(t) => self.translate_list(*t, src, dst_ty, dst),
|
||||||
// direct parameters or get placed on the stack.
|
InterfaceType::Record(t) => self.translate_record(*t, src, dst_ty, dst),
|
||||||
//
|
InterfaceType::Flags(f) => self.translate_flags(*f, src, dst_ty, dst),
|
||||||
// * Primitive types are skipped here since they have no need to be
|
InterfaceType::Tuple(t) => self.translate_tuple(*t, src, dst_ty, dst),
|
||||||
// split out. This is for types like integers and floats.
|
InterfaceType::Variant(v) => self.translate_variant(*v, src, dst_ty, dst),
|
||||||
//
|
InterfaceType::Union(u) => self.translate_union(*u, src, dst_ty, dst),
|
||||||
// * The "top level" of a function is also skipped. That basically
|
InterfaceType::Enum(t) => self.translate_enum(*t, src, dst_ty, dst),
|
||||||
// means that the first call to `translate` will never split out
|
InterfaceType::Option(t) => self.translate_option(*t, src, dst_ty, dst),
|
||||||
// a helper function (since if we're already in a helper function
|
InterfaceType::Result(t) => self.translate_result(*t, src, dst_ty, dst),
|
||||||
// that could cause infinite recursion in the wasm). Otherwise
|
}
|
||||||
// this keeps the top-level list of types in adapters nice and inline
|
}
|
||||||
// too while only possibly considering splitting out deeper types.
|
|
||||||
//
|
|
||||||
// This heuristic may need tweaking over time naturally as more modules
|
|
||||||
// in the wild are seen and performance measurements are taken. For now
|
|
||||||
// this keeps the fuzzers happy by avoiding exponentially-sized output
|
|
||||||
// given an input.
|
|
||||||
if let (Source::Memory(src), Destination::Memory(dst)) = (src, dst) {
|
|
||||||
if !src_primitive && !top_level {
|
|
||||||
// Compile the helper function which will translate the source
|
|
||||||
// type to the destination type. The two parameters to this
|
|
||||||
// function are the source/destination pointers which are
|
|
||||||
// calculated here to pass through. Our own function then
|
|
||||||
// grows a `Body::Call` to the function generated. Note that
|
|
||||||
// `Body::Call` is used here instead of `Instruction::Call`
|
|
||||||
// because we don't know the final index of the generated
|
|
||||||
// function yet. It's filled in at the end of adapter module
|
|
||||||
// translation.
|
|
||||||
let helper = self
|
|
||||||
.module
|
|
||||||
.translate_mem(*src_ty, src.opts, *dst_ty, dst.opts);
|
|
||||||
|
|
||||||
// TODO: overflow checks?
|
// This function does not have enough fuel left to perform this
|
||||||
self.instruction(LocalGet(src.addr.idx));
|
// layer of translation so the translation is deferred to a helper
|
||||||
if src.offset != 0 {
|
// function. The actual translation here is then done by marshalling
|
||||||
self.ptr_uconst(src.opts, src.offset);
|
// the src/dst into the function we're calling and then processing
|
||||||
self.ptr_add(src.opts);
|
// the results.
|
||||||
}
|
None => {
|
||||||
self.instruction(LocalGet(dst.addr.idx));
|
let src_loc = match src {
|
||||||
if dst.offset != 0 {
|
// If the source is on the stack then `stack_get` is used to
|
||||||
self.ptr_uconst(dst.opts, dst.offset);
|
// convert everything to the appropriate flat representation
|
||||||
self.ptr_add(dst.opts);
|
// for the source type.
|
||||||
}
|
Source::Stack(stack) => {
|
||||||
|
for (i, ty) in stack
|
||||||
|
.opts
|
||||||
|
.flat_types(src_ty, self.types)
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
let stack = stack.slice(i..i + 1);
|
||||||
|
self.stack_get(&stack, (*ty).into());
|
||||||
|
}
|
||||||
|
HelperLocation::Stack
|
||||||
|
}
|
||||||
|
// If the source is in memory then the pointer is passed
|
||||||
|
// through, but note that the offset must be factored in
|
||||||
|
// here since the translation function will start from
|
||||||
|
// offset 0.
|
||||||
|
Source::Memory(mem) => {
|
||||||
|
self.push_mem_addr(mem);
|
||||||
|
HelperLocation::Memory
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let dst_loc = match dst {
|
||||||
|
Destination::Stack(..) => HelperLocation::Stack,
|
||||||
|
Destination::Memory(mem) => {
|
||||||
|
self.push_mem_addr(mem);
|
||||||
|
HelperLocation::Memory
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Generate a `FunctionId` corresponding to the `Helper`
|
||||||
|
// configuration that is necessary here. This will ideally be a
|
||||||
|
// "cache hit" and use a preexisting helper which represents
|
||||||
|
// outlining what would otherwise be duplicate code within a
|
||||||
|
// function to one function.
|
||||||
|
let helper = self.module.translate_helper(Helper {
|
||||||
|
src: HelperType {
|
||||||
|
ty: *src_ty,
|
||||||
|
opts: *src.opts(),
|
||||||
|
loc: src_loc,
|
||||||
|
},
|
||||||
|
dst: HelperType {
|
||||||
|
ty: *dst_ty,
|
||||||
|
opts: *dst.opts(),
|
||||||
|
loc: dst_loc,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Emit a `call` instruction which will get "relocated" to a
|
||||||
|
// function index once translation has completely finished.
|
||||||
self.flush_code();
|
self.flush_code();
|
||||||
self.module.funcs[self.result].body.push(Body::Call(helper));
|
self.module.funcs[self.result].body.push(Body::Call(helper));
|
||||||
self.top_level_translate = true;
|
|
||||||
return;
|
// If the destination of the translation was on the stack then
|
||||||
|
// the types on the stack need to be optionally converted to
|
||||||
|
// different types (e.g. if the result here is part of a variant
|
||||||
|
// somewhere else).
|
||||||
|
//
|
||||||
|
// This translation happens inline here by popping the results
|
||||||
|
// into new locals and then using those locals to do a
|
||||||
|
// `stack_set`.
|
||||||
|
if let Destination::Stack(tys, opts) = dst {
|
||||||
|
let flat = self
|
||||||
|
.types
|
||||||
|
.flatten_types(opts, usize::MAX, [*dst_ty])
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(flat.len(), tys.len());
|
||||||
|
let locals = flat
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.map(|ty| self.local_set_new_tmp(*ty))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for (ty, local) in tys.iter().zip(locals.into_iter().rev()) {
|
||||||
|
self.instruction(LocalGet(local.idx));
|
||||||
|
self.stack_set(std::slice::from_ref(ty), local.ty);
|
||||||
|
self.free_temp_local(local);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match src_ty {
|
}
|
||||||
InterfaceType::Bool => self.translate_bool(src, dst_ty, dst),
|
|
||||||
InterfaceType::U8 => self.translate_u8(src, dst_ty, dst),
|
|
||||||
InterfaceType::S8 => self.translate_s8(src, dst_ty, dst),
|
|
||||||
InterfaceType::U16 => self.translate_u16(src, dst_ty, dst),
|
|
||||||
InterfaceType::S16 => self.translate_s16(src, dst_ty, dst),
|
|
||||||
InterfaceType::U32 => self.translate_u32(src, dst_ty, dst),
|
|
||||||
InterfaceType::S32 => self.translate_s32(src, dst_ty, dst),
|
|
||||||
InterfaceType::U64 => self.translate_u64(src, dst_ty, dst),
|
|
||||||
InterfaceType::S64 => self.translate_s64(src, dst_ty, dst),
|
|
||||||
InterfaceType::Float32 => self.translate_f32(src, dst_ty, dst),
|
|
||||||
InterfaceType::Float64 => self.translate_f64(src, dst_ty, dst),
|
|
||||||
InterfaceType::Char => self.translate_char(src, dst_ty, dst),
|
|
||||||
InterfaceType::String => self.translate_string(src, dst_ty, dst),
|
|
||||||
InterfaceType::List(t) => self.translate_list(*t, src, dst_ty, dst),
|
|
||||||
InterfaceType::Record(t) => self.translate_record(*t, src, dst_ty, dst),
|
|
||||||
InterfaceType::Flags(f) => self.translate_flags(*f, src, dst_ty, dst),
|
|
||||||
InterfaceType::Tuple(t) => self.translate_tuple(*t, src, dst_ty, dst),
|
|
||||||
InterfaceType::Variant(v) => self.translate_variant(*v, src, dst_ty, dst),
|
|
||||||
InterfaceType::Union(u) => self.translate_union(*u, src, dst_ty, dst),
|
|
||||||
InterfaceType::Enum(t) => self.translate_enum(*t, src, dst_ty, dst),
|
|
||||||
InterfaceType::Option(t) => self.translate_option(*t, src, dst_ty, dst),
|
|
||||||
InterfaceType::Result(t) => self.translate_result(*t, src, dst_ty, dst),
|
|
||||||
}
|
|
||||||
|
|
||||||
self.top_level_translate = top_level;
|
fn push_mem_addr(&mut self, mem: &Memory<'_>) {
|
||||||
|
self.instruction(LocalGet(mem.addr.idx));
|
||||||
|
if mem.offset != 0 {
|
||||||
|
self.ptr_uconst(mem.opts, mem.offset);
|
||||||
|
self.ptr_add(mem.opts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn translate_bool(&mut self, src: &Source<'_>, dst_ty: &InterfaceType, dst: &Destination) {
|
fn translate_bool(&mut self, src: &Source<'_>, dst_ty: &InterfaceType, dst: &Destination) {
|
||||||
@@ -3025,3 +3148,14 @@ impl std::ops::Drop for TempLocal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<FlatType> for ValType {
|
||||||
|
fn from(ty: FlatType) -> ValType {
|
||||||
|
match ty {
|
||||||
|
FlatType::I32 => ValType::I32,
|
||||||
|
FlatType::I64 => ValType::I64,
|
||||||
|
FlatType::F32 => ValType::F32,
|
||||||
|
FlatType::F64 => ValType::F64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user