qcode_vm/optimize.rs
1//! A cheap cleanup round for freshly lifted code.
2//!
3//! SLEIGH expresses an instruction's semantics through *unique* (temporary)
4//! space: an intermediate result is stored to a temporary and immediately loaded
5//! back. Lifting `add eax, ebx` produces around forty QCode operations, roughly
6//! half of which are this round trip:
7//!
8//! ```text
9//! i32 %tmpe = %eax_3 ^ %ebx_3;
10//! store($temp1:4, 0 <- %tmpe);
11//! i32 %tmp10 = load($temp1:4, 0); // reloads what was just stored
12//! ```
13//!
14//! Plain dead-code elimination cannot touch this — the store has a user, and the
15//! load has users — so the interpreter re-executes the whole round trip on every
16//! pass over the block. Forwarding the stored value to the load removes both.
17//!
18//! This is deliberately the *limited* version of store-to-load forwarding, not
19//! `mem2reg`: it is block-local, needs no alias
20//! analysis, and is linear in the size of the block, so it can run on every
21//! lifted block without reintroducing the quadratic cost that a whole-function
22//! pass would.
23//!
24//! # Why this is sound
25//!
26//! Only *temporary* spaces are forwarded. A SLEIGH unique is scratch private to
27//! one instruction's semantics: it is written before it is read and does not
28//! outlive the block, so no other block, and no guest-visible memory access, can
29//! observe it. Registers and RAM are left alone, since a call, a fault handler,
30//! or another block legitimately observes those.
31//!
32//! Two further restrictions keep it honest:
33//!
34//! * A load is forwarded only when it matches the last store to that space *and
35//! has the same width*. A narrower or wider access is left alone rather than
36//! guessed at.
37//! * A store through a non-constant pointer clears what is known about that
38//! space, since it may land anywhere in it.
39
40use qcode::{
41 context::Context,
42 space::{MemorySpaceId, Space, SpaceType},
43 value::{
44 BasicBlock, BlockId, Instruction, ValueId, ValueRef,
45 insn::{InstructionId, Load, Mnemonic, Store},
46 },
47};
48use rustc_hash::{FxHashMap, FxHashSet};
49
50/// What one cleanup round changed.
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
52pub struct Cleanup {
53 /// Loads replaced by the value that was stored.
54 pub forwarded_loads: usize,
55 /// Stores removed because nothing reads them any more.
56 pub removed_stores: usize,
57}
58
59impl Cleanup {
60 pub fn is_empty(&self) -> bool {
61 self.forwarded_loads == 0 && self.removed_stores == 0
62 }
63}
64
65/// Whether `space` is scratch private to the block being lifted.
66fn is_temporary(ctx: &Context<'_>, space: MemorySpaceId) -> bool {
67 match space {
68 // A per-function temporary space is scratch by construction.
69 MemorySpaceId::Temp(_) => true,
70 MemorySpaceId::Shared(id) => {
71 matches!(Space::from_id(ctx, id).ty, SpaceType::Unique)
72 }
73 }
74}
75
76/// The constant address a pointer denotes, if it is one.
77///
78/// SLEIGH addresses a temporary by a `Temp` value rather than a literal — the
79/// interpreter evaluates one to its address — so both forms are constants here.
80fn constant_address(ctx: &Context<'_>, ptr: ValueId) -> Option<u64> {
81 match ValueRef::new(ptr, ctx) {
82 ValueRef::Literal(literal) => Some(literal.value()),
83 ValueRef::Temp(temp) => Some(temp.address() as u64),
84 ValueRef::Varnode(varnode) => Some(varnode.address() as u64),
85 _ => None,
86 }
87}
88
89/// Forwards temporary-space stores to the loads that read them back, in
90/// `block_id` only.
91pub fn forward_temp_stores(ctx: &mut Context<'_>, block_id: BlockId) -> Cleanup {
92 let func = block_id.func;
93 let insn_ids: Vec<InstructionId> = BasicBlock::from_id(ctx, block_id).instruction_ids();
94
95 /// The operands of the one or two mnemonics this pass reads, copied out of
96 /// the instruction so that nothing has to be cloned to look at them.
97 enum Access {
98 Store {
99 space: qcode::space::LocalMemorySpaceId,
100 ptr: qcode::value::LocalValueId,
101 size: usize,
102 src: qcode::value::LocalValueId,
103 },
104 Load {
105 space: qcode::space::LocalMemorySpaceId,
106 ptr: qcode::value::LocalValueId,
107 size: usize,
108 },
109 }
110
111 /// The value last stored to a temporary location, and its width.
112 struct Stored {
113 value: ValueId,
114 size: usize,
115 store: InstructionId,
116 }
117
118 let mut available: FxHashMap<(MemorySpaceId, u64), Stored> = FxHashMap::default();
119 // Stores whose every reader was forwarded. A store still standing at the end
120 // of the block is kept: something outside this pass's knowledge may read it.
121 let mut redundant: FxHashSet<(InstructionId, (MemorySpaceId, u64))> = FxHashSet::default();
122 // Spaces where an access through an unknown pointer was seen: nothing about
123 // them can be concluded, so none of their stores may be removed.
124 let mut poisoned: FxHashSet<MemorySpaceId> = FxHashSet::default();
125 // Slots that were read by something other than a forwarded load.
126 let mut read_otherwise: FxHashSet<(MemorySpaceId, u64)> = FxHashSet::default();
127 let mut consumed: FxHashSet<InstructionId> = FxHashSet::default();
128 let mut forwards: Vec<(ValueId, ValueId)> = Vec::new();
129
130 for &insn_id in &insn_ids {
131 // The operands are copied out rather than the mnemonic cloned. A
132 // `Mnemonic` owns its argument list, so cloning one allocates; doing it
133 // for every instruction of every lifted block made allocation a
134 // measurable share of translation time, to read four `Copy` ids.
135 let accessed = match *Instruction::from_id(ctx, insn_id).mnemonic() {
136 Mnemonic::Store(Store {
137 space,
138 ptr,
139 size,
140 src,
141 }) => Access::Store {
142 space,
143 ptr,
144 size,
145 src,
146 },
147 Mnemonic::Load(Load { space, ptr, size }) => Access::Load { space, ptr, size },
148 _ => continue,
149 };
150 match accessed {
151 Access::Store {
152 space,
153 ptr,
154 size,
155 src,
156 } => {
157 let space = space.qualify(func);
158 if !is_temporary(ctx, space) {
159 continue;
160 }
161 match constant_address(ctx, ptr.qualify(func)) {
162 Some(addr) => {
163 // Overwriting an earlier store to the same slot whose
164 // readers were all forwarded makes the earlier one dead.
165 available.insert(
166 (space, addr),
167 Stored {
168 value: src.qualify(func),
169 size,
170 store: insn_id,
171 },
172 );
173 }
174 // An unknown destination may be anywhere in the space, so
175 // nothing known about it survives.
176 None => {
177 poisoned.insert(space);
178 available.retain(|(other, _), _| *other != space);
179 }
180 }
181 }
182 Access::Load { space, ptr, size } => {
183 let space = space.qualify(func);
184 if !is_temporary(ctx, space) {
185 continue;
186 }
187 let Some(addr) = constant_address(ctx, ptr.qualify(func)) else {
188 // An unknown source could read any of this space, so no
189 // store to it can be considered fully consumed.
190 poisoned.insert(space);
191 available.retain(|(other, _), _| *other != space);
192 continue;
193 };
194 match available.get(&(space, addr)) {
195 // Same slot, same width: the load is exactly the value that
196 // was stored.
197 Some(stored) if stored.size == size => {
198 forwards.push((ValueId::Instruction(insn_id), stored.value));
199 consumed.insert(insn_id);
200 redundant.insert((stored.store, (space, addr)));
201 }
202 // A different width reads bytes this pass does not model,
203 // so the store behind it is genuinely read.
204 Some(_) => {
205 read_otherwise.insert((space, addr));
206 available.remove(&(space, addr));
207 }
208 None => {
209 read_otherwise.insert((space, addr));
210 }
211 }
212 }
213 }
214 }
215
216 if forwards.is_empty() {
217 return Cleanup::default();
218 }
219
220 let forwarded_loads = forwards.len();
221
222 // A forwarded value can itself be a load this pass is removing: one slot's
223 // stored value is another slot's forwarded load. Rewriting uses in program
224 // order would then reintroduce a reference to an instruction about to go,
225 // so each target is followed to the value that actually survives first.
226 let mut resolved: FxHashMap<ValueId, ValueId> = FxHashMap::default();
227 for &(load_result, stored_value) in &forwards {
228 let survivor = resolved.get(&stored_value).copied().unwrap_or(stored_value);
229 resolved.insert(load_result, survivor);
230 }
231
232 let body = ctx.function_mut(func);
233 for (load_result, _) in forwards {
234 let survivor = resolved[&load_result];
235 body.replace_all_uses_with(load_result, survivor);
236 }
237 // A store goes only when every read of its slot in this block was
238 // forwarded and nothing accessed the space through an unknown pointer.
239 let dead_stores: Vec<InstructionId> = redundant
240 .iter()
241 .filter(|(_, slot)| !poisoned.contains(&slot.0) && !read_otherwise.contains(slot))
242 .map(|(store, _)| *store)
243 .collect();
244 // Removed in one pass over the block: unlinking them one at a time walks
245 // the instruction list once per instruction, which on an absorbed guest
246 // basic block is the dominant cost of the whole round.
247 let dead: FxHashSet<_> = consumed
248 .iter()
249 .chain(dead_stores.iter())
250 .map(|id| id.localize(func))
251 .collect();
252 body.remove_block_instructions(block_id, &dead);
253
254 Cleanup {
255 forwarded_loads,
256 removed_stores: dead_stores.len(),
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use qcode::value::FunctionBody;
264
265 /// Registers and RAM must be left alone even though the shape matches.
266 #[test]
267 fn only_temporary_spaces_are_forwarded() {
268 let mut ctx = Context::new();
269 let function = FunctionBody::make_at_addr(&mut ctx, 0x1000, None).id;
270 let block = BasicBlock::make(&mut ctx, function).with_address(0x1000).id;
271 // The default space is RAM, which this pass must not touch.
272 let ram = MemorySpaceId::Shared(ctx.shared.default_space);
273 assert!(!is_temporary(&ctx, ram));
274 assert_eq!(forward_temp_stores(&mut ctx, block), Cleanup::default());
275 }
276
277 #[test]
278 fn an_empty_block_is_unchanged() {
279 let mut ctx = Context::new();
280 let function = FunctionBody::make_at_addr(&mut ctx, 0x1000, None).id;
281 let block = BasicBlock::make(&mut ctx, function).with_address(0x1000).id;
282 assert!(forward_temp_stores(&mut ctx, block).is_empty());
283 }
284}