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