Skip to main content

rucc_codegen/
choice.rs

1//! Letting a select on a comparison read what the comparison left in the condition state.
2//!
3//! Design: `spec/10-backend.md` section 10.6, and `spec/optimizer/37-machine-level-optimization.md`
4//! section 37.4.
5//!
6//! A rule selects `select c, t, f` as a test of the byte `c` and a conditional move on the answer,
7//! because the byte is the only thing a rule can name. When the byte came from a comparison that is
8//! three instructions where the machine wanted one. The comparison sets the condition state, the
9//! byte is written from it, the test of the byte sets the condition state again, and only then
10//! does the move read it:
11//!
12//! ```text
13//!   cmpl %esi, %edi
14//!   setg %al                  cmpl %esi, %edi
15//!   testb %al, %al      ->    cmovgl %edx, %ecx
16//!   cmovnel %edx, %ecx
17//! ```
18//!
19//! This is the branch [`crate::layout`] folds, written as a select, and it is done the same way for
20//! the same reasons. [`fusable`] asks before allocation which comparisons have a byte that selects
21//! in the same block are the whole of what reads, because that is a question about a register that
22//! is written once. [`moves`] runs after the layout, where nothing is left that could put an
23//! instruction between the comparison and a move reading what it left, and rewrites the comparison
24//! and all of its selects together or none of them.
25//!
26//! What it is worth is what phiopt makes. A loop keeping the largest of each of eight slots, which
27//! is `if (v > best[k]) best[k] = v` and becomes a select once the store is made on both paths,
28//! runs two instructions fewer on every element, and those are two of the four on the path from
29//! the load to the store.
30//!
31//! # What stops it
32//!
33//! Anything between the comparison and a select that writes the condition state, which the target
34//! says of every name it does not know. And anything that writes the register the byte was given,
35//! since a select reading that register afterwards is reading something else. Either one ends the
36//! walk, and a select the walk did not reach keeps the byte, so the comparison keeps it too and
37//! every select behind it stays as it was.
38//!
39//! A comparison of floats is not in the table and is left alone. What it leaves in the condition
40//! state is two answers, one for whether the operands were ordered at all, and a move can read only
41//! one of them.
42
43use rucc_base::Interner;
44use rucc_base::hash::Map;
45use rucc_mir::{self as mir, Role};
46use rucc_target::{BranchInsts, FlagInsts, Fusion, MachineInsts};
47
48use crate::changes::{self, Changes, Plan};
49
50/// The comparisons whose byte only selects in the same block read, each with how many do.
51///
52/// Run before allocation, on the same function [`moves`] is later given, for the reason
53/// [`crate::layout::fusable`] is: whether anything else reads a register is a question about a
54/// virtual one, and after allocation a register is written many times.
55///
56/// The byte has to be what a select reads as its condition and nothing else. A select that also
57/// chose the byte as one of its values would want the byte kept, and so the count of reads has to
58/// come out the same as the count of selects that read it as a condition.
59#[must_use]
60pub fn fusable(
61    func: &mir::Func,
62    insts: &BranchInsts,
63    names: &mut Interner,
64) -> Map<mir::Inst, usize> {
65    let compares = compares(insts, names);
66    let selects = selects(insts, names);
67    let reads = changes::Reads::of(func);
68    let mut found = Map::default();
69    for block in func.blocks() {
70        let mut waiting: Map<mir::Reg, (mir::Inst, usize)> = Map::default();
71        for inst in func.insts(block) {
72            let data = &func[inst];
73            let operands = &func[data.operands];
74            if selects.contains(&data.opcode) {
75                let condition = operands.get(3).map(|operand| operand.reg);
76                if let Some(entry) = condition.and_then(|reg| waiting.get_mut(&reg)) {
77                    entry.1 += 1;
78                }
79            }
80            if compares.contains_key(&data.opcode) {
81                let byte = operands.first().filter(|operand| operand.role != Role::Use);
82                if let Some(byte) = byte.filter(|byte| byte.reg.is_virtual()) {
83                    waiting.insert(byte.reg, (inst, 0));
84                }
85            }
86        }
87        for (reg, (compare, count)) in waiting {
88            if count > 0 && reads.count(reg) == count {
89                found.insert(compare, count);
90            }
91        }
92    }
93    found
94}
95
96/// Turns every comparison [`fusable`] found, and the selects reading its byte, into the
97/// comparison keeping nothing and the moves on its condition.
98///
99/// Gives back how many comparisons it did that for, which the tests read and nothing else does.
100pub fn moves(
101    func: &mut mir::Func,
102    insts: &BranchInsts,
103    flags: &FlagInsts,
104    machine: &MachineInsts,
105    names: &mut Interner,
106    fusable: &Map<mir::Inst, usize>,
107) -> usize {
108    if fusable.is_empty() {
109        return 0;
110    }
111    // Every name the rewrite could want, before the walk rather than inside it, for the reason the
112    // compare pass gives: the walk reads names out of the interner while it edits the function.
113    let compares = compares(insts, names);
114    let kept: Map<&str, mir::Opcode> =
115        insts.fused.iter().map(|fusion| (fusion.cmp, opcode(insts, names, fusion.cmp))).collect();
116    let chosen: Map<(mir::Opcode, &str), mir::Opcode> = insts
117        .moves
118        .iter()
119        .map(|entry| {
120            let select = opcode(insts, names, entry.select);
121            ((select, entry.when), opcode(insts, names, entry.cmov))
122        })
123        .collect();
124    let names = &*names;
125    let mut counts = changes::Reads::of(func);
126    let mut made = 0;
127    for block in func.blocks().collect::<Vec<_>>() {
128        let sequence: Vec<mir::Inst> = func.insts(block).collect();
129        for (at, &compare) in sequence.iter().enumerate() {
130            let Some(&wanted) = fusable.get(&compare) else { continue };
131            let Some(&fusion) = compares.get(&func[compare].opcode) else { continue };
132            let Some(&byte) = func[func[compare].operands].first() else { continue };
133            let found = reached(func, flags, names, &chosen, fusion, byte, &sequence[at + 1..]);
134            if found.len() != wanted {
135                continue;
136            }
137            let Some(&cmp) = kept.get(fusion.cmp) else { continue };
138            let mut set = Changes::new();
139            set.rewrite(compare, flags_only(func, compare, cmp));
140            for (select, cmov) in found {
141                let mut plan = Plan::of(func, select);
142                plan.operands.truncate(3);
143                set.rewrite(select, Plan { opcode: cmov, ..plan });
144            }
145            if set.commit(func, &mut counts, names, machine).is_ok() {
146                made += 1;
147            }
148        }
149    }
150    made
151}
152
153/// The selects on the byte a comparison wrote that the condition state it left still reaches, in
154/// order, each with the move it becomes.
155///
156/// The walk ends at the first instruction that writes the condition state or the byte's register,
157/// and a select on the byte is not the first of those even though its test is, because the test is
158/// the half that goes.
159fn reached(
160    func: &mir::Func,
161    flags: &FlagInsts,
162    names: &Interner,
163    chosen: &Map<(mir::Opcode, &str), mir::Opcode>,
164    fusion: &Fusion,
165    byte: mir::Operand,
166    after: &[mir::Inst],
167) -> Vec<(mir::Inst, mir::Opcode)> {
168    let place = (byte.class, byte.reg);
169    let mut found = Vec::new();
170    for &inst in after {
171        let data = &func[inst];
172        let operands = &func[data.operands];
173        let writes = operands
174            .iter()
175            .any(|operand| operand.role != Role::Use && (operand.class, operand.reg) == place);
176        if let Some(&cmov) = chosen.get(&(data.opcode, fusion.if_true)) {
177            let [_, false_arm, true_arm, condition] = operands else { break };
178            let arms = [false_arm, true_arm];
179            if (condition.class, condition.reg) == place
180                && arms.iter().all(|arm| (arm.class, arm.reg) != place)
181            {
182                found.push((inst, cmov));
183                if writes {
184                    break;
185                }
186                continue;
187            }
188        }
189        let Some(name) = names.resolve(data.opcode.name()).strip_prefix(flags.prefix) else {
190            break;
191        };
192        if (flags.writes)(name) || writes {
193            break;
194        }
195    }
196    found
197}
198
199/// The comparison with the byte at the front taken off, which is the one that keeps nothing.
200///
201/// An addressing mode names its base and its index by where they are among the operands, and every
202/// operand comes down one place, so the two positions come down with them.
203fn flags_only(func: &mir::Func, compare: mir::Inst, cmp: mir::Opcode) -> Plan {
204    let mut plan = Plan::of(func, compare);
205    plan.operands.remove(0);
206    plan.amode = plan.amode.map(|mut amode| {
207        amode.base = amode.base.map(|position| position - 1);
208        amode.index = amode.index.map(|position| position - 1);
209        amode
210    });
211    Plan { opcode: cmp, ..plan }
212}
213
214/// The comparisons that keep a byte, by opcode, each with its entry in the branch table.
215fn compares(insts: &BranchInsts, names: &mut Interner) -> Map<mir::Opcode, &'static Fusion> {
216    insts.fused.iter().map(|fusion| (opcode(insts, names, fusion.set), fusion)).collect()
217}
218
219/// The selects that test a byte, by opcode.
220fn selects(insts: &BranchInsts, names: &mut Interner) -> Vec<mir::Opcode> {
221    let mut found: Vec<mir::Opcode> =
222        insts.moves.iter().map(|entry| opcode(insts, names, entry.select)).collect();
223    found.dedup();
224    found
225}
226
227/// The opcode of that name on this target.
228fn opcode(insts: &BranchInsts, names: &mut Interner, name: &str) -> mir::Opcode {
229    mir::Opcode::new(names.intern(&format!("{}{name}", insts.prefix)))
230}
231
232#[cfg(test)]
233mod tests {
234    use rucc_target::x86_64::{BRANCH, FLAGS, GPR, MACHINE};
235
236    use super::*;
237
238    /// A function with one block, and the names it was built with.
239    fn empty() -> (Interner, mir::Func, mir::Block) {
240        let mut names = Interner::new();
241        let mut func = mir::Func::new(names.intern("f"));
242        let block = func.create_block();
243        (names, func, block)
244    }
245
246    /// The opcode of that name on this target.
247    fn op(names: &mut Interner, name: &str) -> mir::Opcode {
248        opcode(&BRANCH, names, name)
249    }
250
251    /// Both halves of the pass, the way the pipeline runs them, with nothing in between.
252    fn fuse(func: &mut mir::Func, names: &mut Interner) -> usize {
253        let found = fusable(func, &BRANCH, names);
254        moves(func, &BRANCH, &FLAGS, &MACHINE, names, &found)
255    }
256
257    /// What every instruction in a block came to, as opcodes with the target's prefix taken off.
258    fn shape(func: &mir::Func, names: &Interner, block: mir::Block) -> Vec<String> {
259        func.insts(block)
260            .map(|inst| {
261                let name = names.resolve(func[inst].opcode.name());
262                name.strip_prefix(BRANCH.prefix).unwrap_or("").to_owned()
263            })
264            .collect()
265    }
266
267    /// What a select writes, which is the register its false arm is in, the way the rule selects
268    /// it and the allocator leaves it.
269    fn tied(f: mir::Reg) -> mir::Operand {
270        mir::Operand::write(f, GPR).with(rucc_mir::Constraint::Reuse(1))
271    }
272
273    /// A comparison of two registers keeping a byte, and a select on the byte.
274    fn compare_and_select(
275        func: &mut mir::Func,
276        names: &mut Interner,
277        block: mir::Block,
278        condition: &str,
279    ) -> [mir::Reg; 2] {
280        let [x, y, byte, t, f] = [(); 5].map(|()| func.new_vreg(GPR));
281        let cmp = op(names, &format!("cmp_set_{condition}_32"));
282        let select = op(names, "test_cmov_ne_32");
283        func.build(block, cmp).def(byte, GPR).uses(x, GPR).uses(y, GPR).finish();
284        func.build(block, select)
285            .operand(tied(f))
286            .uses(f, GPR)
287            .uses(t, GPR)
288            .uses(byte, GPR)
289            .finish();
290        [byte, t]
291    }
292
293    /// The shape this is for. The comparison keeps nothing, the select becomes the move on the
294    /// comparison's own condition, and its operands are the three it had without the byte.
295    #[test]
296    fn a_select_on_a_comparison_becomes_a_move_on_its_condition() {
297        for condition in ["e", "l", "ge", "b", "a"] {
298            let (mut names, mut func, block) = empty();
299            compare_and_select(&mut func, &mut names, block, condition);
300
301            assert_eq!(fuse(&mut func, &mut names), 1, "{condition}");
302            let expected = ["cmp_rr_32".to_owned(), format!("cmov_{condition}_32")];
303            assert_eq!(shape(&func, &names, block), expected);
304            let last = func.insts(block).last().expect("the move");
305            assert_eq!(func[func[last].operands].len(), 3);
306        }
307    }
308
309    /// Two selects on one comparison, which is what `min` and `max` of the same pair come to. Both
310    /// become moves, and the comparison keeps nothing because nothing is left to read the byte.
311    #[test]
312    fn two_selects_on_one_comparison_both_become_moves() {
313        let (mut names, mut func, block) = empty();
314        let [byte, _] = compare_and_select(&mut func, &mut names, block, "g");
315        let [t, f] = [(); 2].map(|()| func.new_vreg(GPR));
316        let select = op(&mut names, "test_cmov_ne_64");
317        func.build(block, select)
318            .operand(tied(f))
319            .uses(f, GPR)
320            .uses(t, GPR)
321            .uses(byte, GPR)
322            .finish();
323
324        assert_eq!(fuse(&mut func, &mut names), 1);
325        assert_eq!(shape(&func, &names, block), ["cmp_rr_32", "cmov_g_32", "cmov_g_64"]);
326    }
327
328    /// A move between the two leaves the condition state alone, which is what the allocator puts
329    /// there when the value the move overwrites is still wanted afterwards.
330    #[test]
331    fn a_copy_between_the_comparison_and_the_select_does_not_stop_it() {
332        let (mut names, mut func, block) = empty();
333        let [x, y, byte, t, f, spare] = [(); 6].map(|()| func.new_vreg(GPR));
334        let cmp = op(&mut names, "cmp_set_l_32");
335        let copy = op(&mut names, "mov_rr_64");
336        let select = op(&mut names, "test_cmov_ne_32");
337        func.build(block, cmp).def(byte, GPR).uses(x, GPR).uses(y, GPR).finish();
338        func.build(block, copy).def(spare, GPR).uses(f, GPR).finish();
339        func.build(block, select)
340            .operand(tied(f))
341            .uses(f, GPR)
342            .uses(t, GPR)
343            .uses(byte, GPR)
344            .finish();
345
346        assert_eq!(fuse(&mut func, &mut names), 1);
347        assert_eq!(shape(&func, &names, block), ["cmp_rr_32", "mov_rr_64", "cmov_l_32"]);
348    }
349
350    /// Arithmetic between the two writes the condition state, so what the select would read is
351    /// what the arithmetic left and the test of the byte has to stay.
352    #[test]
353    fn arithmetic_between_the_comparison_and_the_select_keeps_the_test() {
354        let (mut names, mut func, block) = empty();
355        let [x, y, byte, t, f] = [(); 5].map(|()| func.new_vreg(GPR));
356        let cmp = op(&mut names, "cmp_set_l_32");
357        let add = op(&mut names, "add_rr_32");
358        let select = op(&mut names, "test_cmov_ne_32");
359        func.build(block, cmp).def(byte, GPR).uses(x, GPR).uses(y, GPR).finish();
360        func.build(block, add).def(t, GPR).uses(t, GPR).uses(x, GPR).finish();
361        func.build(block, select)
362            .operand(tied(f))
363            .uses(f, GPR)
364            .uses(t, GPR)
365            .uses(byte, GPR)
366            .finish();
367
368        assert_eq!(fuse(&mut func, &mut names), 0);
369        assert_eq!(shape(&func, &names, block), ["cmp_set_l_32", "add_rr_32", "test_cmov_ne_32"]);
370    }
371
372    /// A byte something other than a select reads, here a store of it, has to be written, so the
373    /// comparison keeps it and the select keeps its test.
374    #[test]
375    fn a_byte_something_else_reads_is_kept_and_so_is_the_test() {
376        let (mut names, mut func, block) = empty();
377        let [byte, _] = compare_and_select(&mut func, &mut names, block, "e");
378        let spare = func.new_vreg(GPR);
379        let copy = op(&mut names, "mov_rr_64");
380        func.build(block, copy).def(spare, GPR).uses(byte, GPR).finish();
381
382        assert_eq!(fuse(&mut func, &mut names), 0);
383        assert_eq!(shape(&func, &names, block), ["cmp_set_e_32", "test_cmov_ne_32", "mov_rr_64"]);
384    }
385
386    /// A select that chooses the byte as one of its values wants the byte itself and not only what
387    /// it said, so nothing changes.
388    #[test]
389    fn a_select_that_chooses_the_byte_itself_keeps_the_test() {
390        let (mut names, mut func, block) = empty();
391        let [x, y, byte, f] = [(); 4].map(|()| func.new_vreg(GPR));
392        let cmp = op(&mut names, "cmp_set_ne_32");
393        let select = op(&mut names, "test_cmov_ne_32");
394        func.build(block, cmp).def(byte, GPR).uses(x, GPR).uses(y, GPR).finish();
395        func.build(block, select)
396            .operand(tied(f))
397            .uses(f, GPR)
398            .uses(byte, GPR)
399            .uses(byte, GPR)
400            .finish();
401
402        assert_eq!(fuse(&mut func, &mut names), 0);
403    }
404
405    /// The byte's register written again between the comparison and the select, which after
406    /// allocation is a reload into the same register. The select reads what the reload wrote,
407    /// which the pass cannot tell is the same answer, so it leaves both alone.
408    #[test]
409    fn a_register_written_again_before_the_select_keeps_the_test() {
410        let (mut names, mut func, block) = empty();
411        let byte = mir::Reg::physical(rucc_target::x86_64::RAX);
412        let [x, y, t, f, other] = [(); 5].map(|()| func.new_vreg(GPR));
413        let cmp = op(&mut names, "cmp_set_l_32");
414        let copy = op(&mut names, "mov_rr_64");
415        let select = op(&mut names, "test_cmov_ne_32");
416        func.build(block, cmp).def(byte, GPR).uses(x, GPR).uses(y, GPR).finish();
417        func.build(block, copy).def(byte, GPR).uses(other, GPR).finish();
418        func.build(block, select)
419            .operand(tied(f))
420            .uses(f, GPR)
421            .uses(t, GPR)
422            .uses(byte, GPR)
423            .finish();
424        let found: Map<_, _> =
425            [(func.insts(block).next().expect("the comparison"), 1)].into_iter().collect();
426
427        assert_eq!(moves(&mut func, &BRANCH, &FLAGS, &MACHINE, &mut names, &found), 0);
428        assert_eq!(shape(&func, &names, block), ["cmp_set_l_32", "mov_rr_64", "test_cmov_ne_32"]);
429    }
430
431    /// A comparison against memory keeps its address once the byte in front of the operands is
432    /// gone, which means the positions the address names come down by one.
433    #[test]
434    fn a_comparison_against_memory_keeps_its_address() {
435        let (mut names, mut func, block) = empty();
436        let [x, base, byte, t, f] = [(); 5].map(|()| func.new_vreg(GPR));
437        let cmp = op(&mut names, "cmp_set_l_rm_32");
438        let select = op(&mut names, "test_cmov_ne_32");
439        let place = mir::Mem { disp: 8, ..mir::Mem::at(mir::Operand::read(base, GPR)) };
440        let compare = func.build(block, cmp).def(byte, GPR).uses(x, GPR).mem(place).finish();
441        func.build(block, select)
442            .operand(tied(f))
443            .uses(f, GPR)
444            .uses(t, GPR)
445            .uses(byte, GPR)
446            .finish();
447
448        assert_eq!(fuse(&mut func, &mut names), 1);
449        assert_eq!(shape(&func, &names, block), ["cmp_rm_32", "cmov_l_32"]);
450        let mode = func[func[compare].mem.expect("the address")];
451        assert_eq!((mode.base, mode.disp), (Some(1), 8));
452    }
453}