Skip to main content

rucc_codegen/
split.rs

1//! Splitting critical edges, so that every edge that carries values has somewhere to put them.
2//!
3//! Design: `spec/10-backend.md` section 10.4.
4//!
5//! An edge carries values when the block it goes to takes parameters, and giving a parameter its
6//! value is a move. The move has to happen on the edge and not before it or after it, because
7//! before it is a block that goes somewhere else too and after it is a block that is arrived at
8//! from somewhere else too, and in either case the move would run on a path it was not written
9//! for. An edge out of a block with one successor can put its moves at the end of that block,
10//! since every path through it takes the edge. An edge into a block with one predecessor can put
11//! them at the start of that block, for the same reason the other way round. An edge that is
12//! neither, which is what a critical edge is, has neither place, and the allocator says so:
13//! `rucc_regalloc` asserts that it never sees one.
14//!
15//! So one is turned into two. A block with nothing in it goes on the edge, the arguments move on
16//! to the second half, and both halves are now uncritical: the first goes to a block with one
17//! predecessor and the second leaves a block with one successor. Which of the two the moves end
18//! up in is the allocator's answer and not this one's, and either is correct.
19//!
20//! # What it leaves behind
21//!
22//! An empty block, which is a jump to the next thing unless the layout puts it where it falls
23//! through. That is a cost, and it is why an edge with nothing to carry is left alone: there are
24//! no moves to find a place for, so splitting it would buy a jump and nothing else.
25
26use rucc_mir as mir;
27
28/// Splits every critical edge that carries values, and gives back how many it split.
29///
30/// Run after lowering and before allocation. Running it twice is running it once, because the
31/// blocks it adds have one successor each and are never the source of a critical edge.
32pub fn critical(func: &mut mir::Func) -> usize {
33    let preds = preds(func);
34    let blocks: Vec<mir::Block> = func.blocks().collect();
35    let mut split = 0;
36    for block in blocks {
37        if func[block].succs.len() < 2 {
38            continue;
39        }
40        for index in 0..func[block].succs.len() {
41            let call = func[block].succs[index].clone();
42            if call.args.is_empty() || preds[call.block.index()] < 2 {
43                continue;
44            }
45            // The new block is at the end of the layout, which is where a block that is a jump
46            // and nothing else does the least harm before the layout pass has an opinion.
47            //
48            // It runs exactly as often as the edge it sits on is taken, and both halves of that
49            // edge are now that edge, which is why the weight is copied onto all three rather
50            // than left at what a block nobody told anything runs. A block on a cold edge that
51            // claimed to run once per call would be one the layout put in the middle of the hot
52            // path.
53            let weight = call.weight;
54            let half = func.create_block();
55            func.set_weight(half, weight);
56            *func.succs_mut(half) = vec![call];
57            func.succs_mut(block)[index] = mir::BlockCall::to(half).taken(weight);
58            split += 1;
59        }
60    }
61    split
62}
63
64/// How many edges arrive at each block, counted by index rather than in layout order so that a
65/// block added while splitting can be looked up in the same table.
66fn preds(func: &mir::Func) -> Vec<usize> {
67    let mut counts = vec![0; func.block_count()];
68    for block in func.blocks() {
69        for call in &func[block].succs {
70            counts[call.block.index()] += 1;
71        }
72    }
73    counts
74}
75
76#[cfg(test)]
77mod tests {
78    use rucc_base::Interner;
79    use rucc_target::x86_64::{GPR, REGS};
80
81    use super::*;
82
83    /// A diamond: one block that goes two ways and one block both ways arrive at, with as many
84    /// parameters on the block they arrive at as the test asks for.
85    fn diamond(params: usize) -> (Interner, mir::Func, [mir::Block; 4]) {
86        let mut names = Interner::new();
87        let mut func = mir::Func::new(names.intern("f"));
88        let head = func.create_block();
89        let left = func.create_block();
90        let right = func.create_block();
91        let join = func.create_block();
92        // The values arrive in the head, so that they have somewhere to be defined and the
93        // printer has a name for them. Nothing here runs an allocator, which is the one thing
94        // that would object to a first block with parameters.
95        let args: Vec<mir::Reg> = (0..params).map(|_| func.append_param(head, GPR)).collect();
96        for _ in 0..params {
97            func.append_param(join, GPR);
98        }
99        *func.succs_mut(head) = vec![mir::BlockCall::to(left), mir::BlockCall::to(right)];
100        *func.succs_mut(left) = vec![mir::BlockCall::with(join, args.clone())];
101        *func.succs_mut(right) = vec![mir::BlockCall::with(join, args)];
102        (names, func, [head, left, right, join])
103    }
104
105    /// Where each block goes, which is the whole of what this changes.
106    fn edges(func: &mir::Func) -> Vec<Vec<usize>> {
107        func.blocks()
108            .map(|block| func[block].succs.iter().map(|call| call.block.index()).collect())
109            .collect()
110    }
111
112    #[test]
113    fn an_edge_that_is_the_only_way_out_is_left_alone() {
114        let (_, mut func, _) = diamond(1);
115        // The two edges into the join carry a value each and neither is critical, because the
116        // block each leaves goes nowhere else.
117        assert_eq!(critical(&mut func), 0);
118        assert_eq!(edges(&func), vec![vec![1, 2], vec![3], vec![3], vec![]]);
119    }
120
121    #[test]
122    fn a_critical_edge_carrying_a_value_is_split_in_two() {
123        let (_, mut func, [head, _, _, join]) = diamond(1);
124        // Now the head goes straight to the join as well, so both of its arms are critical: it
125        // has two ways out and the join has three ways in.
126        let arg = func.append_param(head, GPR);
127        func.succs_mut(head).push(mir::BlockCall::with(join, vec![arg]));
128        func.succs_mut(head).swap(1, 2);
129
130        assert_eq!(critical(&mut func), 1);
131        assert_eq!(
132            edges(&func),
133            // The head's second arm is the new block and the new block goes to the join. The
134            // other two arms are untouched, because each goes to a block with one way in.
135            vec![vec![1, 4, 2], vec![3], vec![3], vec![], vec![3]]
136        );
137    }
138
139    #[test]
140    fn a_critical_edge_carrying_nothing_is_left_alone() {
141        let (_, mut func, [head, _, _, join]) = diamond(0);
142        func.succs_mut(head).push(mir::BlockCall::to(join));
143
144        // Critical and not split, because there is no move to find a place for and a block that
145        // is a jump and nothing else is worth more than nothing.
146        assert_eq!(critical(&mut func), 0);
147    }
148
149    #[test]
150    fn the_arguments_move_on_to_the_half_that_arrives() {
151        let (names, mut func, [head, _, _, join]) = diamond(1);
152        let arg = func.append_param(head, GPR);
153        func.succs_mut(head).push(mir::BlockCall::with(join, vec![arg]));
154
155        assert_eq!(critical(&mut func), 1);
156        // What the first half carries is nothing, since the block it goes to asks for nothing,
157        // and what the second half carries is what the whole edge used to.
158        let half = func.blocks().last().expect("the block the split added");
159        assert_eq!(func[head].succs[2].args, Vec::new());
160        assert_eq!(func[half].succs[0].args, vec![arg]);
161        assert_eq!(
162            mir::print_func(&func, &names, &REGS),
163            "mfunc @f {\nblock0(%0:gpr, %1:gpr):\n    block1, block2, block4\n\n\
164             block1:\n    block3(%0)\n\nblock2:\n    block3(%0)\n\n\
165             block3(%2:gpr):\n\nblock4:\n    block3(%1)\n}\n"
166        );
167    }
168
169    #[test]
170    fn splitting_twice_is_splitting_once() {
171        let (_, mut func, [head, _, _, join]) = diamond(1);
172        let arg = func.append_param(head, GPR);
173        func.succs_mut(head).push(mir::BlockCall::with(join, vec![arg]));
174
175        assert_eq!(critical(&mut func), 1);
176        assert_eq!(critical(&mut func), 0);
177    }
178}