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 25 use crate::alloc::string::String; 26 use crate::alloc::vec::Vec; 27 use crate::ir::function::{FunctionStencil, VersionMarker}; 28 use crate::ir::Function; 29 use crate::machinst::{CompiledCode, CompiledCodeStencil}; 30 use crate::result::CompileResult; 31 use crate::{isa::TargetIsa, timing}; 32 use crate::{trace, CompileError, Context}; 33 use alloc::borrow::{Cow, ToOwned as _}; 34 use alloc::string::ToString as _; 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 ) -> CompileResult<(&CompiledCode, bool)> { 44 let cache_key_hash = { 45 let _tt = timing::try_incremental_cache(); 46 47 let cache_key_hash = compute_cache_key(isa, &mut self.func); 48 49 if let Some(blob) = cache_store.get(&cache_key_hash.0) { 50 match try_finish_recompile(&self.func, &blob) { 51 Ok(compiled_code) => { 52 let info = compiled_code.code_info(); 53 54 if isa.flags().enable_incremental_compilation_cache_checks() { 55 let actual_result = self.compile(isa)?; 56 assert_eq!(*actual_result, compiled_code); 57 assert_eq!(actual_result.code_info(), info); 58 // no need to set `compiled_code` here, it's set by `compile()`. 59 return Ok((actual_result, true)); 60 } 61 62 let compiled_code = self.compiled_code.insert(compiled_code); 63 return Ok((compiled_code, true)); 64 } 65 Err(err) => { 66 trace!("error when finishing recompilation: {err}"); 67 } 68 } 69 } 70 71 cache_key_hash 72 }; 73 74 let stencil = self.compile_stencil(isa).map_err(|err| CompileError { 75 inner: err, 76 func: &self.func, 77 })?; 78 79 let stencil = { 80 let _tt = timing::store_incremental_cache(); 81 let (stencil, res) = serialize_compiled(stencil); 82 if let Ok(blob) = res { 83 cache_store.insert(&cache_key_hash.0, blob); 84 } 85 stencil 86 }; 87 88 let compiled_code = self 89 .compiled_code 90 .insert(stencil.apply_params(&self.func.params)); 91 92 Ok((compiled_code, false)) 93 } 94 } 95 96 /// Backing storage for an incremental compilation cache, when enabled. 97 pub trait CacheKvStore { 98 /// Given a cache key hash, retrieves the associated opaque serialized data. 99 fn get(&self, key: &[u8]) -> Option<Cow<[u8]>>; 100 101 /// Given a new cache key and a serialized blob obtained from `serialize_compiled`, stores it 102 /// in the cache store. 103 fn insert(&mut self, key: &[u8], val: Vec<u8>); 104 } 105 106 /// Hashed `CachedKey`, to use as an identifier when looking up whether a function has already been 107 /// compiled or not. 108 #[derive(Clone, Hash, PartialEq, Eq)] 109 pub struct CacheKeyHash([u8; 32]); 110 111 impl std::fmt::Display for CacheKeyHash { 112 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 113 write!(f, "CacheKeyHash:{:?}", self.0) 114 } 115 } 116 117 #[derive(serde::Serialize, serde::Deserialize)] 118 struct CachedFunc { 119 // Note: The version marker must be first to ensure deserialization stops in case of a version 120 // mismatch before attempting to deserialize the actual compiled code. 121 version_marker: VersionMarker, 122 stencil: CompiledCodeStencil, 123 } 124 125 /// Key for caching a single function's compilation. 126 /// 127 /// If two functions get the same `CacheKey`, then we can reuse the compiled artifacts, modulo some 128 /// fixups. 129 /// 130 /// Note: the key will be invalidated across different versions of cranelift, as the 131 /// `FunctionStencil` contains a `VersionMarker` itself. 132 #[derive(Hash)] 133 struct CacheKey<'a> { 134 stencil: &'a FunctionStencil, 135 parameters: CompileParameters, 136 } 137 138 #[derive(Clone, PartialEq, Hash, serde::Serialize, serde::Deserialize)] 139 struct CompileParameters { 140 isa: String, 141 triple: String, 142 flags: String, 143 isa_flags: Vec<String>, 144 } 145 146 impl CompileParameters { 147 fn from_isa(isa: &dyn TargetIsa) -> Self { 148 Self { 149 isa: isa.name().to_owned(), 150 triple: isa.triple().to_string(), 151 flags: isa.flags().to_string(), 152 isa_flags: isa 153 .isa_flags() 154 .into_iter() 155 .map(|v| v.value_string()) 156 .collect(), 157 } 158 } 159 } 160 161 impl<'a> CacheKey<'a> { 162 /// Creates a new cache store key for a function. 163 /// 164 /// This is a bit expensive to compute, so it should be cached and reused as much as possible. 165 fn new(isa: &dyn TargetIsa, f: &'a mut Function) -> Self { 166 // Make sure the blocks and instructions are sequenced the same way as we might 167 // have serialized them earlier. This is the symmetric of what's done in 168 // `try_load`. 169 let mut block = f.stencil.layout.entry_block().expect("Missing entry block"); 170 loop { 171 f.stencil.layout.full_block_renumber(block); 172 if let Some(next_block) = f.stencil.layout.next_block(block) { 173 block = next_block; 174 } else { 175 break; 176 } 177 } 178 CacheKey { 179 stencil: &f.stencil, 180 parameters: CompileParameters::from_isa(isa), 181 } 182 } 183 } 184 185 /// Compute a cache key, and hash it on your behalf. 186 /// 187 /// Since computing the `CacheKey` is a bit expensive, it should be done as least as possible. 188 pub fn compute_cache_key(isa: &dyn TargetIsa, func: &mut Function) -> CacheKeyHash { 189 use core::hash::{Hash as _, Hasher}; 190 use sha2::Digest as _; 191 192 struct Sha256Hasher(sha2::Sha256); 193 194 impl Hasher for Sha256Hasher { 195 fn finish(&self) -> u64 { 196 panic!("Sha256Hasher doesn't support finish!"); 197 } 198 fn write(&mut self, bytes: &[u8]) { 199 self.0.update(bytes); 200 } 201 } 202 203 let cache_key = CacheKey::new(isa, func); 204 205 let mut hasher = Sha256Hasher(sha2::Sha256::new()); 206 cache_key.hash(&mut hasher); 207 let hash: [u8; 32] = hasher.0.finalize().into(); 208 209 CacheKeyHash(hash) 210 } 211 212 /// Given a function that's been successfully compiled, serialize it to a blob that the caller may 213 /// store somewhere for future use by `try_finish_recompile`. 214 /// 215 /// As this function requires ownership on the `CompiledCodeStencil`, it gives it back at the end 216 /// of the function call. The value is left untouched. 217 pub fn serialize_compiled( 218 result: CompiledCodeStencil, 219 ) -> (CompiledCodeStencil, Result<Vec<u8>, bincode::Error>) { 220 let cached = CachedFunc { 221 version_marker: VersionMarker, 222 stencil: result, 223 }; 224 let result = bincode::serialize(&cached); 225 (cached.stencil, result) 226 } 227 228 /// An error returned when recompiling failed. 229 #[derive(Debug)] 230 pub enum RecompileError { 231 /// The version embedded in the cache entry isn't the same as cranelift's current version. 232 VersionMismatch, 233 /// An error occurred while deserializing the cache entry. 234 Deserialize(bincode::Error), 235 } 236 237 impl fmt::Display for RecompileError { 238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 239 match self { 240 RecompileError::VersionMismatch => write!(f, "cranelift version mismatch",), 241 RecompileError::Deserialize(err) => { 242 write!(f, "bincode failed during deserialization: {err}") 243 } 244 } 245 } 246 } 247 248 /// Given a function that's been precompiled and its entry in the caching storage, try to shortcut 249 /// compilation of the given function. 250 /// 251 /// Precondition: the bytes must have retrieved from a cache store entry which hash value 252 /// is strictly the same as the `Function`'s computed hash retrieved from `compute_cache_key`. 253 pub fn try_finish_recompile(func: &Function, bytes: &[u8]) -> Result<CompiledCode, RecompileError> { 254 match bincode::deserialize::<CachedFunc>(bytes) { 255 Ok(result) => { 256 if result.version_marker != func.stencil.version_marker { 257 Err(RecompileError::VersionMismatch) 258 } else { 259 Ok(result.stencil.apply_params(&func.params)) 260 } 261 } 262 Err(err) => Err(RecompileError::Deserialize(err)), 263 } 264 } 265