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