rucc_opt/thread.rs
1//! Jump threading, the part of it that does not copy anything.
2//!
3//! Design: `spec/optimizer/23-jump-threading.md`. If, on the path through block A into block B, the
4//! condition B tests is already decided, then A should branch straight to the arm B was going to
5//! take and skip B's test. On real C that removes more branches than anything else in the compiler,
6//! because C is full of conditions that are redundant along some paths and not along others.
7//!
8//! It is also the pass most likely to explode, because the general form works by copying B, and a
9//! copy grows the function, and the growth compounds because each thread makes new paths on which
10//! further threading is possible. Section 23.4 is four separate limits on that growth and section
11//! 23.6 names the subset where there is none: the case where the block being threaded past does not
12//! have to be copied at all, which is pure edge redirection. That subset is what is here.
13//!
14//! # What decides a branch here, and what does not
15//!
16//! Arguments in this IR live on the edge rather than in the block, so a block parameter is a value
17//! that arrives differently depending on which way control came. Bind a block's parameters to what
18//! one edge carries and its terminator may resolve on that edge while resolving on no other, which
19//! is the whole of the path sensitivity this pass has. It covers section 23.3's example directly:
20//!
21//! ```c
22//! if (a) x = 1; else x = 2;
23//! if (x == 1) ...
24//! ```
25//!
26//! Nothing dominating the second test decides it, so the forward threader of section 23.2 cannot
27//! see it and neither can `simplify-cfg`. But `x` arrives at the second test as a block parameter,
28//! it is 1 along one edge and 2 along the other, and both edges resolve. Both are threaded, nothing
29//! is left reaching the block, and the second branch goes.
30//!
31//! What is not here is section 23.3's backward search with the path-sensitive range solver. This
32//! asks about one edge and not about a path of them, so a condition decided two blocks back and not
33//! one is a condition this does not see. The range machinery for that exists in [`crate::range`] and
34//! the search is the larger half of the document.
35//!
36//! # Why no block has to be copied
37//!
38//! Section 23.1 quotes GCC's six step surgery, whose first step is a copy of B. The copy exists so
39//! that B's side effects still happen on the threaded path and so that the values B defines are
40//! available to the arm the thread lands on. Where neither is needed, neither is the copy, and this
41//! pass threads exactly the edges where neither is needed:
42//!
43//! - Every instruction in B other than its terminator has no effects, so a path that skips them
44//! skips nothing that had to happen. That is the same predicate [`crate::dce`] deletes an
45//! instruction under, which is the point: an instruction it would delete outright is one a path
46//! can walk past.
47//! - Nothing outside B reads a value B defines. Those are the values the copy would have existed to
48//! compute, and both the arm's arguments and the blocks further down are asking for them.
49//!
50//! The second condition has to be about the whole function and not just about the arm. An argument
51//! is how a value crosses into a block that B does not dominate, but a block B does dominate reads
52//! what B defined with no argument at all, because dominance is the only permission a use needs.
53//! Threading an edge past B takes that dominance away, and the read is then of a value that was
54//! never computed on the path taken. Checking only the arm's arguments misses exactly that, which is
55//! what `a_value_the_block_defines_and_something_below_it_reads_needs_the_copy` is about.
56//!
57//! B's parameters are covered by the same rule, since a parameter is a value B defines. Along the
58//! edge being redirected they are known, so B's own reads of them are substituted rather than
59//! refused, but a read from below is a read of a value that is about to stop existing. And a value
60//! the arm carries that is defined outside B dominates the block it is being carried out of, so it
61//! dominates the predecessor as well: it is on every path to B, the predecessor has an edge to B, so
62//! it is on every path to the predecessor. That is section 23.1's "the values must still dominate",
63//! and it is the same argument `spec/optimizer/21-cfg-simplification.md` section 21.4 needs for
64//! forwarder removal.
65//!
66//! # The loop rules, which are refusals and not scores
67//!
68//! Section 23.5. Threading a path into a loop somewhere other than its header makes an irreducible
69//! loop, and document 06.4 established that rucc does not split nodes and gives up on irreducible
70//! regions instead. So the rule here is stronger than GCC's, where it is one input to a cost model:
71//! a thread that would do it is refused, at every level. A predecessor that is a latch is refused
72//! too, because moving a latch's edge is how the single latch property document 07.3 wants stops
73//! being true. And a block already in an irreducible region is left alone entirely, since the loop
74//! forest has given up on it and the two checks above would be reading an answer nobody stands
75//! behind.
76//!
77//! Because nothing is copied, no new cycle can appear. The new edge from A goes where the edge out
78//! of B went, so a path along it is a path that was already there with B taken out of the middle.
79//! Loops can therefore only be destroyed, and the loop forest is rebuilt after each thread anyway,
80//! which is what keeps the next decision honest.
81//!
82//! # Which level this runs at
83//!
84//! Every level that optimizes, including `-Os` and `-Oz`. Section 23.6 restricts threading at those
85//! two to the case where the block is empty, on the ground that it is the only part that is free,
86//! and this pass is that part generalized: a block whose instructions all have no effects and whose
87//! outgoing arguments do not come from it costs the same as an empty one, which is nothing.
88//!
89//! Once, and not to a fixed point. Threading enables threading, and section 23.7 says the answer to
90//! that is a fixed number of instances rather than a loop, because threading is the pass where
91//! adversarial input is easiest to construct. Section 23.5 asks for two instances at `-O2`, an early
92//! one and a late one after the loop pipeline and SCCP. There is one here, in the early position.
93//! The late one wants the passes that are not written yet.
94//!
95//! # What it counts
96//!
97//! Every refusal is recorded, and they are the measurement section 23.8 asks this document for.
98//! Three of them count edges that decide a branch this pass cannot thread without the copy, split by
99//! which part of the copy is in the way: something in the block that has to happen, a value the arm
100//! carries that the block worked out, and a value the block defines that a block below it reads.
101//! Together they are the size of the prize for building section 23.1's surgery, and separately they
102//! say what the surgery has to do first. The fourth counts edges refused on loop structure, which is
103//! the price of document 06.4's position on irreducible regions stated as a number rather than as an
104//! argument.
105//!
106//! On the 1461 programs of the corpus at `-O2`, 623 edges decide the branch they arrive at and one
107//! of them is threadable without a copy. So the subset that is free is close to worthless on real C,
108//! and this pass earns its place by measuring that rather than by what it removes. The 623 is the
109//! number that justifies the rest of document 23.
110//!
111//! The split says where the rest of the work is. 619 of the 623 are blocked on a value the block
112//! defines being read below it, 4 on the arm carrying one, and none at all on the block doing
113//! something that has to happen. That is one conclusion rather than three: the block being threaded
114//! past is almost never doing work that matters, it is holding a value that matters, so section
115//! 23.1's copy is there to reconstruct values and not to repeat effects. A cheaper thing than a full
116//! block copy might do it, and that is worth knowing before the surgery is written rather than
117//! after.
118
119use std::collections::HashSet;
120
121use rucc_base::Idx;
122use rucc_ir::{Block, BlockCall, Def, Func, Inst, Opcode, Value};
123
124use crate::simplify_cfg::{Bindings, Edges, incoming, sweep, taken};
125use crate::{Analyses, Fuel, Loops, Pass, Preserved, Stats, uses};
126
127/// Recorded once for each edge that was pointed past a branch it decides.
128const THREADED: &str =
129 "edge pointed straight at the arm of the branch it arrives at that it decides";
130
131/// Recorded for an edge that would have been threaded if there had been fuel for it.
132const NO_FUEL: &str = "edge left on a branch it decides, the pass ran out of fuel";
133
134/// Recorded for an edge whose block does something a path through it cannot skip.
135const WOULD_COPY_EFFECT: &str =
136 "edge decides the branch it arrives at, but something in the block has to happen on the way";
137
138/// Recorded for an edge whose block defines a value read below it.
139const WOULD_COPY_READ_BELOW: &str =
140 "edge decides the branch it arrives at, but a block below reads a value this one defines";
141
142/// Recorded for an edge whose arm carries a value the block itself computed.
143const WOULD_COPY_CARRIED: &str =
144 "edge decides the branch it arrives at, but the arm carries a value the block works out";
145
146/// Recorded for an edge that decides a branch but whose thread would spoil the loop forest.
147const WOULD_BREAK_A_LOOP: &str =
148 "edge decides the branch it arrives at, but threading it would give a loop a second way in";
149
150/// The pass.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub struct Thread;
153
154impl Pass for Thread {
155 fn name(&self) -> &'static str {
156 "thread"
157 }
158
159 fn describe(&self) -> &'static str {
160 "an edge that already decides the branch it arrives at is pointed at the arm that branch \
161 would have taken"
162 }
163
164 fn preserves(&self) -> Preserved {
165 // Nothing. An edge moves, so every analysis built on the graph was built on a different
166 // graph, which is the same answer `simplify-cfg` gives for the same reason.
167 Preserved::NONE
168 }
169
170 fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
171 let mut stats = Stats::new();
172 let Some(entry) = func.entry() else { return stats };
173 // The edges are kept here rather than asked for as a graph, because this pass moves them
174 // as it goes and a cached graph would be about the shape the function had one thread ago.
175 // A block call rather than a predecessor, since redirecting an edge wants the slot in the
176 // pool and there is no finding it again from the block the edge used to arrive at.
177 let mut edges: Edges = incoming(func);
178 let leaky = leaky(func);
179 let unbound = Bindings::new();
180 let mut threaded = false;
181 'blocks: for block in func.blocks().collect::<Vec<Block>>() {
182 if block == entry || func[block].params.is_empty() {
183 continue;
184 }
185 let Some(term) = func.terminator(block) else { continue };
186 if !matches!(func[term].opcode, Opcode::BrIf | Opcode::Switch) {
187 continue;
188 }
189 // A branch that goes the same way whichever edge control arrived on is `simplify-cfg`'s
190 // to fold, and folding it once is cheaper than pointing every edge into the block at the
191 // same arm separately.
192 if taken(func, term, &unbound).is_some() {
193 continue;
194 }
195 // Two of the three reasons section 23.1 copies B are about the block rather than about
196 // one edge into it, so they are settled once here and not once per edge below.
197 let copied = if !skippable(func, block) {
198 Some(WOULD_COPY_EFFECT)
199 } else if leaky.contains(&block) {
200 Some(WOULD_COPY_READ_BELOW)
201 } else {
202 None
203 };
204 for (from, at) in edges.get(&block).cloned().unwrap_or_default() {
205 // A block that branches to itself, where the branch resolves, is a loop that does
206 // not end, and redirecting its own edge is not a description of anything a person
207 // wrote. The block below refuses it as well, since the block is its own latch.
208 if from == block {
209 continue;
210 }
211 let subst = bind(func, block, at);
212 let Some(call) = taken(func, term, &subst) else { continue };
213 if call.block == block {
214 continue;
215 }
216 if let Some(reason) = copied {
217 stats.missed(reason);
218 continue;
219 }
220 let Some(args) = carried(func, block, call, &subst) else {
221 stats.missed(WOULD_COPY_CARRIED);
222 continue;
223 };
224 if !allowed(an.loops(func), from, call.block) {
225 stats.missed(WOULD_BREAK_A_LOOP);
226 continue;
227 }
228 if !fuel.take() {
229 // Where the pass stops rather than where it starts skipping, because a budget
230 // that has reached zero will not have anything in it at the next block either
231 // and the two refusals above are the counts worth being true.
232 stats.missed(NO_FUEL);
233 break 'blocks;
234 }
235 let args = func.push_values(&args);
236 func.set_block_call(at, BlockCall { block: call.block, args });
237 // The record has to follow the edge, so that a block further down the walk sees the
238 // predecessor it now has. That is what lets one thread make the next one possible
239 // within the single walk this pass is.
240 if let Some(list) = edges.get_mut(&block) {
241 list.retain(|&(_, slot)| slot != at);
242 }
243 edges.entry(call.block).or_default().push((from, at));
244 // The loop forest was about the function as it was a moment ago, and the manager
245 // clears the cache after the pass returns, which is too late for the next edge.
246 an.clear();
247 stats.optimized(THREADED);
248 threaded = true;
249 }
250 }
251 if threaded {
252 // Threading every edge into a block leaves nothing arriving at it, and section 6.5
253 // makes taking an unreachable block out the standing obligation of whichever pass
254 // stranded it rather than something the next pass tidies up. The verifier holds every
255 // pass to that, so this is not a courtesy.
256 sweep(func, an, &mut stats);
257 }
258 stats
259 }
260}
261
262/// What this block's parameters hold along one edge into it.
263fn bind(func: &Func, block: Block, at: Idx<BlockCall>) -> Bindings {
264 let args = func[at].args;
265 let params = func[block].params.iter().copied();
266 params.zip(func[args].iter().copied()).collect()
267}
268
269/// The arguments the redirected edge carries, or `None` when one of them is only computed here.
270///
271/// A parameter of the block is replaced by whatever the edge being redirected was passing for it. A
272/// value from anywhere else is passed on as it stands, because a value used in this block and
273/// defined outside it dominates the predecessor, which is the argument the module doc makes. A value
274/// defined by an instruction in this block is the case that needs section 23.1's copy, and it is the
275/// answer this returns `None` for.
276///
277/// [`leaky`] does not cover this one. An argument on the arm is read by the block's own terminator,
278/// so the value never leaves the block by that route and the block is not leaky on account of it.
279/// The two checks are about the two ways a value gets out, and both are needed.
280fn carried(func: &Func, block: Block, call: BlockCall, subst: &Bindings) -> Option<Vec<Value>> {
281 let mut out = Vec::with_capacity(func[call.args].len());
282 for &arg in &func[call.args] {
283 if let Some(&bound) = subst.get(&arg) {
284 out.push(bound);
285 continue;
286 }
287 if let Def::Result { inst, .. } = func[arg].def {
288 if func.block_of(inst) == Some(block) {
289 return None;
290 }
291 }
292 out.push(arg);
293 }
294 Some(out)
295}
296
297/// Whether a path may walk past everything this block does on the way to its terminator.
298///
299/// The predicate is [`Opcode::has_effects`], which is what [`crate::dce`] deletes an instruction
300/// under, and the terminator is exempt because the thread is what replaces it. `is_terminator` on
301/// the function rather than on the opcode, for the reason dead code elimination gives: `asm goto`
302/// branches and its opcode does not say so.
303///
304/// A load answers that it has effects, so a block with one in it is not threaded past. That is
305/// conservative rather than necessary, since skipping a load skips a value nothing on the threaded
306/// path reads, and it is most of what [`WOULD_COPY_EFFECT`] turns out to be counting.
307fn skippable(func: &Func, block: Block) -> bool {
308 func.insts(block).all(|inst| func.is_terminator(inst) || !func[inst].opcode.has_effects())
309}
310
311/// Every block that defines a value read from somewhere other than itself.
312///
313/// The other half of what section 23.1's copy is for, and the half an argument list does not show.
314/// A block the candidate dominates reads what the candidate defined with nothing carrying it across,
315/// because dominance is the only permission a use needs in this IR. Point an edge past the candidate
316/// and that dominance is gone, so the read below is of a value nothing on the new path computed.
317///
318/// One walk for the whole function rather than one per candidate block, and it is computed once and
319/// never refreshed. It only goes stale in the safe direction. Threading never adds a read of a value
320/// defined in the block it went past, since [`carried`] refuses the edge when an arm carries one and
321/// everything else it passes on was defined further up, so a block in here can only ever have
322/// belonged in here less than it did.
323fn leaky(func: &Func) -> HashSet<Block> {
324 let mut out = HashSet::new();
325 for block in func.blocks().collect::<Vec<Block>>() {
326 for inst in func.insts(block).collect::<Vec<Inst>>() {
327 uses::operands(func, inst, |value| {
328 if let Some(home) = defined_in(func, value) {
329 if home != block {
330 out.insert(home);
331 }
332 }
333 });
334 }
335 }
336 out
337}
338
339/// The block a value comes from, whether it is a parameter of one or a result computed in one.
340fn defined_in(func: &Func, value: Value) -> Option<Block> {
341 match func[value].def {
342 Def::Result { inst, .. } => func.block_of(inst),
343 Def::Param { block, .. } => Some(block),
344 }
345}
346
347/// Whether the loop structure survives pointing this edge at that block.
348///
349/// Section 23.5, and every answer of `false` is a refusal rather than a cost. Entering a loop
350/// anywhere but at its header makes the loop irreducible, moving a latch's edge is how the single
351/// latch property stops holding, and a block the forest has already given up on is one there is no
352/// useful answer about.
353fn allowed(loops: &Loops, from: Block, into: Block) -> bool {
354 if loops.is_irreducible(from) || loops.is_irreducible(into) {
355 return false;
356 }
357 if loops.all().any(|id| loops.latches(id).contains(&from)) {
358 return false;
359 }
360 let mut id = loops.innermost(into);
361 while let Some(loop_id) = id {
362 // Only a loop the predecessor is outside of, because an edge that stays within a loop is
363 // not a way into it.
364 if !loops.contains(loop_id, from) && loops.header(loop_id) != into {
365 return false;
366 }
367 id = loops.parent(loop_id);
368 }
369 true
370}
371
372#[cfg(test)]
373mod tests {
374 use rucc_base::Interner;
375 use rucc_ir::{
376 Block, Builder, Flags, Func, IntPred, MemInfo, MemOrder, Restrict, Signature, Type, Value,
377 };
378
379 use super::Thread;
380 use crate::stats::Kind;
381 use crate::{Analyses, Fuel, Pass, Stats};
382
383 /// Runs the pass with as much fuel as it wants.
384 fn thread(func: &mut Func) -> Stats {
385 Thread.run(func, &mut Analyses::new(), &mut Fuel::unlimited())
386 }
387
388 /// The blocks the function still has, by number.
389 fn blocks(func: &Func) -> Vec<usize> {
390 func.blocks().map(Block::index).collect()
391 }
392
393 /// Where a block's terminator goes, as block numbers.
394 fn goes_to(func: &Func, block: usize) -> Vec<usize> {
395 let block = Block::from_usize(block);
396 let term = func.terminator(block).expect("every block here has one");
397 func.successors(term).map(|call| call.block.index()).collect()
398 }
399
400 /// What a block's terminator carries on its first edge.
401 fn carries(func: &Func, block: usize) -> Vec<Value> {
402 let block = Block::from_usize(block);
403 let term = func.terminator(block).expect("every block here has one");
404 let call = func.successors(term).next().expect("a terminator here has an edge");
405 func[call.args].to_vec()
406 }
407
408 /// Section 23.3's example: two arms set one value to two constants and a join tests it.
409 ///
410 /// Block 0 is the entry, blocks 1 and 2 are the arms carrying `left` and `right`, block 3 is
411 /// the join and takes the value as a parameter, and blocks 4 and 5 are the two ways the test
412 /// can come out. The value the arms carry comes back, so a test can say which one was
413 /// substituted into what.
414 fn diamond(left: i128, right: i128) -> (Func, [Value; 2]) {
415 let mut names = Interner::new();
416 let mut func = Func::new(names.intern("f"), Signature::new());
417 let entry = func.create_block();
418 let arms = [func.create_block(), func.create_block()];
419 let join = func.create_block();
420 let param = func.append_param(join, Type::int(32));
421 let yes = func.create_block();
422 let no = func.create_block();
423
424 let mut build = Builder::new(&mut func, entry);
425 let cond = build.iconst(Type::int(1), 1);
426 build.br_if(cond, arms[0], &[], arms[1], &[]);
427 let mut sent = Vec::new();
428 for (arm, value) in arms.iter().zip([left, right]) {
429 let mut build = Builder::new(&mut func, *arm);
430 let it = build.iconst(Type::int(32), value);
431 sent.push(it);
432 build.jump(join, &[it]);
433 }
434 let mut build = Builder::new(&mut func, join);
435 let one = build.iconst(Type::int(32), 1);
436 let test = build.icmp(IntPred::Eq, param, one);
437 build.br_if(test, yes, &[], no, &[]);
438 for block in [yes, no] {
439 let mut build = Builder::new(&mut func, block);
440 build.ret(&[]);
441 }
442 (func, [sent[0], sent[1]])
443 }
444
445 #[test]
446 fn both_edges_of_a_join_that_decides_its_test_are_threaded() {
447 let (mut func, _) = diamond(1, 2);
448 let stats = thread(&mut func);
449 assert_eq!(stats.count(Kind::Optimized, super::THREADED), 2);
450 // The arm carrying 1 goes to the true side and the arm carrying 2 to the false side, so
451 // the block that tested it has nothing left arriving at it.
452 assert_eq!(goes_to(&func, 1), vec![4]);
453 assert_eq!(goes_to(&func, 2), vec![5]);
454 // And nothing arrives at the block that tested it, so it goes with the same sweep
455 // `simplify-cfg` uses. The verifier holds a pass to that rather than letting the next one
456 // tidy up after it.
457 assert_eq!(blocks(&func), vec![0, 1, 2, 4, 5]);
458 assert_eq!(stats.count(Kind::Optimized, crate::simplify_cfg::REMOVED), 1);
459 }
460
461 #[test]
462 fn an_edge_that_does_not_decide_the_test_is_left_alone() {
463 let mut names = Interner::new();
464 let signature = Signature::new().with_params(&[Type::int(32)]);
465 let mut func = Func::new(names.intern("f"), signature);
466 let entry = func.create_block();
467 // A parameter of the function rather than a constant, so binding it to the block's
468 // parameter says nothing about the test.
469 let outside = func.append_param(entry, Type::int(32));
470 let join = func.create_block();
471 let param = func.append_param(join, Type::int(32));
472 let yes = func.create_block();
473 let no = func.create_block();
474
475 let mut build = Builder::new(&mut func, entry);
476 build.jump(join, &[outside]);
477 let mut build = Builder::new(&mut func, join);
478 let one = build.iconst(Type::int(32), 1);
479 let test = build.icmp(IntPred::Eq, param, one);
480 build.br_if(test, yes, &[], no, &[]);
481 for block in [yes, no] {
482 let mut build = Builder::new(&mut func, block);
483 build.ret(&[]);
484 }
485
486 let stats = thread(&mut func);
487 assert_eq!(stats.count(Kind::Optimized, super::THREADED), 0);
488 assert_eq!(goes_to(&func, 0), vec![1]);
489 }
490
491 #[test]
492 fn a_branch_decided_whichever_way_control_arrived_is_left_to_simplify_cfg() {
493 let mut names = Interner::new();
494 let mut func = Func::new(names.intern("f"), Signature::new());
495 let entry = func.create_block();
496 let arms = [func.create_block(), func.create_block()];
497 let join = func.create_block();
498 func.append_param(join, Type::int(32));
499 let yes = func.create_block();
500 let no = func.create_block();
501
502 let mut build = Builder::new(&mut func, entry);
503 let cond = build.iconst(Type::int(1), 1);
504 build.br_if(cond, arms[0], &[], arms[1], &[]);
505 for (arm, value) in arms.iter().zip([1, 2]) {
506 let mut build = Builder::new(&mut func, *arm);
507 let it = build.iconst(Type::int(32), value);
508 build.jump(join, &[it]);
509 }
510 let mut build = Builder::new(&mut func, join);
511 // The test reads nothing the edges carry, so it comes out the same way whichever edge
512 // control arrived on and it is `simplify-cfg`'s to fold once rather than this pass's to
513 // point every edge at separately.
514 let known = build.iconst(Type::int(1), 1);
515 build.br_if(known, yes, &[], no, &[]);
516 for block in [yes, no] {
517 let mut build = Builder::new(&mut func, block);
518 build.ret(&[]);
519 }
520
521 let stats = thread(&mut func);
522 assert_eq!(stats.count(Kind::Optimized, super::THREADED), 0);
523 assert_eq!(goes_to(&func, 1), vec![3]);
524 assert_eq!(goes_to(&func, 2), vec![3]);
525 }
526
527 #[test]
528 fn a_block_with_something_that_happens_in_it_needs_the_copy() {
529 let mut names = Interner::new();
530 let mut func = Func::new(names.intern("f"), Signature::new());
531 let entry = func.create_block();
532 let arms = [func.create_block(), func.create_block()];
533 let join = func.create_block();
534 let param = func.append_param(join, Type::int(32));
535 let yes = func.create_block();
536 let no = func.create_block();
537
538 let mut build = Builder::new(&mut func, entry);
539 let cond = build.iconst(Type::int(1), 1);
540 build.br_if(cond, arms[0], &[], arms[1], &[]);
541 for (arm, value) in arms.iter().zip([1, 2]) {
542 let mut build = Builder::new(&mut func, *arm);
543 let it = build.iconst(Type::int(32), value);
544 build.jump(join, &[it]);
545 }
546 let mut build = Builder::new(&mut func, join);
547 // A store above the test. It has to happen on every path that reached the block, so no
548 // path may walk past it, and threading either edge would be a path that did.
549 let what = build.iconst(Type::int(32), 7);
550 let address = build.iconst(Type::int(64), 16);
551 let address = build.unary(rucc_ir::Opcode::IntToPtr, address, Type::PTR);
552 let info = MemInfo {
553 size: 4,
554 align: 4,
555 order: MemOrder::NotAtomic,
556 tbaa: None,
557 restrict: Restrict::NONE,
558 };
559 build.store(what, address, info, Flags::NONE);
560 let one = build.iconst(Type::int(32), 1);
561 let test = build.icmp(IntPred::Eq, param, one);
562 build.br_if(test, yes, &[], no, &[]);
563 for block in [yes, no] {
564 let mut build = Builder::new(&mut func, block);
565 build.ret(&[]);
566 }
567
568 let stats = thread(&mut func);
569 assert_eq!(stats.count(Kind::Optimized, super::THREADED), 0);
570 assert_eq!(stats.count(Kind::Missed, super::WOULD_COPY_EFFECT), 2);
571 }
572
573 /// The shape a clamp compiles to, which is where the corpus caught this being wrong.
574 ///
575 /// `raw < 15 ? 15 : raw` puts the value under test in a block parameter and then hands the same
576 /// parameter to the arm that did not change it. The arm reads it with nothing carrying it there,
577 /// because the join dominates the arm, and an edge threaded past the join is a path on which the
578 /// read has no value behind it. It compiled to a program that printed the wrong number.
579 #[test]
580 fn a_value_the_block_defines_and_something_below_it_reads_needs_the_copy() {
581 let mut names = Interner::new();
582 let mut func = Func::new(names.intern("f"), Signature::new());
583 let entry = func.create_block();
584 let arms = [func.create_block(), func.create_block()];
585 let join = func.create_block();
586 let param = func.append_param(join, Type::int(32));
587 let yes = func.create_block();
588 let no = func.create_block();
589
590 let mut build = Builder::new(&mut func, entry);
591 let cond = build.iconst(Type::int(1), 1);
592 build.br_if(cond, arms[0], &[], arms[1], &[]);
593 for (arm, value) in arms.iter().zip([1, 2]) {
594 let mut build = Builder::new(&mut func, *arm);
595 let it = build.iconst(Type::int(32), value);
596 build.jump(join, &[it]);
597 }
598 let mut build = Builder::new(&mut func, join);
599 let one = build.iconst(Type::int(32), 1);
600 let test = build.icmp(IntPred::Eq, param, one);
601 build.br_if(test, yes, &[], no, &[]);
602 let mut build = Builder::new(&mut func, yes);
603 build.ret(&[]);
604 // The read from below. Nothing on the edge carries the parameter here, and nothing has to,
605 // since every path to this block goes through the block that defines it.
606 let mut build = Builder::new(&mut func, no);
607 build.ret(&[param]);
608
609 let stats = thread(&mut func);
610 assert_eq!(stats.count(Kind::Optimized, super::THREADED), 0);
611 assert_eq!(stats.count(Kind::Missed, super::WOULD_COPY_READ_BELOW), 2);
612 assert_eq!(goes_to(&func, 1), vec![3]);
613 assert_eq!(goes_to(&func, 2), vec![3]);
614 }
615
616 #[test]
617 fn an_arm_carrying_a_value_the_block_worked_out_needs_the_copy() {
618 let mut names = Interner::new();
619 let mut func = Func::new(names.intern("f"), Signature::new());
620 let entry = func.create_block();
621 let arms = [func.create_block(), func.create_block()];
622 let join = func.create_block();
623 let param = func.append_param(join, Type::int(32));
624 let yes = func.create_block();
625 func.append_param(yes, Type::int(32));
626 let no = func.create_block();
627
628 let mut build = Builder::new(&mut func, entry);
629 let cond = build.iconst(Type::int(1), 1);
630 build.br_if(cond, arms[0], &[], arms[1], &[]);
631 for (arm, value) in arms.iter().zip([1, 2]) {
632 let mut build = Builder::new(&mut func, *arm);
633 let it = build.iconst(Type::int(32), value);
634 build.jump(join, &[it]);
635 }
636 let mut build = Builder::new(&mut func, join);
637 let one = build.iconst(Type::int(32), 1);
638 let test = build.icmp(IntPred::Eq, param, one);
639 // The true arm carries a sum this block worked out, which is exactly the value section
640 // 23.1's copy of the block exists to make available on the threaded path.
641 let sum = build.binary(rucc_ir::Opcode::Add, param, one, Flags::NONE);
642 build.br_if(test, yes, &[sum], no, &[]);
643 for block in [yes, no] {
644 let mut build = Builder::new(&mut func, block);
645 build.ret(&[]);
646 }
647
648 let stats = thread(&mut func);
649 // The edge carrying 2 takes the false arm, which carries nothing, so it threads. The one
650 // carrying 1 takes the arm with the sum on it and is the one that would need the copy.
651 assert_eq!(stats.count(Kind::Optimized, super::THREADED), 1);
652 assert_eq!(stats.count(Kind::Missed, super::WOULD_COPY_CARRIED), 1);
653 assert_eq!(goes_to(&func, 2), vec![5]);
654 assert_eq!(goes_to(&func, 1), vec![3]);
655 }
656
657 #[test]
658 fn the_block_parameter_is_substituted_into_what_the_arm_carries() {
659 let mut names = Interner::new();
660 let mut func = Func::new(names.intern("f"), Signature::new());
661 let entry = func.create_block();
662 let arms = [func.create_block(), func.create_block()];
663 let join = func.create_block();
664 let param = func.append_param(join, Type::int(32));
665 let yes = func.create_block();
666 func.append_param(yes, Type::int(32));
667 let no = func.create_block();
668
669 let mut build = Builder::new(&mut func, entry);
670 let cond = build.iconst(Type::int(1), 1);
671 build.br_if(cond, arms[0], &[], arms[1], &[]);
672 let mut sent = Vec::new();
673 for (arm, value) in arms.iter().zip([1, 2]) {
674 let mut build = Builder::new(&mut func, *arm);
675 let it = build.iconst(Type::int(32), value);
676 sent.push(it);
677 build.jump(join, &[it]);
678 }
679 let mut build = Builder::new(&mut func, join);
680 let one = build.iconst(Type::int(32), 1);
681 let test = build.icmp(IntPred::Eq, param, one);
682 // The arm passes the block's own parameter on, which along each edge is the constant that
683 // edge was carrying.
684 build.br_if(test, yes, &[param], no, &[]);
685 for block in [yes, no] {
686 let mut build = Builder::new(&mut func, block);
687 build.ret(&[]);
688 }
689
690 let stats = thread(&mut func);
691 assert_eq!(stats.count(Kind::Optimized, super::THREADED), 2);
692 assert_eq!(goes_to(&func, 1), vec![4]);
693 assert_eq!(carries(&func, 1), vec![sent[0]]);
694 }
695
696 #[test]
697 fn a_switch_the_edge_decides_is_threaded() {
698 let mut names = Interner::new();
699 let mut func = Func::new(names.intern("f"), Signature::new());
700 let entry = func.create_block();
701 let arms = [func.create_block(), func.create_block()];
702 let join = func.create_block();
703 let param = func.append_param(join, Type::int(32));
704 let cases = [func.create_block(), func.create_block(), func.create_block()];
705
706 let mut build = Builder::new(&mut func, entry);
707 let cond = build.iconst(Type::int(1), 1);
708 build.br_if(cond, arms[0], &[], arms[1], &[]);
709 for (arm, value) in arms.iter().zip([0, 1]) {
710 let mut build = Builder::new(&mut func, *arm);
711 let it = build.iconst(Type::int(32), value);
712 build.jump(join, &[it]);
713 }
714 let mut build = Builder::new(&mut func, join);
715 build.switch(param, cases[0], &[(0, cases[1]), (1, cases[2])]);
716 for block in cases {
717 let mut build = Builder::new(&mut func, block);
718 build.ret(&[]);
719 }
720
721 let stats = thread(&mut func);
722 assert_eq!(stats.count(Kind::Optimized, super::THREADED), 2);
723 assert_eq!(goes_to(&func, 1), vec![cases[1].index()]);
724 assert_eq!(goes_to(&func, 2), vec![cases[2].index()]);
725 }
726
727 /// A loop whose header takes a parameter, entered from outside with a constant.
728 ///
729 /// Block 0 is the entry and jumps into the header carrying 1, block 1 is the header and tests
730 /// its parameter, block 2 is the body and jumps back carrying the function's own parameter,
731 /// block 3 is the way out and is where the test's false arm goes, and block 4 is somewhere
732 /// outside the loop. Which block the true arm goes to is the caller's to choose, which is what
733 /// makes one of these a thread into the middle of the loop and the other a thread onto a block
734 /// the loop has nothing to do with.
735 ///
736 /// This is the only shape in which a thread can make a loop irreducible when nothing is copied.
737 /// The block being threaded past has to be the header itself, because otherwise the arm being
738 /// threaded onto was already a way into the loop from outside it and the loop was already
739 /// irreducible before this pass looked at it.
740 fn loop_with_a_parameter(arm: usize) -> Func {
741 let mut names = Interner::new();
742 let signature = Signature::new().with_params(&[Type::int(32)]);
743 let mut func = Func::new(names.intern("f"), signature);
744 let entry = func.create_block();
745 let outside = func.append_param(entry, Type::int(32));
746 let header = func.create_block();
747 let param = func.append_param(header, Type::int(32));
748 let body = func.create_block();
749 let out = func.create_block();
750 let elsewhere = func.create_block();
751 let taken = [entry, header, body, out, elsewhere][arm];
752
753 let mut build = Builder::new(&mut func, entry);
754 let one = build.iconst(Type::int(32), 1);
755 build.jump(header, &[one]);
756 let mut build = Builder::new(&mut func, header);
757 let lit = build.iconst(Type::int(32), 1);
758 let test = build.icmp(IntPred::Eq, param, lit);
759 build.br_if(test, taken, &[], out, &[]);
760 let mut build = Builder::new(&mut func, body);
761 // Carrying the function's own parameter, so the edge back decides nothing and each of
762 // these tests is about the one edge that comes from outside.
763 build.jump(header, &[outside]);
764 for block in [out, elsewhere] {
765 let mut build = Builder::new(&mut func, block);
766 build.ret(&[]);
767 }
768 func
769 }
770
771 #[test]
772 fn threading_into_a_loop_anywhere_but_its_header_is_refused() {
773 // The true arm is the body, so pointing the edge from outside at it would give the loop a
774 // second way in and make it irreducible.
775 let mut func = loop_with_a_parameter(2);
776 let stats = thread(&mut func);
777 assert_eq!(stats.count(Kind::Optimized, super::THREADED), 0);
778 assert_eq!(stats.count(Kind::Missed, super::WOULD_BREAK_A_LOOP), 1);
779 assert_eq!(goes_to(&func, 0), vec![1]);
780 }
781
782 #[test]
783 fn threading_onto_a_block_outside_the_loop_is_allowed() {
784 // The true arm is in no loop at all, so the edge from outside can be pointed straight at
785 // it and the loop keeps the one way in it had.
786 let mut func = loop_with_a_parameter(4);
787 let stats = thread(&mut func);
788 assert_eq!(stats.count(Kind::Optimized, super::THREADED), 1);
789 assert_eq!(goes_to(&func, 0), vec![4]);
790 }
791
792 #[test]
793 fn threading_onto_the_header_of_a_loop_is_allowed() {
794 let mut names = Interner::new();
795 let mut func = Func::new(names.intern("f"), Signature::new());
796 let entry = func.create_block();
797 let join = func.create_block();
798 let param = func.append_param(join, Type::int(32));
799 let header = func.create_block();
800 let out = func.create_block();
801
802 let mut build = Builder::new(&mut func, entry);
803 let one = build.iconst(Type::int(32), 1);
804 build.jump(join, &[one]);
805 let mut build = Builder::new(&mut func, join);
806 let lit = build.iconst(Type::int(32), 1);
807 let test = build.icmp(IntPred::Eq, param, lit);
808 build.br_if(test, header, &[], out, &[]);
809 let mut build = Builder::new(&mut func, header);
810 // A loop of one block, so the header is its own latch and the block being threaded onto
811 // is the header itself, which is the way in the loop already has.
812 let again = build.iconst(Type::int(1), 1);
813 build.br_if(again, header, &[], out, &[]);
814 let mut build = Builder::new(&mut func, out);
815 build.ret(&[]);
816
817 let stats = thread(&mut func);
818 assert_eq!(stats.count(Kind::Optimized, super::THREADED), 1);
819 assert_eq!(goes_to(&func, 0), vec![2]);
820 }
821
822 #[test]
823 fn fuel_stops_the_threading_where_it_stands() {
824 let (mut func, _) = diamond(1, 2);
825 let mut fuel = Fuel::of(1);
826 let stats = Thread.run(&mut func, &mut Analyses::new(), &mut fuel);
827 assert_eq!(stats.count(Kind::Optimized, super::THREADED), 1);
828 assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
829 assert_eq!(goes_to(&func, 2), vec![3], "the second edge is where it was");
830 }
831}