Skip to main content

rucc_codegen/
compare.rs

1//! Taking out a comparison the machine has already made.
2//!
3//! Design: `spec/optimizer/37-machine-level-optimization.md` section 37.4.
4//!
5//! A comparison produces no value. It sets a few bits nobody named and the instruction behind it
6//! reads them, so a comparison that sets the bits that are already there is one nothing could tell
7//! had run. There are two ways for that to happen, and both of them are about an instruction a
8//! little way in front rather than about a dataflow the whole function takes part in.
9//!
10//! The same comparison twice. `if (x == y) ... else if (x != y)` and every expression that asks a
11//! question and then asks its negation come out as two comparisons of the same two registers with
12//! nothing between them but the bytes each one kept. The second asks what the first asked and the
13//! answer has not moved.
14//!
15//! A comparison against zero of something arithmetic has just worked out. `if (a & MASK)` is an
16//! `and` and then a comparison of its result against zero, and the `and` set the bits that
17//! comparison would have set on its way past. This is the common one by a long way: at `-O2` over
18//! the SQLite amalgamation there are 2250 of these and 0 of the other shape.
19//!
20//! # Why it runs after the layout rather than before
21//!
22//! Because this is the second pass to work on a pair of instructions whose middle has to stay
23//! empty, and the first is the block layout. A branch on a comparison is written there as the
24//! comparison with its byte taken off and a jump that reads the condition state, and what is
25//! between those two is live and is not a register, so anything that ran afterwards and put an
26//! instruction between them would be wrong. Running last is the whole of what makes this safe,
27//! which is the sentence section 37.4 uses about the layout itself.
28//!
29//! It also makes the two shapes one shape. A comparison the layout folded a branch into is a
30//! comparison that keeps nothing, one whose byte something else wanted is a comparison that keeps
31//! a byte, and after the layout both are sitting in a block to be looked at the same way. Before
32//! the layout the first kind does not exist yet, so a pass that ran earlier would have to either
33//! leave every branch alone or undo the fusion to get at one.
34//!
35//! # What a block boundary is
36//!
37//! The end of everything this knows. The state a comparison leaves is not a register and nothing
38//! in this back end carries one from a block to its successors: the layout writes the jump that
39//! reads a comparison into the same block as the comparison, which is the only place one is read
40//! at all. So the walk starts each block knowing nothing, which is what makes it a walk rather
41//! than a dataflow.
42//!
43//! # What it will not do
44//!
45//! A comparison with anything between it and the instruction that already made it that writes the
46//! condition state. The target says which instructions those are and says it about every name it
47//! does not recognise, so an opcode added to a rule set and not to that description makes this
48//! find less rather than making it wrong.
49//!
50//! A comparison of a register something wrote in between. The bits are still the bits the earlier
51//! instruction left, but they are about what the register held then and the comparison is about
52//! what it holds now. Every definition between the two is checked against the registers the
53//! earlier one was about, which are physical by the time this runs and so are the ones the machine
54//! will really read.
55//!
56//! A comparison against zero after arithmetic whose condition reads a part of the condition state
57//! the arithmetic did not leave the way a comparison would have. `subl` says whether its answer
58//! was zero and a comparison of that answer against zero would agree, and it says whether the
59//! subtraction overflowed where the comparison would have said it did not, so a signed `<` after
60//! one reads a sign and an overflow that no longer belong together. [`rucc_target::Zeroing`] is
61//! where each instruction says which conditions it is good for, and every condition that ends up
62//! reading what the arithmetic left has to be one of them, including the ones behind the
63//! comparison rather than on it.
64//!
65//! A comparison against zero after arithmetic that wrote a different number of bits. `andl` leaves
66//! a statement about thirty two bits and `cmpq $0` asks about sixty four, and on this machine the
67//! upper half is then zero and the two disagree about the sign.
68//!
69//! A comparison against zero after arithmetic whose condition state nothing is found to read. That
70//! is a comparison that is dead rather than redundant, and taking a dead one out is a different
71//! question: it needs no earlier instruction at all, so answering it here would mean answering it
72//! only where an earlier instruction happened to be.
73
74use std::collections::HashMap;
75
76use rucc_base::Interner;
77use rucc_mir::{self as mir, Role};
78use rucc_target::{Compare, FlagInsts, Reads, RegClass, Zeroing};
79
80/// A register, and the file it is drawn from.
81///
82/// The class as well as the number, because the two files number from zero and `xmm0` is not
83/// `rax`. The width is deliberately not here: `%al` and `%eax` are one register, so a write of
84/// either is a write of the other and a statement about what the other held is a statement about
85/// a value that has moved.
86type Place = (RegClass, mir::Reg);
87
88/// Takes out every comparison whose condition state the instruction in front of it already left.
89///
90/// Gives back how many went, which the tests read and nothing else does.
91pub fn redundant(func: &mut mir::Func, insts: &FlagInsts, names: &mut Interner) -> usize {
92    // Every name the rewrite could want, before the walk rather than inside it. The walk holds a
93    // name it read out of the interner while it edits the function, and interning a new one there
94    // would be the same interner borrowed twice.
95    let opcodes: HashMap<&str, mir::Opcode> = insts
96        .compares
97        .iter()
98        .filter_map(|entry| entry.kept)
99        .map(|kept| (kept, mir::Opcode::new(names.intern(&format!("{}{kept}", insts.prefix)))))
100        .collect();
101    let names = &*names;
102    let mut gone = 0;
103    for block in func.blocks().collect::<Vec<_>>() {
104        let sequence: Vec<mir::Inst> = func.insts(block).collect();
105        let mut left: Option<Left> = None;
106        for at in 0..sequence.len() {
107            let inst = sequence[at];
108            let Some(name) = opcode(func, insts, names, inst) else {
109                left = None;
110                continue;
111            };
112            left = if let Some(entry) = insts.compare(name) {
113                let already = left
114                    .as_ref()
115                    .is_some_and(|had| had.answers(func, insts, names, &sequence, at, entry));
116                if already {
117                    // What is left of the instruction is asked before the rewrite rather than
118                    // after, because one of the two answers is that there is nothing left of it.
119                    let after = stale(func, inst, left);
120                    take(func, &opcodes, inst, entry);
121                    gone += 1;
122                    after
123                } else {
124                    stale(func, inst, Some(Left::made(func, entry, inst)))
125                }
126            } else if let Some(zeroing) = insts.zeroed(name) {
127                // Before the general question of whether the name writes the condition state,
128                // because every one of these does and this is what it wrote there.
129                Left::zeroed(func, insts, name, zeroing, inst)
130            } else if (insts.writes)(name) {
131                None
132            } else {
133                stale(func, inst, left)
134            };
135        }
136    }
137    gone
138}
139
140/// What the condition state holds, and which registers it is a statement about.
141struct Left {
142    /// Which of the two ways it got there.
143    how: How,
144    /// The registers the statement is about, which anything writing one of makes it stale.
145    about: Vec<Place>,
146}
147
148/// The two ways the condition state comes to hold something this pass can use.
149enum How {
150    /// A comparison made it, and this is the question it asked.
151    Made {
152        /// The name of the comparison that keeps nothing, which is what says two are the same.
153        asks: &'static str,
154        /// What it compared, in the order it read them.
155        read: Vec<Place>,
156        /// The constant it compared against, if it compared against one.
157        imm: Option<i64>,
158    },
159    /// Arithmetic left it, and this is what a comparison against zero has to look like to be one
160    /// the arithmetic already made.
161    Zeroed {
162        /// How wide the value it wrote is.
163        width: u32,
164        /// Which conditions may read what it left.
165        covers: Zeroing,
166    },
167}
168
169impl Left {
170    /// What a comparison leaves behind.
171    fn made(func: &mir::Func, entry: &Compare, inst: mir::Inst) -> Self {
172        let read: Vec<Place> = reads(func, inst).into_iter().map(|(_, place)| place).collect();
173        Self {
174            how: How::Made {
175                asks: entry.asks,
176                read: read.clone(),
177                imm: func[inst].imm.map(|at| func[at].0),
178            },
179            about: read,
180        }
181    }
182
183    /// What arithmetic leaves behind, when what it wrote is one register of a width the
184    /// description names.
185    ///
186    /// The statement is about the register it wrote rather than about the ones it read, which is
187    /// what makes its own definition not something that makes it stale: what it wrote is the value
188    /// the comparison it stands in for is about.
189    fn zeroed(
190        func: &mir::Func,
191        insts: &FlagInsts,
192        name: &str,
193        zeroing: &Zeroing,
194        inst: mir::Inst,
195    ) -> Option<Self> {
196        let written = writes(func, inst);
197        let [(at, def)] = written[..] else { return None };
198        let width = (insts.width)(name, at)?;
199        Some(Self { how: How::Zeroed { width, covers: *zeroing }, about: vec![def] })
200    }
201
202    /// Whether this comparison is one the condition state already answers.
203    fn answers(
204        &self,
205        func: &mir::Func,
206        insts: &FlagInsts,
207        names: &Interner,
208        sequence: &[mir::Inst],
209        at: usize,
210        entry: &Compare,
211    ) -> bool {
212        let inst = sequence[at];
213        let asked: Vec<Place> = reads(func, inst).into_iter().map(|(_, place)| place).collect();
214        let against = func[inst].imm.map(|at| func[at].0);
215        match &self.how {
216            // The same question about the same values, so every bit of the answer is the bit that
217            // is already there and what reads it is not something anyone has to ask.
218            How::Made { asks, read, imm } => {
219                *asks == entry.asks && *read == asked && *imm == against
220            }
221            How::Zeroed { width, covers } => {
222                if against != Some(0) || self.about != asked {
223                    return false;
224                }
225                let [(index, _)] = reads(func, inst)[..] else { return false };
226                let Some(name) = opcode(func, insts, names, inst) else { return false };
227                if (insts.width)(name, index) != Some(*width) {
228                    return false;
229                }
230                let conditions = conditions(func, insts, names, sequence, at);
231                !conditions.is_empty() && conditions.iter().all(|&reads| covers.covers(reads))
232            }
233        }
234    }
235}
236
237/// The conditions that read what an instruction leaves in the condition state.
238///
239/// Its own first, which is where a comparison that keeps a byte carries the condition it is about,
240/// and then the ones behind it as far as whatever writes the condition state next. Both halves
241/// matter and for one reason: the rewrite leaves the readers where they are and takes the
242/// comparison out from under them, so each of them ends up reading what the instruction further
243/// back left instead.
244fn conditions(
245    func: &mir::Func,
246    insts: &FlagInsts,
247    names: &Interner,
248    sequence: &[mir::Inst],
249    at: usize,
250) -> Vec<Reads> {
251    let mut found = Vec::new();
252    let Some(name) = opcode(func, insts, names, sequence[at]) else { return found };
253    found.extend(insts.reads(name));
254    for &inst in &sequence[at + 1..] {
255        let Some(name) = opcode(func, insts, names, inst) else { break };
256        if (insts.writes)(name) {
257            break;
258        }
259        found.extend(insts.reads(name));
260    }
261    found
262}
263
264/// The same state, unless the instruction wrote a register it was a statement about.
265fn stale(func: &mir::Func, inst: mir::Inst, left: Option<Left>) -> Option<Left> {
266    let left = left?;
267    let touched = writes(func, inst).iter().any(|&(_, place)| left.about.contains(&place));
268    (!touched).then_some(left)
269}
270
271/// The registers an instruction reads, each with the index it reads it at.
272fn reads(func: &mir::Func, inst: mir::Inst) -> Vec<(u8, Place)> {
273    picked(func, inst, Role::Use)
274}
275
276/// The registers an instruction writes, each with the index it writes it at.
277fn writes(func: &mir::Func, inst: mir::Inst) -> Vec<(u8, Place)> {
278    let mut found = picked(func, inst, Role::Def);
279    found.extend(picked(func, inst, Role::EarlyDef));
280    found
281}
282
283/// The operands in that role, each with the index it is at.
284fn picked(func: &mir::Func, inst: mir::Inst, role: Role) -> Vec<(u8, Place)> {
285    func[func[inst].operands]
286        .iter()
287        .enumerate()
288        .filter(|(_, operand)| operand.role == role)
289        .filter_map(|(at, operand)| Some((u8::try_from(at).ok()?, (operand.class, operand.reg))))
290        .collect()
291}
292
293/// Turns a comparison into what is left of it, which is a byte or nothing at all.
294///
295/// The byte keeps the register it was going to and the constant goes, because what the constant
296/// was for was the comparison and the comparison is the part that is not happening. Nothing else
297/// about the instruction moves, which is what keeps this a rewrite of one instruction rather than
298/// a rewrite of the block around it.
299fn take(
300    func: &mut mir::Func,
301    opcodes: &HashMap<&str, mir::Opcode>,
302    inst: mir::Inst,
303    entry: &Compare,
304) {
305    let Some(kept) = entry.kept else {
306        func.remove_inst(inst);
307        return;
308    };
309    let Some(&opcode) = opcodes.get(kept) else { return };
310    let byte: Vec<mir::Operand> = func[func[inst].operands]
311        .iter()
312        .filter(|operand| operand.role != Role::Use)
313        .copied()
314        .collect();
315    let operands = func.push_operands(&byte);
316    func[inst].opcode = opcode;
317    func[inst].operands = operands;
318    func[inst].imm = None;
319}
320
321/// The name this target knows an instruction by, for an instruction that is one of this target's.
322///
323/// The opcode in machine IR carries the target's prefix, because a function in the middle of being
324/// compiled holds instructions of one machine and the prefix is what says which. Anything without
325/// it is not something this description covers, and the rest of the pass treats that as knowing
326/// nothing rather than as knowing it is safe.
327fn opcode<'a>(
328    func: &mir::Func,
329    insts: &FlagInsts,
330    names: &'a Interner,
331    inst: mir::Inst,
332) -> Option<&'a str> {
333    names.resolve(func[inst].opcode.name()).strip_prefix(insts.prefix)
334}
335
336#[cfg(test)]
337mod tests {
338    use rucc_target::x86_64::{FLAGS, GPR};
339
340    use super::*;
341
342    /// A function with one block, and the names it was built with.
343    fn empty() -> (Interner, mir::Func, mir::Block) {
344        let mut names = Interner::new();
345        let mut func = mir::Func::new(names.intern("f"));
346        let block = func.create_block();
347        (names, func, block)
348    }
349
350    /// The opcode of that name on this target.
351    fn op(names: &mut Interner, name: &str) -> mir::Opcode {
352        mir::Opcode::new(names.intern(&format!("{}{name}", FLAGS.prefix)))
353    }
354
355    /// The pass, over the machine this crate has a backend for.
356    fn takes(func: &mut mir::Func, names: &mut Interner) -> usize {
357        redundant(func, &FLAGS, names)
358    }
359
360    /// What every instruction in a block came to, as opcodes with the target's prefix taken off.
361    fn shape(func: &mir::Func, names: &Interner, block: mir::Block) -> Vec<String> {
362        func.insts(block)
363            .map(|inst| {
364                names
365                    .resolve(func[inst].opcode.name())
366                    .strip_prefix(FLAGS.prefix)
367                    .unwrap_or("")
368                    .to_owned()
369            })
370            .collect()
371    }
372
373    /// The shape the issue is named after: the same comparison made twice with nothing between the
374    /// two but the byte the first one kept. The second asks what the first asked, so what is left
375    /// of it is the byte alone.
376    #[test]
377    fn the_same_comparison_twice_leaves_one_comparison_and_two_bytes() {
378        let (mut names, mut func, block) = empty();
379        let value = func.new_vreg(GPR);
380        let first = func.new_vreg(GPR);
381        let second = func.new_vreg(GPR);
382        let ne = op(&mut names, "cmp_set_ne_ri_32");
383        let e = op(&mut names, "cmp_set_e_ri_32");
384        func.build(block, ne).def(first, GPR).uses(value, GPR).imm(0).finish();
385        func.build(block, e).def(second, GPR).uses(value, GPR).imm(0).finish();
386
387        assert_eq!(takes(&mut func, &mut names), 1);
388        assert_eq!(shape(&func, &names, block), ["cmp_set_ne_ri_32", "set_e"]);
389    }
390
391    /// The same two comparisons with something writing the compared register in between. The bits
392    /// are the bits the first one left and they are about a value that has moved on.
393    #[test]
394    fn a_comparison_of_a_register_something_wrote_in_between_stays() {
395        let (mut names, mut func, block) = empty();
396        let value = func.new_vreg(GPR);
397        let other = func.new_vreg(GPR);
398        let first = func.new_vreg(GPR);
399        let second = func.new_vreg(GPR);
400        let ne = op(&mut names, "cmp_set_ne_ri_32");
401        let e = op(&mut names, "cmp_set_e_ri_32");
402        let copy = op(&mut names, "mov_rr_64");
403        func.build(block, ne).def(first, GPR).uses(value, GPR).imm(0).finish();
404        func.build(block, copy).def(value, GPR).uses(other, GPR).finish();
405        func.build(block, e).def(second, GPR).uses(value, GPR).imm(0).finish();
406
407        assert_eq!(takes(&mut func, &mut names), 0);
408        assert_eq!(shape(&func, &names, block).len(), 3);
409    }
410
411    /// The common shape, which is `if (a & MASK)`. The `and` clears the carry and the overflow and
412    /// sets the zero and the sign from what it wrote, which is every bit the comparison would have
413    /// set and the same values, so every condition may read it.
414    #[test]
415    fn a_comparison_against_zero_after_a_bitwise_operation_goes() {
416        for condition in ["e", "l", "b"] {
417            let (mut names, mut func, block) = empty();
418            let value = func.new_vreg(GPR);
419            let byte = func.new_vreg(GPR);
420            let and = op(&mut names, "and_ri_32");
421            let cmp = op(&mut names, &format!("cmp_set_{condition}_ri_32"));
422            func.build(block, and).def(value, GPR).uses(value, GPR).imm(255).finish();
423            func.build(block, cmp).def(byte, GPR).uses(value, GPR).imm(0).finish();
424
425            assert_eq!(takes(&mut func, &mut names), 1, "set{condition}");
426            assert_eq!(
427                shape(&func, &names, block),
428                ["and_ri_32".to_owned(), format!("set_{condition}")]
429            );
430        }
431    }
432
433    /// The same after a subtraction, which is the one that is only half true. The zero bit is what
434    /// a comparison of the answer against zero would have set it to, and the overflow is not, so
435    /// the conditions built out of the sign and the overflow together have to stay.
436    #[test]
437    fn a_comparison_against_zero_after_a_subtraction_goes_only_for_the_zero_conditions() {
438        for (condition, left) in [("e", 1), ("ne", 1), ("l", 0), ("ge", 0), ("a", 0)] {
439            let (mut names, mut func, block) = empty();
440            let value = func.new_vreg(GPR);
441            let other = func.new_vreg(GPR);
442            let byte = func.new_vreg(GPR);
443            let sub = op(&mut names, "sub_rr_32");
444            let cmp = op(&mut names, &format!("cmp_set_{condition}_ri_32"));
445            func.build(block, sub).def(value, GPR).uses(value, GPR).uses(other, GPR).finish();
446            func.build(block, cmp).def(byte, GPR).uses(value, GPR).imm(0).finish();
447
448            assert_eq!(takes(&mut func, &mut names), left, "set{condition}");
449        }
450    }
451
452    /// A comparison the layout already folded a branch into, which keeps no byte at all. There is
453    /// nothing left of one of those, and the jump behind it reads what the `and` left.
454    #[test]
455    fn a_comparison_that_keeps_nothing_is_taken_out_and_the_jump_reads_what_is_there() {
456        let (mut names, mut func, block) = empty();
457        let value = func.new_vreg(GPR);
458        let and = op(&mut names, "and_ri_32");
459        let cmp = op(&mut names, "cmp_ri_32");
460        let jump = op(&mut names, "jcc_l");
461        func.build(block, and).def(value, GPR).uses(value, GPR).imm(255).finish();
462        func.build(block, cmp).uses(value, GPR).imm(0).finish();
463        func.build(block, jump).finish();
464
465        assert_eq!(takes(&mut func, &mut names), 1);
466        assert_eq!(shape(&func, &names, block), ["and_ri_32", "jcc_l"]);
467    }
468
469    /// The same three instructions with a subtraction in front. The condition is behind the
470    /// comparison rather than on it, so finding it means looking at what reads what the comparison
471    /// would have left, and a signed `<` is not something a subtraction answers.
472    #[test]
473    fn a_comparison_that_keeps_nothing_is_refused_on_the_condition_behind_it() {
474        let (mut names, mut func, block) = empty();
475        let value = func.new_vreg(GPR);
476        let other = func.new_vreg(GPR);
477        let sub = op(&mut names, "sub_rr_32");
478        let cmp = op(&mut names, "cmp_ri_32");
479        let jump = op(&mut names, "jcc_l");
480        func.build(block, sub).def(value, GPR).uses(value, GPR).uses(other, GPR).finish();
481        func.build(block, cmp).uses(value, GPR).imm(0).finish();
482        func.build(block, jump).finish();
483
484        assert_eq!(takes(&mut func, &mut names), 0);
485        assert_eq!(shape(&func, &names, block).len(), 3);
486    }
487
488    /// Arithmetic that wrote half of what the comparison is asking about. The upper half is zero
489    /// because this machine writes it that way, so the two agree about whether the value is zero
490    /// and disagree about its sign, and the description has no way to say half of one condition.
491    #[test]
492    fn a_comparison_wider_than_the_arithmetic_in_front_of_it_stays() {
493        let (mut names, mut func, block) = empty();
494        let value = func.new_vreg(GPR);
495        let byte = func.new_vreg(GPR);
496        let and = op(&mut names, "and_ri_32");
497        let cmp = op(&mut names, "cmp_set_e_ri_64");
498        func.build(block, and).def(value, GPR).uses(value, GPR).imm(255).finish();
499        func.build(block, cmp).def(byte, GPR).uses(value, GPR).imm(0).finish();
500
501        assert_eq!(takes(&mut func, &mut names), 0);
502        assert_eq!(shape(&func, &names, block).len(), 2);
503    }
504
505    /// Something between the two that writes the condition state. A multiply is not in the
506    /// description's list because this machine leaves the zero bit undefined after one, so what it
507    /// left is not something to read and not something to reason from either.
508    #[test]
509    fn anything_that_writes_the_condition_state_in_between_makes_the_comparison_stay() {
510        let (mut names, mut func, block) = empty();
511        let value = func.new_vreg(GPR);
512        let other = func.new_vreg(GPR);
513        let byte = func.new_vreg(GPR);
514        let and = op(&mut names, "and_ri_32");
515        let mul = op(&mut names, "imul_rr_32");
516        let cmp = op(&mut names, "cmp_set_e_ri_32");
517        func.build(block, and).def(value, GPR).uses(value, GPR).imm(255).finish();
518        func.build(block, mul).def(other, GPR).uses(other, GPR).uses(other, GPR).finish();
519        func.build(block, cmp).def(byte, GPR).uses(value, GPR).imm(0).finish();
520
521        assert_eq!(takes(&mut func, &mut names), 0);
522        assert_eq!(shape(&func, &names, block).len(), 3);
523    }
524
525    /// A comparison that keeps nothing and whose condition state nothing is found to read. It is
526    /// dead rather than redundant, and this pass is not the one that answers that.
527    #[test]
528    fn a_comparison_nothing_is_found_to_read_stays() {
529        let (mut names, mut func, block) = empty();
530        let value = func.new_vreg(GPR);
531        let and = op(&mut names, "and_ri_32");
532        let cmp = op(&mut names, "cmp_ri_32");
533        func.build(block, and).def(value, GPR).uses(value, GPR).imm(255).finish();
534        func.build(block, cmp).uses(value, GPR).imm(0).finish();
535
536        assert_eq!(takes(&mut func, &mut names), 0);
537        assert_eq!(shape(&func, &names, block).len(), 2);
538    }
539
540    /// The condition state does not cross a block boundary, and neither does this.
541    #[test]
542    fn a_comparison_in_another_block_is_not_one_the_arithmetic_answers() {
543        let (mut names, mut func, block) = empty();
544        let next = func.create_block();
545        let value = func.new_vreg(GPR);
546        let byte = func.new_vreg(GPR);
547        let and = op(&mut names, "and_ri_32");
548        let cmp = op(&mut names, "cmp_set_e_ri_32");
549        func.build(block, and).def(value, GPR).uses(value, GPR).imm(255).finish();
550        *func.succs_mut(block) = vec![mir::BlockCall::to(next)];
551        func.build(next, cmp).def(byte, GPR).uses(value, GPR).imm(0).finish();
552
553        assert_eq!(takes(&mut func, &mut names), 0);
554        assert_eq!(shape(&func, &names, next).len(), 1);
555    }
556}