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