Implement an incremental compilation cache for Cranelift (#4551)

This is the implementation of https://github.com/bytecodealliance/wasmtime/issues/4155, using the "inverted API" approach suggested by @cfallin (thanks!) in Cranelift, and trait object to provide a backend for an all-included experience in Wasmtime. 

After the suggestion of Chris, `Function` has been split into mostly two parts:

- on the one hand, `FunctionStencil` contains all the fields required during compilation, and that act as a compilation cache key: if two function stencils are the same, then the result of their compilation (`CompiledCodeBase<Stencil>`) will be the same. This makes caching trivial, as the only thing to cache is the `FunctionStencil`.
- on the other hand, `FunctionParameters` contain the... function parameters that are required to finalize the result of compilation into a `CompiledCode` (aka `CompiledCodeBase<Final>`) with proper final relocations etc., by applying fixups and so on.

Most changes are here to accomodate those requirements, in particular that `FunctionStencil` should be `Hash`able to be used as a key in the cache:

- most source locations are now relative to a base source location in the function, and as such they're encoded as `RelSourceLoc` in the `FunctionStencil`. This required changes so that there's no need to explicitly mark a `SourceLoc` as the base source location, it's automatically detected instead the first time a non-default `SourceLoc` is set.
- user-defined external names in the `FunctionStencil` (aka before this patch `ExternalName::User { namespace, index }`) are now references into an external table of `UserExternalNameRef -> UserExternalName`, present in the `FunctionParameters`, and must be explicitly declared using `Function::declare_imported_user_function`.
- some refactorings have been made for function names:
  - `ExternalName` was used as the type for a `Function`'s name; while it thus allowed `ExternalName::Libcall` in this place, this would have been quite confusing to use it there. Instead, a new enum `UserFuncName` is introduced for this name, that's either a user-defined function name (the above `UserExternalName`) or a test case name.
  - The future of `ExternalName` is likely to become a full reference into the `FunctionParameters`'s mapping, instead of being "either a handle for user-defined external names, or the thing itself for other variants". I'm running out of time to do this, and this is not trivial as it implies touching ISLE which I'm less familiar with.

The cache computes a sha256 hash of the `FunctionStencil`, and uses this as the cache key. No equality check (using `PartialEq`) is performed in addition to the hash being the same, as we hope that this is sufficient data to avoid collisions.

A basic fuzz target has been introduced that tries to do the bare minimum:

- check that a function successfully compiled and cached will be also successfully reloaded from the cache, and returns the exact same function.
- check that a trivial modification in the external mapping of `UserExternalNameRef -> UserExternalName` hits the cache, and that other modifications don't hit the cache.
  - This last check is less efficient and less likely to happen, so probably should be rethought a bit.

Thanks to both @alexcrichton and @cfallin for your very useful feedback on Zulip.

Some numbers show that for a large wasm module we're using internally, this is a 20% compile-time speedup, because so many `FunctionStencil`s are the same, even within a single module. For a group of modules that have a lot of code in common, we get hit rates up to 70% when they're used together. When a single function changes in a wasm module, every other function is reloaded; that's still slower than I expect (between 10% and 50% of the overall compile time), so there's likely room for improvement. 

Fixes #4155.
This commit is contained in:
Benjamin Bouvier
2022-08-12 18:47:43 +02:00
committed by GitHub
parent ac9725840d
commit 8a9b1a9025
103 changed files with 2176 additions and 693 deletions

View File

@@ -11,11 +11,12 @@ use crate::ir::{
ExtFuncData, FuncRef, GlobalValue, GlobalValueData, Heap, HeapData, Inst, InstructionData,
JumpTable, JumpTableData, Opcode, SigRef, StackSlot, StackSlotData, Table, TableData, Type,
};
use crate::ir::{DataFlowGraph, ExternalName, Layout, Signature};
use crate::ir::{DataFlowGraph, Layout, Signature};
use crate::ir::{DynamicStackSlots, SourceLocs, StackSlots};
use crate::isa::CallConv;
use crate::value_label::ValueLabelsRanges;
use crate::write::write_function;
use crate::HashMap;
#[cfg(feature = "enable-serde")]
use alloc::string::String;
use core::fmt;
@@ -27,9 +28,13 @@ use serde::ser::Serializer;
#[cfg(feature = "enable-serde")]
use serde::{Deserialize, Serialize};
use super::entities::UserExternalNameRef;
use super::extname::UserFuncName;
use super::{RelSourceLoc, SourceLoc, UserExternalName};
/// A version marker used to ensure that serialized clif ir is never deserialized with a
/// different version of Cranelift.
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, PartialEq, Hash)]
pub struct VersionMarker;
#[cfg(feature = "enable-serde")]
@@ -60,21 +65,99 @@ impl<'de> Deserialize<'de> for VersionMarker {
}
}
///
/// Functions can be cloned, but it is not a very fast operation.
/// The clone will have all the same entity numbers as the original.
/// Function parameters used when creating this function, and that will become applied after
/// compilation to materialize the final `CompiledCode`.
#[derive(Clone)]
#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
pub struct Function {
pub struct FunctionParameters {
/// The first `SourceLoc` appearing in the function, serving as a base for every relative
/// source loc in the function.
base_srcloc: Option<SourceLoc>,
/// External user-defined function references.
user_named_funcs: PrimaryMap<UserExternalNameRef, UserExternalName>,
/// Inverted mapping of `user_named_funcs`, to deduplicate internally.
user_ext_name_to_ref: HashMap<UserExternalName, UserExternalNameRef>,
}
impl FunctionParameters {
/// Creates a new `FunctionParameters` with the given name.
pub fn new() -> Self {
Self {
base_srcloc: None,
user_named_funcs: Default::default(),
user_ext_name_to_ref: Default::default(),
}
}
/// Returns the base `SourceLoc`.
///
/// If it was never explicitly set with `ensure_base_srcloc`, will return an invalid
/// `SourceLoc`.
pub fn base_srcloc(&self) -> SourceLoc {
self.base_srcloc.unwrap_or_default()
}
/// Sets the base `SourceLoc`, if not set yet, and returns the base value.
pub fn ensure_base_srcloc(&mut self, srcloc: SourceLoc) -> SourceLoc {
match self.base_srcloc {
Some(val) => val,
None => {
self.base_srcloc = Some(srcloc);
srcloc
}
}
}
/// Retrieve a `UserExternalNameRef` for the given name, or add a new one.
///
/// This method internally deduplicates same `UserExternalName` so they map to the same
/// reference.
pub fn ensure_user_func_name(&mut self, name: UserExternalName) -> UserExternalNameRef {
if let Some(reff) = self.user_ext_name_to_ref.get(&name) {
*reff
} else {
let reff = self.user_named_funcs.push(name.clone());
self.user_ext_name_to_ref.insert(name, reff);
reff
}
}
/// Resets an already existing user function name to a new value.
pub fn reset_user_func_name(&mut self, index: UserExternalNameRef, name: UserExternalName) {
if let Some(prev_name) = self.user_named_funcs.get_mut(index) {
self.user_ext_name_to_ref.remove(prev_name);
*prev_name = name.clone();
self.user_ext_name_to_ref.insert(name, index);
}
}
/// Returns the internal mapping of `UserExternalNameRef` to `UserExternalName`.
pub fn user_named_funcs(&self) -> &PrimaryMap<UserExternalNameRef, UserExternalName> {
&self.user_named_funcs
}
fn clear(&mut self) {
self.base_srcloc = None;
self.user_named_funcs.clear();
self.user_ext_name_to_ref.clear();
}
}
/// Function fields needed when compiling a function.
///
/// Additionally, these fields can be the same for two functions that would be compiled the same
/// way, and finalized by applying `FunctionParameters` onto their `CompiledCodeStencil`.
#[derive(Clone, PartialEq, Hash)]
#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
pub struct FunctionStencil {
/// A version marker used to ensure that serialized clif ir is never deserialized with a
/// different version of Cranelift.
// Note: This must be the first field to ensure that Serde will deserialize it before
// attempting to deserialize other fields that are potentially changed between versions.
pub version_marker: VersionMarker,
/// Name of this function. Mostly used by `.clif` files.
pub name: ExternalName,
/// Signature of this function.
pub signature: Signature,
@@ -106,7 +189,7 @@ pub struct Function {
///
/// Track the original source location for each instruction. The source locations are not
/// interpreted by Cranelift, only preserved.
pub srclocs: SourceLocs,
srclocs: SourceLocs,
/// An optional global value which represents an expression evaluating to
/// the stack limit for this function. This `GlobalValue` will be
@@ -116,28 +199,8 @@ pub struct Function {
pub stack_limit: Option<ir::GlobalValue>,
}
impl Function {
/// Create a function with the given name and signature.
pub fn with_name_signature(name: ExternalName, sig: Signature) -> Self {
Self {
version_marker: VersionMarker,
name,
signature: sig,
sized_stack_slots: StackSlots::new(),
dynamic_stack_slots: DynamicStackSlots::new(),
global_values: PrimaryMap::new(),
heaps: PrimaryMap::new(),
tables: PrimaryMap::new(),
jump_tables: PrimaryMap::new(),
dfg: DataFlowGraph::new(),
layout: Layout::new(),
srclocs: SecondaryMap::new(),
stack_limit: None,
}
}
/// Clear all data structures in this function.
pub fn clear(&mut self) {
impl FunctionStencil {
fn clear(&mut self) {
self.signature.clear(CallConv::Fast);
self.sized_stack_slots.clear();
self.dynamic_stack_slots.clear();
@@ -151,11 +214,6 @@ impl Function {
self.stack_limit = None;
}
/// Create a new empty, anonymous function with a Fast calling convention.
pub fn new() -> Self {
Self::with_name_signature(ExternalName::default(), Signature::new(CallConv::Fast))
}
/// Creates a jump table in the function, to be used by `br_table` instructions.
pub fn create_jump_table(&mut self, data: JumpTableData) -> JumpTable {
self.jump_tables.push(data)
@@ -178,11 +236,6 @@ impl Function {
self.dfg.signatures.push(signature)
}
/// Declare an external function import.
pub fn import_function(&mut self, data: ExtFuncData) -> FuncRef {
self.dfg.ext_funcs.push(data)
}
/// Declares a global value accessible to the function.
pub fn create_global_value(&mut self, data: GlobalValueData) -> GlobalValue {
self.global_values.push(data)
@@ -218,19 +271,6 @@ impl Function {
self.tables.push(data)
}
/// Return an object that can display this function with correct ISA-specific annotations.
pub fn display(&self) -> DisplayFunction<'_> {
DisplayFunction(self, Default::default())
}
/// Return an object that can display this function with correct ISA-specific annotations.
pub fn display_with<'a>(
&'a self,
annotations: DisplayFunctionAnnotations<'a>,
) -> DisplayFunction<'a> {
DisplayFunction(self, annotations)
}
/// Find a presumed unique special-purpose function parameter value.
///
/// Returns the value of the last `purpose` parameter, or `None` if no such parameter exists.
@@ -260,8 +300,8 @@ impl Function {
/// Rewrite the branch destination to `new_dest` if the destination matches `old_dest`.
/// Does nothing if called with a non-jump or non-branch instruction.
///
/// Unlike [change_branch_destination](Function::change_branch_destination), this method rewrite the destinations of
/// multi-destination branches like `br_table`.
/// Unlike [change_branch_destination](FunctionStencil::change_branch_destination), this method
/// rewrite the destinations of multi-destination branches like `br_table`.
pub fn rewrite_branch_destination(&mut self, inst: Inst, old_dest: Block, new_dest: Block) {
match self.dfg.analyze_branch(inst) {
BranchInfo::SingleDest(dest, ..) => {
@@ -356,6 +396,120 @@ impl Function {
pub fn fixed_stack_size(&self) -> u32 {
self.sized_stack_slots.values().map(|ss| ss.size).sum()
}
/// Returns the list of relative source locations for this function.
pub(crate) fn rel_srclocs(&self) -> &SecondaryMap<Inst, RelSourceLoc> {
&self.srclocs
}
}
/// Functions can be cloned, but it is not a very fast operation.
/// The clone will have all the same entity numbers as the original.
#[derive(Clone)]
#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
pub struct Function {
/// Name of this function.
///
/// Mostly used by `.clif` files, only there for debugging / naming purposes.
pub name: UserFuncName,
/// All the fields required for compiling a function, independently of details irrelevant to
/// compilation and that are stored in the `FunctionParameters` `params` field instead.
pub stencil: FunctionStencil,
/// All the parameters that can be applied onto the function stencil, that is, that don't
/// matter when caching compilation artifacts.
pub params: FunctionParameters,
}
impl core::ops::Deref for Function {
type Target = FunctionStencil;
fn deref(&self) -> &Self::Target {
&self.stencil
}
}
impl core::ops::DerefMut for Function {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.stencil
}
}
impl Function {
/// Create a function with the given name and signature.
pub fn with_name_signature(name: UserFuncName, sig: Signature) -> Self {
Self {
name,
stencil: FunctionStencil {
version_marker: VersionMarker,
signature: sig,
sized_stack_slots: StackSlots::new(),
dynamic_stack_slots: DynamicStackSlots::new(),
global_values: PrimaryMap::new(),
heaps: PrimaryMap::new(),
tables: PrimaryMap::new(),
jump_tables: PrimaryMap::new(),
dfg: DataFlowGraph::new(),
layout: Layout::new(),
srclocs: SecondaryMap::new(),
stack_limit: None,
},
params: FunctionParameters::new(),
}
}
/// Clear all data structures in this function.
pub fn clear(&mut self) {
self.stencil.clear();
self.params.clear();
self.name = UserFuncName::default();
}
/// Create a new empty, anonymous function with a Fast calling convention.
pub fn new() -> Self {
Self::with_name_signature(Default::default(), Signature::new(CallConv::Fast))
}
/// Return an object that can display this function with correct ISA-specific annotations.
pub fn display(&self) -> DisplayFunction<'_> {
DisplayFunction(self, Default::default())
}
/// Return an object that can display this function with correct ISA-specific annotations.
pub fn display_with<'a>(
&'a self,
annotations: DisplayFunctionAnnotations<'a>,
) -> DisplayFunction<'a> {
DisplayFunction(self, annotations)
}
/// Sets an absolute source location for the given instruction.
///
/// If no base source location has been set yet, records it at the same time.
pub fn set_srcloc(&mut self, inst: Inst, srcloc: SourceLoc) {
let base = self.params.ensure_base_srcloc(srcloc);
self.stencil.srclocs[inst] = RelSourceLoc::from_base_offset(base, srcloc);
}
/// Returns an absolute source location for the given instruction.
pub fn srcloc(&self, inst: Inst) -> SourceLoc {
let base = self.params.base_srcloc();
self.stencil.srclocs[inst].expand(base)
}
/// Declare a user-defined external function import, to be referenced in `ExtFuncData::User` later.
pub fn declare_imported_user_function(
&mut self,
name: UserExternalName,
) -> UserExternalNameRef {
self.params.ensure_user_func_name(name)
}
/// Declare an external function import.
pub fn import_function(&mut self, data: ExtFuncData) -> FuncRef {
self.stencil.dfg.ext_funcs.push(data)
}
}
/// Additional annotations for function display.