Skip to main content

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}