1 //! This module provides a set of primitives that allow implementing an incremental cache on top of 2 //! Cranelift, making it possible to reuse previous compiled artifacts for functions that have been 3 //! compiled previously. 4 //! 5 //! This set of operation is experimental and can be enabled using the Cargo feature 6 //! `incremental-cache`. 7 //! 8 //! This can bring speedups in different cases: change-code-and-immediately-recompile iterations 9 //! get faster, modules sharing lots of code can reuse each other's artifacts, etc. 10 //! 11 //! The three main primitives are the following: 12 //! - `compute_cache_key` is used to compute the cache key associated to a `Function`. This is 13 //! basically the content of the function, modulo a few things the caching system is resilient to. 14 //! - `serialize_compiled` is used to serialize the result of a compilation, so it can be reused 15 //! later on by... 16 //! - `try_finish_recompile`, which reads binary blobs serialized with `serialize_compiled`, 17 //! re-creating the compilation artifact from those. 18 //! 19 //! The `CacheStore` trait and `Context::compile_with_cache` method are provided as 20 //! high-level, easy-to-use facilities to make use of that cache, and show an example of how to use 21 //! the above three primitives to form a full incremental caching system. 22 23 use core::fmt; 24 use core::hash::{Hash, Hasher}; 25 26 use crate::alloc::vec::Vec; 27 use crate::ir::Function; 28 use crate::ir::function::{FunctionStencil, VersionMarker}; 29 use crate::machinst::{CompiledCode, CompiledCodeStencil}; 30 use crate::result::CompileResult; 31 use crate::{CompileError, Context, trace}; 32 use crate::{isa::TargetIsa, timing}; 33 use alloc::borrow::Cow; 34 use cranelift_control::ControlPlane; 35 36 impl Context { 37 /// Compile the function, as in `compile`, but tries to reuse compiled artifacts from former 38 /// compilations using the provided cache store. 39 pub fn compile_with_cache( 40 &mut self, 41 isa: &dyn TargetIsa, 42 cache_store: &mut dyn CacheKvStore, 43 ctrl_plane: &mut ControlPlane, 44 ) -> CompileResult<'_, (&CompiledCode, bool)> { 45 let cache_key_hash = { 46 let _tt = timing::try_incremental_cache(); 47 48 let cache_key_hash = compute_cache_key(isa, &self.func); 49 50 if let Some(blob) = cache_store.get(&cache_key_hash.0) { 51 match try_finish_recompile(&self.func, &blob) { 52 Ok(compiled_code) => { 53 let info = compiled_code.code_info(); 54 55 if isa.flags().enable_incremental_compilation_cache_checks() { 56 let actual_result = self.compile(isa, ctrl_plane)?; 57 assert_eq!(*actual_result, compiled_code); 58 assert_eq!(actual_result.code_info(), info); 59 // no need to set `compiled_code` here, it's set by `compile()`. 60 return Ok((actual_result, true)); 61 } 62 63 let compiled_code = self.compiled_code.insert(compiled_code); 64 return Ok((compiled_code, true)); 65 } 66 Err(err) => { 67 trace!("error when finishing recompilation: {err}"); 68 } 69 } 70 } 71 72 cache_key_hash 73 }; 74 75 let stencil = self 76 .compile_stencil(isa, ctrl_plane) 77 .map_err(|err| CompileError { 78 inner: err, 79 func: &self.func, 80 })?; 81 82 let stencil = { 83 let _tt = timing::store_incremental_cache(); 84 let (stencil, res) = serialize_compiled(stencil); 85 if let Ok(blob) = res { 86 cache_store.insert(&cache_key_hash.0, blob); 87 } 88 stencil 89 }; 90 91 let compiled_code = self 92 .compiled_code 93 .insert(stencil.apply_params(&self.func.params)); 94 95 Ok((compiled_code, false)) 96 } 97 } 98 99 /// Backing storage for an incremental compilation cache, when enabled. 100 pub trait CacheKvStore { 101 /// Given a cache key hash, retrieves the associated opaque serialized data. 102 fn get(&self, key: &[u8]) -> Option<Cow<'_, [u8]>>; 103 104 /// Given a new cache key and a serialized blob obtained from `serialize_compiled`, stores it 105 /// in the cache store. 106 fn insert(&mut self, key: &[u8], val: Vec<u8>); 107 } 108 109 /// Hashed `CachedKey`, to use as an identifier when looking up whether a function has already been 110 /// compiled or not. 111 #[derive(Clone, Hash, PartialEq, Eq)] 112 pub struct CacheKeyHash([u8; 32]); 113 114 impl core::fmt::Display for CacheKeyHash { 115 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 116 write!(f, "CacheKeyHash:{:?}", self.0) 117 } 118 } 119 120 #[derive(serde_derive::Serialize, serde_derive::Deserialize)] 121 struct CachedFunc { 122 // Note: The version marker must be first to ensure deserialization stops in case of a version 123 // mismatch before attempting to deserialize the actual compiled code. 124 version_marker: VersionMarker, 125 stencil: CompiledCodeStencil, 126 } 127 128 /// Key for caching a single function's compilation. 129 /// 130 /// If two functions get the same `CacheKey`, then we can reuse the compiled artifacts, modulo some 131 /// fixups. 132 /// 133 /// Note: the key will be invalidated across different versions of cranelift, as the 134 /// `FunctionStencil` contains a `VersionMarker` itself. 135 struct CacheKey<'a> { 136 stencil: &'a FunctionStencil, 137 isa: &'a dyn TargetIsa, 138 } 139 140 impl<'a> Hash for CacheKey<'a> { 141 fn hash<H: Hasher>(&self, state: &mut H) { 142 self.stencil.hash(state); 143 self.isa.name().hash(state); 144 self.isa.triple().hash(state); 145 self.isa.flags().hash(state); 146 self.isa.isa_flags_hash_key().hash(state); 147 } 148 } 149 150 impl<'a> CacheKey<'a> { 151 /// Creates a new cache store key for a function. 152 /// 153 /// This is a bit expensive to compute, so it should be cached and reused as much as possible. 154 fn new(isa: &'a dyn TargetIsa, f: &'a Function) -> Self { 155 CacheKey { 156 stencil: &f.stencil, 157 isa, 158 } 159 } 160 } 161 162 /// Compute a cache key, and hash it on your behalf. 163 /// 164 /// Since computing the `CacheKey` is a bit expensive, it should be done as least as possible. 165 pub fn compute_cache_key(isa: &dyn TargetIsa, func: &Function) -> CacheKeyHash { 166 use core::hash::{Hash as _, Hasher}; 167 use sha2::Digest as _; 168 169 struct Sha256Hasher(sha2::Sha256); 170 171 impl Hasher for Sha256Hasher { 172 fn finish(&self) -> u64 { 173 panic!("Sha256Hasher doesn't support finish!"); 174 } 175 fn write(&mut self, bytes: &[u8]) { 176 self.0.update(bytes); 177 } 178 } 179 180 let cache_key = CacheKey::new(isa, func); 181 182 let mut hasher = Sha256Hasher(sha2::Sha256::new()); 183 cache_key.hash(&mut hasher); 184 let hash: [u8; 32] = hasher.0.finalize().into(); 185 186 CacheKeyHash(hash) 187 } 188 189 /// Given a function that's been successfully compiled, serialize it to a blob that the caller may 190 /// store somewhere for future use by `try_finish_recompile`. 191 /// 192 /// As this function requires ownership on the `CompiledCodeStencil`, it gives it back at the end 193 /// of the function call. The value is left untouched. 194 pub fn serialize_compiled( 195 result: CompiledCodeStencil, 196 ) -> (CompiledCodeStencil, Result<Vec<u8>, postcard::Error>) { 197 let cached = CachedFunc { 198 version_marker: VersionMarker, 199 stencil: result, 200 }; 201 let result = postcard::to_allocvec(&cached); 202 (cached.stencil, result) 203 } 204 205 /// An error returned when recompiling failed. 206 #[derive(Debug)] 207 pub enum RecompileError { 208 /// The version embedded in the cache entry isn't the same as cranelift's current version. 209 VersionMismatch, 210 /// An error occurred while deserializing the cache entry. 211 Deserialize(postcard::Error), 212 } 213 214 impl fmt::Display for RecompileError { 215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 216 match self { 217 RecompileError::VersionMismatch => write!(f, "cranelift version mismatch",), 218 RecompileError::Deserialize(err) => { 219 write!(f, "postcard failed during deserialization: {err}") 220 } 221 } 222 } 223 } 224 225 /// Given a function that's been precompiled and its entry in the caching storage, try to shortcut 226 /// compilation of the given function. 227 /// 228 /// Precondition: the bytes must have retrieved from a cache store entry which hash value 229 /// is strictly the same as the `Function`'s computed hash retrieved from `compute_cache_key`. 230 pub fn try_finish_recompile(func: &Function, bytes: &[u8]) -> Result<CompiledCode, RecompileError> { 231 match postcard::from_bytes::<CachedFunc>(bytes) { 232 Ok(result) => { 233 if result.version_marker != func.stencil.version_marker { 234 Err(RecompileError::VersionMismatch) 235 } else { 236 Ok(result.stencil.apply_params(&func.params)) 237 } 238 } 239 Err(err) => Err(RecompileError::Deserialize(err)), 240 } 241 } 242