qcode_jit/jit.rs
1//! Owning, caching and running compiled blocks.
2
3use cranelift::prelude::*;
4use cranelift_jit::{JITBuilder, JITModule};
5use cranelift_module::{Linkage, Module};
6use qcode::{
7 context::Context,
8 value::{
9 BlockId, ValueId,
10 insn::{InstructionId, Mnemonic},
11 },
12};
13use qcode_emulator::{EmulatorErrorKind, SizedValue, StandaloneEmulator};
14use qcode_vm::{
15 BlockExecutor, Executed, VmMemory, qcode_jit_load, qcode_jit_sdiv128, qcode_jit_srem128,
16 qcode_jit_store, qcode_jit_udiv128, qcode_jit_urem128,
17};
18
19use crate::compile::{BLOCK_OK, BlockTranslator, Export, Helpers, SpaceTable, Unsupported};
20
21/// What is known about one block: the index of its native code, or the reason
22/// the compiler declined it, together with the instruction count that answer
23/// was reached for.
24type CacheEntry = Option<(usize, Result<usize, Unsupported>)>;
25
26/// A block that has been compiled to native code.
27struct Compiled {
28 /// The compiled body. Its arguments are the base of an array of space base
29 /// pointers, in the order [`SpaceTable`] records, the base of the export
30 /// buffer (one `u64` slot per entry of `exports`), the base of the guest's
31 /// software TLB, and the `VmMemory` its slow paths call back into. It
32 /// returns [`BLOCK_OK`], or [`BLOCK_FAULT`](crate::compile::BLOCK_FAULT) if
33 /// an access faulted and the block stopped there.
34 entry: extern "C" fn(*const *mut u8, *mut u64, *mut u8, *mut VmMemory) -> i32,
35 table: SpaceTable,
36 /// The terminator operands this block computes, in slot order.
37 exports: Vec<Export>,
38 /// How many body instructions this block has — everything but the
39 /// terminator. The caller needs it to position the interpreter, and taking
40 /// it from here saves resolving the block through the module arena again.
41 body_len: usize,
42 /// Where each of `table`'s spaces lives in the machine's flat storage.
43 ///
44 /// Resolved on first execution and kept: a slot is stable for the life of
45 /// the spaces, so re-entering a hot block costs an array index per space
46 /// rather than a map lookup.
47 slots: Vec<usize>,
48}
49
50/// How much work the JIT is taking, and how much it is declining.
51#[derive(Debug, Default, Clone)]
52pub struct JitStats {
53 /// Blocks translated to native code.
54 pub compiled: u64,
55 /// Blocks the compiler declined; these run on the interpreter.
56 pub declined: u64,
57 /// Executions that ran as native code.
58 pub native_runs: u64,
59}
60
61/// A JIT backend: compiles blocks on first use and runs them thereafter.
62///
63/// Holding the [`JITModule`] means compiled code lives as long as this does.
64pub struct Jit {
65 module: JITModule,
66 /// The runtime's slow-path accessors, declared once and referenced by every
67 /// compiled block.
68 helpers: Helpers,
69 /// Compiled blocks, indexed by the handles in `cache`.
70 compiled: Vec<Compiled>,
71 /// What is known about each block: an index into `compiled`, or the reason
72 /// it was declined. Declining is cached too, so a block the compiler cannot
73 /// take is only examined once.
74 ///
75 /// Each entry records the instruction count it was made for, because a
76 /// block is *not* immutable here: a VM that lifts on demand first presents
77 /// an empty placeholder (which the compiler rightly declines), then fills
78 /// it, then runs a cleanup pass over it. Trusting the entry regardless of
79 /// count would freeze that first decline forever and the block would never
80 /// be compiled.
81 ///
82 /// Stored as slots indexed by the block id's two components rather than in
83 /// a map: this is read on *every* block execution, and hashing a
84 /// `(function, local)` pair each time was a measurable share of run time.
85 /// Sparse ids cost only an unused slot.
86 cache: Vec<Vec<CacheEntry>>,
87 /// Reused across runs so a hot block does not allocate to be entered.
88 scratch: Vec<*mut u8>,
89 /// Likewise for the export buffer compiled code writes its terminator
90 /// operands into.
91 exports: Vec<u64>,
92 pub stats: JitStats,
93}
94
95impl Default for Jit {
96 fn default() -> Self {
97 Self::new()
98 }
99}
100
101impl Jit {
102 pub fn new() -> Self {
103 let mut flags = settings::builder();
104 // Compilation happens on the guest's critical path, so favour getting
105 // through it over the last few percent of code quality.
106 flags
107 .set("opt_level", "speed")
108 .expect("opt_level is a known flag");
109 // The verifier re-checks Cranelift IR this backend has just built, on
110 // the guest's critical path, for every block. It is a development aid
111 // for the compiler itself; what guards *this* translation is the
112 // divergence harness, which compares compiled code against the
113 // interpreter block by block over whole programs.
114 flags
115 .set("enable_verifier", "false")
116 .expect("enable_verifier is a known flag");
117 let isa = cranelift_native::builder()
118 .expect("host is a supported target")
119 .finish(settings::Flags::new(flags))
120 .expect("isa builds for the host");
121 let mut builder = JITBuilder::with_isa(isa, cranelift_module::default_libcall_names());
122 // The two calls compiled code makes. Registered by name because that is
123 // how Cranelift resolves an external function; the addresses are this
124 // process's own, so there is no dynamic loading involved.
125 builder.symbol("qcode_jit_load", qcode_jit_load as *const u8);
126 builder.symbol("qcode_jit_store", qcode_jit_store as *const u8);
127 builder.symbol("qcode_jit_udiv128", qcode_jit_udiv128 as *const u8);
128 builder.symbol("qcode_jit_urem128", qcode_jit_urem128 as *const u8);
129 builder.symbol("qcode_jit_sdiv128", qcode_jit_sdiv128 as *const u8);
130 builder.symbol("qcode_jit_srem128", qcode_jit_srem128 as *const u8);
131 let mut module = JITModule::new(builder);
132
133 let mut load_sig = module.make_signature();
134 // (memory, address, size, out) -> status
135 load_sig.params.push(AbiParam::new(types::I64));
136 load_sig.params.push(AbiParam::new(types::I64));
137 load_sig.params.push(AbiParam::new(types::I32));
138 load_sig.params.push(AbiParam::new(types::I64));
139 load_sig.returns.push(AbiParam::new(types::I32));
140 let load = module
141 .declare_function("qcode_jit_load", Linkage::Import, &load_sig)
142 .expect("the load helper declares once");
143
144 let mut store_sig = module.make_signature();
145 // (memory, address, size, value) -> status
146 store_sig.params.push(AbiParam::new(types::I64));
147 store_sig.params.push(AbiParam::new(types::I64));
148 store_sig.params.push(AbiParam::new(types::I32));
149 store_sig.params.push(AbiParam::new(types::I64));
150 store_sig.returns.push(AbiParam::new(types::I32));
151 let store = module
152 .declare_function("qcode_jit_store", Linkage::Import, &store_sig)
153 .expect("the store helper declares once");
154
155 // (a low, a high, b low, b high, out) -> ()
156 let mut divide_sig = module.make_signature();
157 for _ in 0..5 {
158 divide_sig.params.push(AbiParam::new(types::I64));
159 }
160 let mut wide_division = |name: &str| {
161 module
162 .declare_function(name, Linkage::Import, ÷_sig)
163 .expect("a division helper declares once")
164 };
165 let divisions = [
166 wide_division("qcode_jit_udiv128"),
167 wide_division("qcode_jit_urem128"),
168 wide_division("qcode_jit_sdiv128"),
169 wide_division("qcode_jit_srem128"),
170 ];
171
172 Self {
173 module,
174 helpers: Helpers {
175 load,
176 store,
177 divisions,
178 },
179 compiled: Vec::new(),
180 cache: Vec::new(),
181 scratch: Vec::new(),
182 exports: Vec::new(),
183 stats: JitStats::default(),
184 }
185 }
186
187 /// Whether `block` has native code, compiling it on first sight.
188 ///
189 /// A decline is remembered, so an unsupported block costs one compilation
190 /// attempt over the life of the machine rather than one per execution.
191 fn resolve(&mut self, ctx: &Context<'_>, block: BlockId) -> Result<usize, Unsupported> {
192 let count = ctx.block(block).instruction_ids().len();
193 let func: usize = block.func.into();
194 let local: usize = block.local.into();
195 if let Some(Some((cached_count, known))) =
196 self.cache.get(func).and_then(|slots| slots.get(local))
197 && *cached_count == count
198 {
199 return known.clone();
200 }
201
202 let outcome = self.compile(ctx, block);
203 match &outcome {
204 Ok(_) => self.stats.compiled += 1,
205 Err(_) => self.stats.declined += 1,
206 }
207 if func >= self.cache.len() {
208 self.cache.resize_with(func + 1, Vec::new);
209 }
210 let slots = &mut self.cache[func];
211 if local >= slots.len() {
212 slots.resize(local + 1, None);
213 }
214 slots[local] = Some((count, outcome.clone()));
215 outcome
216 }
217
218 fn compile(&mut self, ctx: &Context<'_>, block: BlockId) -> Result<usize, Unsupported> {
219 let body_len = ctx.block(block).instruction_ids().len().saturating_sub(1);
220 let mut signature = self.module.make_signature();
221 // spaces, exports, tlb, memory.
222 for _ in 0..4 {
223 signature.params.push(AbiParam::new(types::I64));
224 }
225 signature.returns.push(AbiParam::new(types::I32));
226
227 let name = format!("qcode_block_{}_{}", self.compiled.len(), self.cache.len());
228 let id = self
229 .module
230 .declare_function(&name, Linkage::Export, &signature)
231 .map_err(|_| Unsupported::Mnemonic("function declaration failed"))?;
232
233 let mut context = self.module.make_context();
234 context.func.signature = signature;
235 let helpers = crate::compile::HelperRefs {
236 load: self
237 .module
238 .declare_func_in_func(self.helpers.load, &mut context.func),
239 store: self
240 .module
241 .declare_func_in_func(self.helpers.store, &mut context.func),
242 divisions: self
243 .helpers
244 .divisions
245 .map(|id| self.module.declare_func_in_func(id, &mut context.func)),
246 };
247
248 // A fresh builder context per attempt: a declined block abandons its
249 // half-built function, which would leave a shared context dirty and
250 // trip Cranelift's emptiness assertion on the next compilation.
251 let mut builder_ctx = FunctionBuilderContext::new();
252 let (table, exports) = {
253 let mut builder = FunctionBuilder::new(&mut context.func, &mut builder_ctx);
254 let entry = builder.create_block();
255 builder.append_block_params_for_function_params(entry);
256 builder.switch_to_block(entry);
257 builder.seal_block(entry);
258
259 let mut translator = BlockTranslator::new(ctx, builder, entry, helpers);
260 match translator.translate_body(block) {
261 Ok(()) => {
262 let compiled = (translator.table.clone(), translator.exports.clone());
263 translator.finish();
264 compiled
265 }
266 Err(unsupported) => {
267 // The half-built function is simply dropped; nothing was
268 // defined in the module, so there is nothing to undo.
269 self.module.clear_context(&mut context);
270 return Err(unsupported);
271 }
272 }
273 };
274
275 self.module
276 .define_function(id, &mut context)
277 .map_err(|_| Unsupported::Mnemonic("function definition failed"))?;
278 self.module.clear_context(&mut context);
279 self.module
280 .finalize_definitions()
281 .map_err(|_| Unsupported::Mnemonic("finalization failed"))?;
282
283 let code = self.module.get_finalized_function(id);
284 // SAFETY: `code` is the entry point Cranelift just finalized for the
285 // signature declared above — four pointer-width arguments and a 32-bit
286 // status result.
287 let entry = unsafe {
288 std::mem::transmute::<
289 *const u8,
290 extern "C" fn(*const *mut u8, *mut u64, *mut u8, *mut VmMemory) -> i32,
291 >(code)
292 };
293
294 self.compiled.push(Compiled {
295 entry,
296 table,
297 exports,
298 body_len,
299 slots: Vec::new(),
300 });
301 Ok(self.compiled.len() - 1)
302 }
303
304 /// Compiles `block` without running it, reporting why if it is declined.
305 ///
306 /// For tooling that wants to report coverage over a module.
307 pub fn try_compile(&mut self, ctx: &Context<'_>, block: BlockId) -> Result<(), Unsupported> {
308 self.resolve(ctx, block).map(|_| ())
309 }
310
311 /// Runs `block` as native code, if it has any.
312 ///
313 /// Returns `Ok(None)` when the block is not compiled, which is the caller's
314 /// signal to run it on the interpreter instead, and `Ok(Some(n))` when it
315 /// ran, where `n` is the number of body instructions it retired.
316 pub fn run_block(
317 &mut self,
318 ctx: &Context<'_>,
319 emu: &mut StandaloneEmulator<VmMemory>,
320 block: BlockId,
321 chain: bool,
322 ) -> Result<Option<Executed>, EmulatorErrorKind> {
323 let mut current = block;
324 let mut retired = 0;
325 loop {
326 let Ok(index) = self.resolve(ctx, current) else {
327 // Nothing compiled here. If earlier blocks ran, the machine is
328 // already at `current`'s start and the interpreter takes over
329 // from there; otherwise this call did nothing at all.
330 return Ok((retired > 0).then_some(Executed {
331 block: current,
332 body: 0,
333 retired,
334 }));
335 };
336 let body = self.enter(ctx, emu, index)?;
337 retired += body as u64;
338 self.stats.native_runs += 1;
339
340 // Only continue while the successor is one this backend can also
341 // run: deciding the branch here is what keeps control inside
342 // compiled code, and the interpreter would otherwise redo it.
343 let next = if chain {
344 self.next_block(ctx, emu, current)
345 } else {
346 None
347 };
348 let Some(next) = next.filter(|&next| self.is_compiled(ctx, next)) else {
349 return Ok(Some(Executed {
350 block: current,
351 body,
352 retired,
353 }));
354 };
355 // Chaining means this block's terminator was decided here rather
356 // than by the interpreter, so it is retired work nobody else will
357 // count. Only the *last* block's terminator is left to the caller.
358 retired += 1;
359 current = next;
360 }
361 }
362
363 /// Whether `block` has native code, without compiling it.
364 fn is_compiled(&mut self, ctx: &Context<'_>, block: BlockId) -> bool {
365 self.resolve(ctx, block).is_ok()
366 }
367
368 /// The successor this block's terminator selects, when that is a decision
369 /// the backend can make: an argument-less branch, or a conditional one
370 /// whose condition the compiled body has just exported.
371 ///
372 /// `None` means "leave it to the interpreter" — an indirect branch, a call,
373 /// a return, or any edge that binds block arguments, all of which stay in
374 /// one implementation.
375 fn next_block(
376 &self,
377 ctx: &Context<'_>,
378 emu: &StandaloneEmulator<VmMemory>,
379 block: BlockId,
380 ) -> Option<BlockId> {
381 let &terminator = ctx.block(block).instruction_ids().last()?;
382 let terminator = InstructionId::new(block.func, terminator);
383 let insn = qcode::value::Instruction::from_id(ctx, terminator);
384 let target = match insn.mnemonic() {
385 Mnemonic::Branch(branch) if branch.args.is_empty() => branch.target,
386 Mnemonic::CBranch(cbranch)
387 if cbranch.success_args.is_empty() && cbranch.failure_args.is_empty() =>
388 {
389 let ValueId::Instruction(condition) = cbranch.condition.qualify(block.func) else {
390 return None;
391 };
392 let taken = emu.insn_values.get(&condition)?.as_bits() != 0;
393 if taken {
394 cbranch.success_block
395 } else {
396 cbranch.failure_block
397 }
398 }
399 _ => return None,
400 };
401 Some(BlockId::new(block.func, target))
402 }
403
404 /// Runs one compiled block, leaving its terminator's operands where the
405 /// interpreter would have put them. Returns how many body instructions it
406 /// retired.
407 fn enter(
408 &mut self,
409 _ctx: &Context<'_>,
410 emu: &mut StandaloneEmulator<VmMemory>,
411 index: usize,
412 ) -> Result<usize, EmulatorErrorKind> {
413 let compiled = &mut self.compiled[index];
414 // Taken as a raw pointer, and everything below derived from it: the
415 // compiled block holds base pointers into the flat spaces *while*
416 // calling back into this same `VmMemory` for the RAM accesses it could
417 // not settle inline. A live `&mut` spanning the call would make those
418 // base pointers ones the compiler is entitled to assume nothing else
419 // reaches.
420 let memory: *mut VmMemory = &raw mut emu.memory;
421
422 // SAFETY: for every dereference of `memory` here — it points to the
423 // emulator's own memory, which outlives this call, and no reference to
424 // it is held across any of them.
425 if compiled.slots.is_empty() {
426 compiled.slots = compiled
427 .table
428 .entries()
429 .iter()
430 .map(|&(space, _)| unsafe { (*memory).flat_mut().slot(space) })
431 .collect();
432 }
433
434 // Each space is grown to the size the block needs *before* its base
435 // pointer is taken: growing reallocates, and compiled code holds these
436 // pointers for the duration of the call.
437 self.scratch.clear();
438 for (&slot, &(_, required)) in compiled.slots.iter().zip(compiled.table.entries()) {
439 self.scratch
440 .push(unsafe { (*memory).flat_mut().base_ptr_at(slot, required)? });
441 }
442 self.exports.clear();
443 self.exports.resize(compiled.exports.len(), 0);
444 // The TLB moves only with the memory itself, which is pinned for the
445 // duration of the call.
446 let tlb = unsafe { (*memory).mmu.tlb_ptr() };
447
448 // SAFETY: the function was compiled from this block and reads and writes
449 // only within the byte ranges recorded in its space table, each of which
450 // has just been made addressable, plus the export buffer, which has just
451 // been sized to the slot count that same compilation recorded, plus
452 // guest RAM through the TLB and the memory it is handed.
453 let status = (compiled.entry)(
454 self.scratch.as_ptr(),
455 self.exports.as_mut_ptr(),
456 tlb,
457 memory,
458 );
459
460 // A faulting access stopped the block where it happened. The fault is
461 // left where an interpreted one would be, for the VM to turn into an
462 // exit; what goes back from here is only the error the interpreter's
463 // own signature can carry.
464 if status != BLOCK_OK as i32 {
465 // SAFETY: as above.
466 let fault = unsafe { (*memory).fault() };
467 let fault = fault.ok_or(EmulatorErrorKind::MemoryReadError(0))?;
468 return Err(if fault.is_write() {
469 EmulatorErrorKind::MemoryWriteError(fault.addr)
470 } else {
471 EmulatorErrorKind::MemoryReadError(fault.addr)
472 });
473 }
474
475 // The terminator is still the interpreter's to run, so the operands it
476 // reads have to look as though the interpreter had computed them.
477 for (export, &bits) in compiled.exports.iter().zip(&self.exports) {
478 emu.insn_values
479 .insert(export.insn, SizedValue::new(bits, export.size));
480 }
481
482 Ok(compiled.body_len)
483 }
484}
485
486/// Lets a [`Jit`] be installed on a machine as its block executor.
487impl BlockExecutor for Jit {
488 fn run_block(
489 &mut self,
490 ctx: &Context<'_>,
491 emu: &mut StandaloneEmulator<VmMemory>,
492 block: BlockId,
493 chain: bool,
494 ) -> Result<Option<Executed>, EmulatorErrorKind> {
495 Jit::run_block(self, ctx, emu, block, chain)
496 }
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502
503 #[test]
504 fn a_fresh_jit_has_compiled_nothing() {
505 let jit = Jit::new();
506 assert_eq!(jit.stats.compiled, 0);
507 assert_eq!(jit.stats.declined, 0);
508 assert_eq!(jit.stats.native_runs, 0);
509 }
510}