rucc_opt/split.rs
1//! Splits a loop into a run of iterations that needs no checks and the rest of it, which keeps them.
2//!
3//! Design: `spec/safe-memory/07-check-elimination.md` section 7.4, which names this and says what it
4//! is for: "Loop splitting is the general form. The checked part and the unchecked part are divided
5//! at `min(n, extent / sizeof(T))`."
6//!
7//! [`crate::hoist`] is the pass next door and it answers a different question. It puts one check in
8//! front of a loop that covers every access the loop makes, which needs the loop to make every one
9//! of them: an exact count, one way out, and a check every iteration reaches. Most loops in real code
10//! are not like that. The census on tamnd/rucc#782 says that of the roughly fifteen hundred checks
11//! SQLite still carries at `-O2`, six hundred and ninety two are in loops with a second way out and
12//! sixty four are checks an iteration can finish without reaching. Neither is a loop hoisting can say
13//! anything about, and both are loops this one can, because it never has to claim the loop reaches
14//! the end of what it might read. It only has to know a prefix that is safe.
15//!
16//! # What the two halves are
17//!
18//! The loop is copied. The original becomes the fast half and loses its checks, the copy becomes the
19//! slow half and keeps them, and a new block in front of the original decides which one runs. That
20//! block counts iterations of the fast half and hands over to the slow half once the count reaches a
21//! limit worked out in the preheader.
22//!
23//! The counter is a new one rather than the loop's own, and the test is against a limit rather than
24//! against anything the loop compares. That is what makes this work on a loop with several ways out:
25//! the fast half keeps every exit the loop had, so leaving early still leaves early, and the extra
26//! test is only ever the reason the fast half stops early and never the reason it runs longer.
27//!
28//! # Where the limit comes from
29//!
30//! For a check whose address is `first + i * step` reading `reach` bytes each time, every iteration
31//! with `i * step + reach <= extent` is one the check cannot fail on, where `extent` is how many
32//! bytes from `first` on belong to whatever owns `first`. So the limit is
33//! `(extent - reach) / step + 1`, or zero when `extent` is smaller than `reach`, and where a loop has
34//! several such checks in it the limit is the smallest of theirs.
35//!
36//! The extent is the half of that a compiler cannot work out, so it is asked at run time, through the
37//! `cap_extent` query that tamnd/rucc#792 added. The query takes a limit on how far to look and the
38//! answer is never more than that, which is why this pass still wants a trip count: what it asks for
39//! is how many bytes the loop was going to read anyway, so the walk in the runtime is bounded by work
40//! the loop is already doing. A count that is too small costs iterations in the slow half and a count
41//! that is too large costs a slightly longer walk, and neither is a wrong answer, which is why the
42//! count is read from any exit that offers one rather than from an exit that runs every time.
43//!
44//! # Why the fast half may drop a check
45//!
46//! `check_bounds` asks whether the bytes an access names lie inside one object. Every address in
47//! `[first, first + extent)` is inside the object that owns `first`, by what the query answers, and
48//! the limit is exactly the iterations whose access stays in that window. So no check in the fast
49//! half could have failed.
50//!
51//! `check_live` asks whether anything owns the address right now, and the query answered that too,
52//! since a byte belonging to the owner of `first` is a byte with an owner. Right now is the catch,
53//! and it is why nothing that could free may be in the loop. A call in the body could free the object
54//! between the question and the iteration that reads it, and then the fast half would read freed
55//! storage with nothing to say so. That is the same restriction hoisting has, for a weaker reason,
56//! and lifting it is a matter of asking `crate::nofree` about the callee rather than refusing every
57//! call.
58//!
59//! Two answers of the query carry the weight and both are argued where the query is implemented. An
60//! address no watched region covers gets the whole limit back, so a loop over a local or a global
61//! splits into a fast half that runs the whole way, which is right because no check on such an
62//! address ever fires under this milestone. An address whose granule nobody owns gets zero, so the
63//! limit is zero, the fast half runs no iterations, and the check inside the slow half is what reports
64//! the dangling pointer, at the access rather than at the loop.
65//!
66//! # Which loops
67//!
68//! Innermost, one latch, a preheader, a count, nothing in it that could free, and no value defined
69//! inside it that anything outside reads. The last is loop closed form, which [`crate::canon`]
70//! establishes, and it is checked rather than assumed because the copy would otherwise leave a reader
71//! outside the loop seeing whichever half happened to define the value.
72//!
73//! Canonicalization runs a long way in front of this, and `simplify-cfg` between the two undoes some
74//! of what it did, so on SQLite the closed form condition is what refuses 351 of the checks this
75//! would otherwise have taken out. Running canonicalization again in front of this gets 156 of them
76//! back and costs 17672 bytes of `.text`, which is a bad trade for eleven more checks, so the answer
77//! is for this to repair the exits of the one loop it is splitting rather than for the pipeline to
78//! repair every loop in the function. That is its own piece of work.
79//!
80//! Not every check in the loop has to be one this can size. A check whose address the analysis cannot
81//! follow simply stays in both halves, and the fast half is then a loop with fewer checks in it rather
82//! than none. That is worth having on its own and it is worth having because it is what a real loop
83//! looks like: one sweep the analysis reads and one index that came out of a table.
84//!
85//! # Which level
86//!
87//! `-O2` and `-O3`, alongside `crate::unroll` and for the same reason. The loop body is copied, so
88//! the function grows by about the size of the loop, and buying speed with code is what those levels
89//! are for and what `-Os` and `-Oz` are for declining.
90
91use std::collections::{HashMap, HashSet};
92
93use rucc_cost::heuristics;
94use rucc_ir::{
95 Block, BlockCall, Builder, Extra, Flags, Func, Inst, InstData, IntPred, Opcode, Type, Value,
96};
97
98use crate::cfg::Cfg;
99use crate::copy;
100use crate::discharge::operand_of;
101use crate::loops::{LoopId, Loops};
102use crate::scev::{Evolution, Scev};
103use crate::trip::{Around, counted, covered, inst_of};
104use crate::{Analyses, Fuel, Pass, Preserved, Stats};
105
106/// What is reported when a loop is split.
107const SPLIT: &str = "loop split, the iterations in front of the first one that could fail a check \
108 run without them";
109
110/// What is reported when the pass ran out of fuel with a loop it was about to split.
111const NO_FUEL: &str = "loop left alone, the pass ran out of fuel";
112
113/// What is reported for a loop with nowhere to work the limit out.
114const NO_PREHEADER: &str = "loop left alone, it has no block in front of it to put a check in";
115
116/// What is reported for a loop with a loop inside it.
117const A_LOOP_INSIDE: &str = "loop left alone, it has another loop inside it";
118
119/// What is reported for a loop with more than one way round.
120const MANY_LATCHES: &str = "loop left alone, it goes back to its header from more than one place";
121
122/// What is reported for a loop with a call in it.
123const A_CALL_INSIDE: &str = "loop left alone, a call in it might free what the loop is reading";
124
125/// What is reported for a loop holding something the copier cannot copy.
126const NOT_COPYABLE: &str = "loop left alone, something in it carries a side table this cannot copy";
127
128/// What is reported for a loop whose values are read after it without going through a parameter.
129const ESCAPES: &str = "loop left alone, a value it defines is read outside it";
130
131/// What is reported for a loop whose two halves would be too much code.
132const TOO_BIG: &str = "loop left alone, the two halves would be more code than the limit allows";
133
134/// What is reported for a check whose address does not walk the loop.
135const NOT_A_SWEEP: &str = "check kept in both halves, its address does not walk the loop by a \
136 constant";
137
138/// What is reported for a check whose address the analysis has nothing to say about.
139const NOT_FOLLOWED: &str = "check kept in both halves, what its address does round the loop is not \
140 something the analysis follows";
141
142/// What is reported for a check whose address does not move.
143const DOES_NOT_MOVE: &str = "check kept in both halves, its address is the same every iteration";
144
145/// What is reported for a check whose address walks backwards.
146const BACKWARDS: &str = "check kept in both halves, its address walks the loop from high to low";
147
148/// What is reported for a check whose step does not keep its alignment.
149const MISALIGNED: &str =
150 "check kept in both halves, its step is not a whole number of its alignment";
151
152/// What is reported for a check that already covers a range the program worked out.
153const ALREADY_COMPUTED: &str =
154 "check kept in both halves, how many bytes it covers is a number only the program has";
155
156/// The pass.
157#[derive(Debug)]
158pub struct Split;
159
160impl Pass for Split {
161 fn name(&self) -> &'static str {
162 "split"
163 }
164
165 fn describe(&self) -> &'static str {
166 "a loop becomes a run of iterations with no checks in it and the rest of the loop with them"
167 }
168
169 fn preserves(&self) -> Preserved {
170 // Blocks appear and edges move, so nothing built on the graph stands.
171 Preserved::NONE
172 }
173
174 fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
175 let mut stats = Stats::new();
176 if func.entry().is_none() {
177 return stats;
178 }
179 let cfg = an.cfg(func).clone();
180 let loops = an.loops(func).clone();
181 if loops.count() == 0 {
182 return stats;
183 }
184
185 // Worked out first and applied afterwards, because scalar evolution reads the function and
186 // the transformation writes it. Every plan is about an innermost loop and no two innermost
187 // loops share a block, so applying one leaves every other one's blocks where they were.
188 let mut plans = Vec::new();
189 {
190 let mut scev = Scev::new(func, &cfg, &loops);
191 for id in loops.all() {
192 sweep(func, &cfg, &loops, &mut scev, id, &mut plans, &mut stats);
193 }
194 }
195
196 let mut changed = false;
197 for plan in plans {
198 if !fuel.take() {
199 stats.missed(NO_FUEL);
200 continue;
201 }
202 apply(func, &plan);
203 stats.optimized(SPLIT);
204 changed = true;
205 }
206 if changed {
207 an.clear();
208 }
209 stats
210 }
211}
212
213/// One check the fast half will not need, and the walk that says so.
214#[derive(Debug)]
215struct Sweep {
216 /// The check itself, which is removed from the fast half and kept in the copy.
217 check: Inst,
218 /// Where the first iteration's address is computed from.
219 base: Value,
220 /// How far past that value the first iteration reads.
221 offset: i128,
222 /// How far the address moves each time round, which is a positive number of bytes.
223 step: i128,
224 /// How many bytes one access covers.
225 reach: i128,
226}
227
228/// One loop to split, worked out before anything is written.
229#[derive(Debug)]
230struct Plan {
231 /// Where the limit is worked out.
232 preheader: Block,
233 /// The block the guard takes over from.
234 header: Block,
235 /// The block the back edge leaves from, which is where the iteration count goes up.
236 latch: Block,
237 /// Everything that is copied, which is the whole loop.
238 body: Vec<Block>,
239 /// How many times the loop goes round, which is what the runtime is asked to look no further
240 /// than.
241 around: Around,
242 /// The checks the fast half will not need, which is never empty in a plan.
243 sweeps: Vec<Sweep>,
244}
245
246/// Plans a loop, or counts what stopped it.
247///
248/// Nothing is reported for a loop with no check in it, because a loop that does no memory access is
249/// not a missed opportunity and a report for every one of them would bury the loops that are.
250fn sweep(
251 func: &Func,
252 cfg: &Cfg,
253 loops: &Loops,
254 scev: &mut Scev<'_>,
255 id: LoopId,
256 plans: &mut Vec<Plan>,
257 stats: &mut Stats,
258) {
259 let body = loops.blocks(id).to_vec();
260 let checks: Vec<Inst> = body
261 .iter()
262 .flat_map(|&block| func.insts(block).collect::<Vec<Inst>>())
263 .filter(|&inst| matches!(func[inst].opcode, Opcode::CheckBounds | Opcode::CheckLive))
264 .collect();
265 if checks.is_empty() {
266 return;
267 }
268
269 let (preheader, latch) = match shaped(func, cfg, loops, id, &body) {
270 Ok(shape) => shape,
271 Err(why) => {
272 stats.missed(why);
273 return;
274 }
275 };
276 let around = match counted(scev, id) {
277 Ok(around) => around,
278 Err(why) => {
279 stats.missed(why);
280 return;
281 }
282 };
283
284 let mut sweeps = Vec::new();
285 for check in checks {
286 match walked(func, scev, id, check) {
287 Ok(sweep) => sweeps.push(sweep),
288 Err(why) => stats.missed(why),
289 }
290 }
291 if sweeps.is_empty() {
292 return;
293 }
294 plans.push(Plan { preheader, header: loops.header(id), latch, body, around, sweeps });
295}
296
297/// The preheader and the latch of a loop this pass may copy, or why there is not one.
298///
299/// The conditions are the module comment's. The one worth restating is the call, because it is the
300/// only one that is about what the fast half is allowed to leave out rather than about whether the
301/// copy can be made at all: the extent is asked once before the loop and believed for the whole of
302/// the fast half, so anything that could hand the storage back in the middle would make the answer
303/// stale, and the fast half has nothing left in it to notice.
304fn shaped(
305 func: &Func,
306 cfg: &Cfg,
307 loops: &Loops,
308 id: LoopId,
309 body: &[Block],
310) -> Result<(Block, Block), &'static str> {
311 let Some(preheader) = loops.preheader(cfg, id) else {
312 return Err(NO_PREHEADER);
313 };
314 let [latch] = loops.latches(id) else {
315 return Err(MANY_LATCHES);
316 };
317 for &block in body {
318 if loops.innermost(block) != Some(id) {
319 return Err(A_LOOP_INSIDE);
320 }
321 for inst in func.insts(block) {
322 if matches!(
323 func[inst].opcode,
324 Opcode::Call
325 | Opcode::CallIndirect
326 | Opcode::TailCall
327 | Opcode::InlineAsm
328 | Opcode::MetaEnd
329 | Opcode::MetaTransfer
330 ) {
331 return Err(A_CALL_INSIDE);
332 }
333 if !copy::copyable(func, inst) {
334 return Err(NOT_COPYABLE);
335 }
336 }
337 }
338 let inside: HashSet<Block> = body.iter().copied().collect();
339 if escapes(func, body, &inside) {
340 return Err(ESCAPES);
341 }
342 let size = body.iter().map(|&block| func.insts(block).count()).sum::<usize>();
343 if size > heuristics::SPLIT_MAX_INSNS as usize {
344 return Err(TOO_BIG);
345 }
346 Ok((preheader, *latch))
347}
348
349/// Whether anything outside the loop reads a value defined inside it.
350///
351/// After [`crate::canon`] there is no such value, because loop closed form has already routed every
352/// one of them through a parameter of the block the loop leaves to. Where there is one, the two
353/// halves would leave it reading whichever of them happened to define it, so this is refused rather
354/// than repaired.
355fn escapes(func: &Func, body: &[Block], inside: &HashSet<Block>) -> bool {
356 let mut defined: HashSet<Value> = HashSet::new();
357 for &block in body {
358 defined.extend(func[block].params.iter().copied());
359 for inst in func.insts(block) {
360 defined.extend(func[inst].results());
361 }
362 }
363 for block in func.blocks() {
364 if inside.contains(&block) {
365 continue;
366 }
367 for inst in func.insts(block) {
368 if func[func[inst].args].iter().any(|value| defined.contains(value)) {
369 return true;
370 }
371 for call in func.successors(inst) {
372 if func[call.args].iter().any(|value| defined.contains(value)) {
373 return true;
374 }
375 }
376 }
377 }
378 false
379}
380
381/// What one check's address does round the loop, or why the pass cannot say.
382fn walked(
383 func: &Func,
384 scev: &mut Scev<'_>,
385 id: LoopId,
386 check: Inst,
387) -> Result<Sweep, &'static str> {
388 let args = &func[func[check].args];
389 // A check that already carries its own extent is one hoisting put somewhere, and how many bytes
390 // it covers is not a number this pass can divide by a step.
391 if args.len() > 2 {
392 return Err(ALREADY_COMPUTED);
393 }
394 let (Some(&capability), Some(&pointer)) = (args.first(), args.get(1)) else {
395 return Err(NOT_A_SWEEP);
396 };
397 if operand_of(func, capability, Opcode::CapOf, 0) != Some(pointer) {
398 return Err(NOT_A_SWEEP);
399 }
400 // A liveness check reads no bytes, so the window it needs is the one byte its address is in.
401 // A bounds check carries how many it reads in its payload.
402 let (reach, align) = match func[check].extra {
403 Extra::Mem(held) => (i128::from(func[held].size), i128::from(func[held].align)),
404 _ => (1, 1),
405 };
406
407 let chrec = match scev.evolution(id, pointer) {
408 Evolution::Affine(chrec) => chrec,
409 // An address that does not move at all is one check covering the same bytes every time
410 // round, which is a check to hoist rather than a loop to split, and hoisting is where that
411 // belongs. It is reported separately so the census says how often the two passes disagree
412 // about the same loop.
413 Evolution::Invariant(_) => return Err(DOES_NOT_MOVE),
414 _ => return Err(NOT_FOLLOWED),
415 };
416 let Some(step) = chrec.step.as_number() else {
417 return Err(NOT_A_SWEEP);
418 };
419 if step <= 0 {
420 return Err(BACKWARDS);
421 }
422 // Scale one because the base is an address. Anything else is a multiple of a pointer, which is
423 // not a thing the loop computed, so it is a shape this reads rather than a case to handle.
424 let (Some(base), 1) = (chrec.base.value, chrec.base.scale) else {
425 return Err(NOT_A_SWEEP);
426 };
427 if step % align != 0 {
428 return Err(MISALIGNED);
429 }
430 Ok(Sweep { check, base, offset: chrec.base.offset, step, reach })
431}
432
433/// Makes the two halves and the block that chooses between them.
434///
435/// The order matters in two places. The copy is made before anything is rewired, so the copy's back
436/// edge is remapped to the copy's own header rather than to a guard that did not exist yet. The
437/// checks come out of the fast half last, so the copy still has them.
438fn apply(func: &mut Func, plan: &Plan) {
439 // The slow half, which is the loop as it stands, under a substitution that renames everything it
440 // defines. Nothing is seeded, so its header gets parameters of its own, which is what a copy
441 // reached from a block that also reaches the original needs.
442 let mut renamed: HashMap<Value, Value> = HashMap::new();
443 let copies = copy::blocks(func, &plan.body, &mut renamed);
444 let slow = copies[&plan.header];
445
446 // The guard, which takes over the header's place: the preheader arrives here, the back edge
447 // comes back to here, and the header is reached from here and nowhere else. Its first parameter
448 // is a counter of its own, because the loop's counter is not something this pass has to find and
449 // a loop with several ways out may not have one.
450 let word = Type::int(64);
451 let types: Vec<Type> = func[plan.header].params.iter().map(|¶m| func[param].ty).collect();
452 let guard = func.create_block();
453 let round = func.append_param(guard, word);
454 let carried: Vec<Value> = types.iter().map(|&ty| func.append_param(guard, ty)).collect();
455
456 let (limit, start) = limited(func, plan);
457
458 let mut build = Builder::new(func, guard);
459 let inside = build.icmp(IntPred::Slt, round, limit);
460 build.br_if(inside, plan.header, &carried, slow, &carried);
461
462 // The way in, which now hands the guard a count of no iterations so far.
463 let term = func.terminator(plan.preheader).expect("a preheader ends in a jump to the header");
464 route(func, term, plan.header, guard, start);
465
466 // The way round, which counts one more. The counter is the guard's parameter and the guard
467 // dominates every block in the fast half, so the latch may read it.
468 let term = func.terminator(plan.latch).expect("a latch ends in a branch back to the header");
469 let mut build = Builder::new(func, plan.latch);
470 let one = build.iconst(word, 1);
471 let next = build.binary(Opcode::Add, round, one, Flags::NSW);
472 for value in [one, next] {
473 let inst = inst_of(func, value);
474 func.remove_inst(inst);
475 func.insert_before(inst, term);
476 }
477 route(func, term, plan.header, guard, next);
478
479 // The checks the fast half does not need. The `cap_of` each one was reading is left where it is,
480 // for `dce` after this pass to take away, which is the arrangement `crate::hoist` and
481 // `crate::discharge` are both in.
482 for sweep in &plan.sweeps {
483 func.remove_inst(sweep.check);
484 }
485}
486
487/// Sends every edge this terminator has to `from` to `to` instead, with one more argument in front.
488fn route(func: &mut Func, term: Inst, from: Block, to: Block, first: Value) {
489 for at in func.target_list(term).iter() {
490 let call = func[at];
491 if call.block != from {
492 continue;
493 }
494 let mut args = vec![first];
495 args.extend_from_slice(&func[call.args]);
496 let args = func.push_values(&args);
497 func.set_block_call(at, BlockCall { block: to, args });
498 }
499}
500
501/// Builds the number of iterations the fast half may run, and the zero the preheader starts it at.
502///
503/// One `cap_extent` per check and the smallest of what they allow, all of it in the preheader in
504/// front of the jump into the loop. A builder appends to the end of a block, which in a block that
505/// already has its terminator is after it, so everything is built first and then moved in front of
506/// the terminator in the order it was built.
507fn limited(func: &mut Func, plan: &Plan) -> (Value, Value) {
508 let term = func.terminator(plan.preheader).expect("a preheader ends in a jump to the header");
509 let mut made = Vec::new();
510 let mut build = Builder::new(func, plan.preheader);
511 let start = build.iconst(Type::int(64), 0);
512 made.push(start);
513
514 let mut limit: Option<Value> = None;
515 for sweep in &plan.sweeps {
516 let allows = reachable(&mut build, &mut made, sweep, plan.around);
517 limit = Some(match limit {
518 None => allows,
519 Some(so_far) => {
520 let smaller = build.icmp(IntPred::Slt, allows, so_far);
521 made.push(smaller);
522 let least = build.select(smaller, allows, so_far);
523 made.push(least);
524 least
525 }
526 });
527 }
528 let limit = limit.expect("a plan holds at least one check");
529
530 for value in made {
531 let inst = inst_of(func, value);
532 func.remove_inst(inst);
533 func.insert_before(inst, term);
534 }
535 (limit, start)
536}
537
538/// How many iterations one check allows, which is `(extent - reach) / step + 1` and never negative.
539///
540/// The subtraction and the addition carry `nsw` and the division does not need it. Everything here
541/// is worked out from the extent, which the runtime answers with a count of bytes it walked and so
542/// is never negative and never larger than the object, so none of this can leave sixty four bits
543/// whatever the limit it was asked for turned out to be.
544fn reachable(
545 build: &mut Builder<'_>,
546 made: &mut Vec<Value>,
547 sweep: &Sweep,
548 around: Around,
549) -> Value {
550 let word = Type::int(64);
551 let first = if sweep.offset == 0 {
552 sweep.base
553 } else {
554 let by = build.iconst(word, sweep.offset);
555 made.push(by);
556 let args = build.func().push_values(&[sweep.base, by]);
557 let sum = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
558 made.push(sum);
559 sum
560 };
561 // How many bytes the loop was going to read, which is how far the runtime is asked to look and
562 // nothing more. An answer short of the truth costs iterations in the slow half and is never
563 // wrong, so a count that saturates rather than one that refuses is the right thing here.
564 let want = match around {
565 Around::Number(times) => {
566 let far = times.saturating_mul(sweep.step).saturating_add(sweep.reach);
567 let far = i64::try_from(far).unwrap_or(i64::MAX);
568 let bytes = build.iconst(word, i128::from(far));
569 made.push(bytes);
570 bytes
571 }
572 Around::Computed(count, reading) => {
573 covered(build, made, count, sweep.step, sweep.reach, reading, Flags::NONE)
574 }
575 };
576
577 let args = build.func().push_values(&[first]);
578 let capability = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
579 made.push(capability);
580 let args = build.func().push_values(&[capability, first, want]);
581 let extent = build.value(InstData { args, ..InstData::new(Opcode::CapExtent) }, word);
582 made.push(extent);
583
584 let reach = build.iconst(word, sweep.reach);
585 made.push(reach);
586 let left = build.binary(Opcode::Sub, extent, reach, Flags::NSW);
587 made.push(left);
588 let zero = build.iconst(word, 0);
589 made.push(zero);
590 // Not even one access fits, which is a dangling pointer or an object smaller than the thing being
591 // read out of it. The fast half runs no iterations and the check in the slow half reports it, at
592 // the access rather than at the loop.
593 let short = build.icmp(IntPred::Slt, left, zero);
594 made.push(short);
595
596 let mut steps = left;
597 if sweep.step != 1 {
598 let by = build.iconst(word, sweep.step);
599 made.push(by);
600 steps = build.binary(Opcode::SDiv, left, by, Flags::NONE);
601 made.push(steps);
602 }
603 let one = build.iconst(word, 1);
604 made.push(one);
605 let allows = build.binary(Opcode::Add, steps, one, Flags::NSW);
606 made.push(allows);
607 let clamped = build.select(short, zero, allows);
608 made.push(clamped);
609 clamped
610}
611
612#[cfg(test)]
613mod tests {
614 use rucc_base::Interner;
615 use rucc_ir::{
616 Block, Builder, Extra, Flags, Func, Inst, InstData, IntPred, MemInfo, MemOrder, Module,
617 Opcode, Restrict, Signature, Type, Value, verify_func,
618 };
619 use rucc_target::{TargetInfo, Triple};
620
621 use super::{SPLIT, Split};
622 use crate::canon::Canon;
623 use crate::stats::Kind;
624 use crate::{Fuel, Pass, Stats};
625
626 /// How many times the loop goes round, and how wide each element of the walk is.
627 const TRIPS: i128 = 16;
628 const WIDTH: i128 = 4;
629
630 /// A counted loop that reads one element each time round and can stop on what it read.
631 ///
632 /// ```text
633 /// entry(a): jump head(0)
634 /// head(i): p = a + i*4; check_bounds cap_of(p), p; v = load p
635 /// br v == 0 -> done, more
636 /// more: next = i + 1; br next < 16 -> head(next), done
637 /// done: ret
638 /// ```
639 ///
640 /// The second way out is the point. Hoisting refuses this loop, because a loop that can stop in
641 /// the middle reads fewer bytes than its count says and one check in front of it for all of them
642 /// would refuse a program that was right. Splitting does not care, because the count it reads is
643 /// only ever an upper limit on how far to look.
644 fn leaving() -> (Interner, Func, Vec<Block>) {
645 walking(Some(TRIPS))
646 }
647
648 /// The same loop, with how many times it goes round handed in rather than written down.
649 ///
650 /// What this reaches is the other half of [`crate::trip::covered`], the one that builds the
651 /// count out of something the loop does not change. It is worth its own test because that
652 /// arithmetic promises not to wrap for hoisting and promises nothing for this pass, and the two
653 /// callers now ask for different things from the same code.
654 fn counting() -> (Interner, Func, Vec<Block>) {
655 walking(None)
656 }
657
658 /// Builds the loop, with the exit test against a number or against a second parameter.
659 fn walking(times: Option<i128>) -> (Interner, Func, Vec<Block>) {
660 let mut names = Interner::new();
661 let mut params = vec![Type::PTR];
662 params.extend(times.is_none().then_some(Type::int(64)));
663 let mut func = Func::new(names.intern("f"), Signature::new().with_params(¶ms));
664 let entry = func.create_block();
665 let head = func.create_block();
666 let more = func.create_block();
667 let done = func.create_block();
668 let array = func.append_param(entry, Type::PTR);
669 let handed = times.is_none().then(|| func.append_param(entry, Type::int(64)));
670 let counter = func.append_param(head, Type::int(64));
671
672 let zero = Builder::new(&mut func, entry).iconst(Type::int(64), 0);
673 Builder::new(&mut func, entry).jump(head, &[zero]);
674
675 let mut build = Builder::new(&mut func, head);
676 let by = build.iconst(Type::int(64), WIDTH);
677 let scaled = build.binary(Opcode::Mul, counter, by, Flags::NSW);
678 let args = build.func().push_values(&[array, scaled]);
679 let pointer = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
680 check(&mut build, pointer);
681 let read = build.load(Type::int(32), pointer, mem(), Flags::NONE);
682 let nothing = build.iconst(Type::int(32), 0);
683 let stop = build.icmp(IntPred::Eq, read, nothing);
684 build.br_if(stop, done, &[], more, &[]);
685
686 let mut build = Builder::new(&mut func, more);
687 let one = build.iconst(Type::int(64), 1);
688 let next = build.binary(Opcode::Add, counter, one, Flags::NSW);
689 let limit = match (times, handed) {
690 (Some(times), _) => build.iconst(Type::int(64), times),
691 (None, handed) => handed.expect("a loop with no number for a limit was handed one"),
692 };
693 let again = build.icmp(IntPred::Slt, next, limit);
694 build.br_if(again, head, &[next], done, &[]);
695 Builder::new(&mut func, done).ret(&[]);
696 (names, func, vec![entry, head, more, done])
697 }
698
699 /// What one access in the loop covers.
700 fn mem() -> MemInfo {
701 MemInfo {
702 size: WIDTH as u64,
703 align: WIDTH as u32,
704 order: MemOrder::NotAtomic,
705 tbaa: None,
706 restrict: Restrict::NONE,
707 }
708 }
709
710 /// Puts `cap_of` and a `check_bounds` at `pointer` into a block.
711 ///
712 /// The shape `rucc-safety` emits, written out here rather than reached for, because `rucc-opt`
713 /// is rank 9 alongside `rucc-safety` and cannot depend on it.
714 fn check(build: &mut Builder<'_>, pointer: Value) {
715 let args = build.func().push_values(&[pointer]);
716 let capability = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
717 let args = build.func().push_values(&[capability, pointer]);
718 let extra = Extra::Mem(build.func().add_mem(mem()));
719 build.inst(InstData { args, extra, ..InstData::new(Opcode::CheckBounds) }, &[]);
720 }
721
722 /// Canonicalizes and then splits, with as much fuel as both want.
723 ///
724 /// Both, because the pass is written against the shape [`Canon`] leaves, and it is
725 /// canonicalization that gives the loop the preheader the limit is worked out in.
726 fn split_up(func: &mut Func) -> Stats {
727 let mut an = crate::machine::fixtures::analyses();
728 Canon.run(func, &mut an, &mut Fuel::unlimited());
729 Split.run(func, &mut an, &mut Fuel::unlimited())
730 }
731
732 /// Every instruction in the function with this opcode, and the block it is in.
733 fn all(func: &Func, opcode: Opcode) -> Vec<(Block, Inst)> {
734 func.blocks()
735 .flat_map(|block| func.insts(block).map(move |inst| (block, inst)).collect::<Vec<_>>())
736 .filter(|&(_, inst)| func[inst].opcode == opcode)
737 .collect()
738 }
739
740 /// Insists the function is one the rest of the compiler may believe.
741 ///
742 /// This is what the tests here rest on. The pass makes a second copy of a loop, gives a new
743 /// block parameters that stand for the old header's, and moves a preheader's worth of
744 /// arithmetic in front of a terminator that was already there, so whether every value is in
745 /// scope where it is read is not something reading the code settles.
746 fn sound(func: &Func, names: &mut Interner) {
747 let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
748 let module = Module::new(names.intern("t.c"), &target);
749 if let Err(errors) = verify_func(&module, func, names) {
750 panic!("{errors:#?}");
751 }
752 }
753
754 #[test]
755 fn a_loop_that_can_stop_early_is_split_even_though_hoisting_will_not_touch_it() {
756 // The census row this pass was written for. Of the checks SQLite still carries at -O2, the
757 // largest group by far is in loops with a second way out, which is exactly the loop here.
758 let (mut names, mut func, _) = leaving();
759 let mut an = crate::machine::fixtures::analyses();
760 Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
761 let refused = crate::hoist::Hoist.run(&mut func, &mut an, &mut Fuel::unlimited());
762 assert!(!refused.changed(), "hoisting has nothing to say about this loop");
763
764 let stats = Split.run(&mut func, &mut an, &mut Fuel::unlimited());
765 assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
766 sound(&func, &mut names);
767 }
768
769 #[test]
770 fn the_half_the_loop_runs_first_has_no_check_in_it_and_the_other_one_keeps_it() {
771 // One check went in and one check came out, and the one that came out is in the copy. That
772 // is the whole transformation: the same work, with the checking half reached only once the
773 // guard says the run of safe iterations is over.
774 let (mut names, mut func, blocks) = leaving();
775 let head = blocks[1];
776 split_up(&mut func);
777
778 let left = all(&func, Opcode::CheckBounds);
779 assert_eq!(left.len(), 1, "one check, and it is the one the slow half kept");
780 assert_ne!(left[0].0, head, "and it is not in the block the loop started in");
781 sound(&func, &mut names);
782 }
783
784 #[test]
785 fn how_far_the_runtime_is_asked_to_look_is_settled_in_front_of_the_loop() {
786 // The one thing a compiler cannot work out here is how many bytes belong to the object, so
787 // it is asked, once, before the loop starts. Once is what makes this worth doing: a query
788 // per loop in place of a check per iteration.
789 let (mut names, mut func, _) = leaving();
790 split_up(&mut func);
791
792 let asked = all(&func, Opcode::CapExtent);
793 assert_eq!(asked.len(), 1, "one question for the one check that was sized");
794 let cfg = crate::Cfg::new(&func);
795 let doms = crate::Dominators::new(&cfg);
796 let loops = crate::Loops::new(&cfg, &doms);
797 assert!(
798 loops.all().all(|id| !loops.contains(id, asked[0].0)),
799 "and it is outside the loop"
800 );
801 sound(&func, &mut names);
802 }
803
804 #[test]
805 fn a_loop_with_a_call_in_it_is_left_alone() {
806 // The extent is asked once and believed for the whole of the fast half, so anything that
807 // could hand the storage back in the middle makes the answer stale and the fast half has
808 // nothing left in it to notice.
809 let (mut names, mut func, blocks) = leaving();
810 let more = blocks[2];
811 let term = func.terminator(more).expect("the latch branches");
812 let callee = names.intern("might_free");
813 let signature = func.add_signature(Signature::new());
814 let call = Builder::new(&mut func, more).call(callee, signature, &[]);
815 func.remove_inst(call);
816 func.insert_before(call, term);
817
818 let stats = split_up(&mut func);
819 assert!(!stats.changed());
820 assert_eq!(stats.count(Kind::Missed, super::A_CALL_INSIDE), 1);
821 }
822
823 #[test]
824 fn a_check_whose_address_does_not_move_is_left_to_hoisting() {
825 // One check on the array itself, every time round. Nothing about it gets better from being
826 // in the fast half, because it is the same bytes every iteration, which is a check to lift
827 // out of the loop rather than a loop to divide in two.
828 let (_, mut func, blocks) = leaving();
829 let (entry, head) = (blocks[0], blocks[1]);
830 let array = func[entry].params[0];
831 let term = func.terminator(head).expect("the header branches");
832 let mut build = Builder::new(&mut func, head);
833 check(&mut build, array);
834 let made: Vec<Inst> = func.insts(head).skip_while(|&inst| inst != term).skip(1).collect();
835 for inst in made {
836 func.remove_inst(inst);
837 func.insert_before(inst, term);
838 }
839
840 let stats = split_up(&mut func);
841 assert_eq!(stats.count(Kind::Missed, super::DOES_NOT_MOVE), 1);
842 assert_eq!(
843 all(&func, Opcode::CheckBounds).len(),
844 3,
845 "the walking check left the fast half, and the still one stayed in both"
846 );
847 }
848
849 #[test]
850 fn a_loop_whose_count_is_an_expression_is_split_on_what_that_expression_says() {
851 // How far to look is worked out in the preheader rather than written down, out of a value
852 // the loop does not change. Nothing here promises the arithmetic stays inside sixty four
853 // bits, and it does not have to: a limit that wrapped is still answered with a true count
854 // of the bytes that belong to the object.
855 let (mut names, mut func, _) = counting();
856 let stats = split_up(&mut func);
857 assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
858 assert_eq!(all(&func, Opcode::CapExtent).len(), 1);
859 assert_eq!(all(&func, Opcode::CheckBounds).len(), 1, "the fast half lost its check");
860 sound(&func, &mut names);
861 }
862
863 #[test]
864 fn the_pass_stops_when_the_fuel_runs_out() {
865 // What `-fopt-fuel` is for, and the reason every transformation here goes through the
866 // counter rather than round it.
867 let (_, mut func, _) = leaving();
868 let mut an = crate::machine::fixtures::analyses();
869 Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
870 let stats = Split.run(&mut func, &mut an, &mut Fuel::of(0));
871 assert!(!stats.changed());
872 assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
873 }
874}