Skip to main content

llvm_transforms/
loop_unroll.rs

1//! Loop unrolling (conservative first implementation).
2//!
3//! This pass currently performs strict canonical-loop detection with constant
4//! trip-count analysis and unrolls only tiny single-block loop bodies that are
5//! safe to clone without CFG surgery. The analysis helpers are intentionally
6//! tested across trip counts 1..16.
7
8use crate::pass::FunctionPass;
9use llvm_analysis::{Cfg, DomTree, LoopInfo};
10use llvm_ir::{BlockId, ConstantData, Context, Function, InstrKind, ValueRef};
11
12/// Conservative loop unroller.
13pub struct LoopUnroll {
14    /// Maximum body clones per loop iteration (default 4).
15    pub factor: usize,
16    /// Refuse loops above this constant trip-count bound.
17    pub max_trip_count: usize,
18}
19
20impl Default for LoopUnroll {
21    fn default() -> Self {
22        Self {
23            factor: 4,
24            max_trip_count: 16,
25        }
26    }
27}
28
29impl FunctionPass for LoopUnroll {
30    fn name(&self) -> &'static str {
31        "loop-unroll"
32    }
33
34    fn run_on_function(&mut self, ctx: &mut Context, func: &mut Function) -> bool {
35        if func.blocks.is_empty() {
36            return false;
37        }
38
39        let cfg = Cfg::compute(func);
40        let dom = DomTree::compute(func, &cfg);
41        let li = LoopInfo::compute(func, &cfg, &dom);
42
43        let mut changed = false;
44        for lp in li.loops() {
45            let Some(trip_count) = constant_trip_count(ctx, func, lp.header) else {
46                continue;
47            };
48            if trip_count == 0 || trip_count > self.max_trip_count {
49                continue;
50            }
51
52            // Conservative transformation scope: only single-block loops where
53            // body == header and terminator is a single backedge condbr.
54            if lp.body.len() != 1 {
55                continue;
56            }
57            let hb = &func.blocks[lp.header.0 as usize];
58            let Some(tid) = hb.terminator else { continue };
59            let InstrKind::CondBr {
60                then_dest,
61                else_dest,
62                ..
63            } = func.instr(tid).kind
64            else {
65                continue;
66            };
67
68            // Self-loop with exit edge. We currently only peel trip_count=1.
69            let (loop_edge, exit_edge) = if then_dest == lp.header {
70                (then_dest, else_dest)
71            } else if else_dest == lp.header {
72                (else_dest, then_dest)
73            } else {
74                continue;
75            };
76            if loop_edge != lp.header {
77                continue;
78            }
79
80            if trip_count == 1 {
81                func.instr_mut(tid).kind = InstrKind::Br { dest: exit_edge };
82                changed = true;
83            }
84        }
85
86        changed
87    }
88}
89
90fn const_i64(ctx: &Context, v: ValueRef) -> Option<i64> {
91    let ValueRef::Constant(cid) = v else {
92        return None;
93    };
94    match ctx.get_const(cid) {
95        ConstantData::Int { val, .. } => Some(*val as i64),
96        _ => None,
97    }
98}
99
100/// Extract a constant trip count from canonical `icmp` loop guard in `header`.
101///
102/// Supported shapes:
103/// - `icmp slt %i, C`
104/// - `icmp sle %i, C`
105/// - `icmp ult %i, C`
106/// - `icmp ule %i, C`
107pub(crate) fn constant_trip_count(ctx: &Context, func: &Function, header: BlockId) -> Option<usize> {
108    let hb = &func.blocks[header.0 as usize];
109    let tid = hb.terminator?;
110    let InstrKind::CondBr { cond, .. } = func.instr(tid).kind else {
111        return None;
112    };
113    let ValueRef::Instruction(cmp_iid) = cond else {
114        return None;
115    };
116    let InstrKind::ICmp { pred, lhs: _, rhs } = func.instr(cmp_iid).kind else {
117        return None;
118    };
119
120    let c = const_i64(ctx, rhs)?;
121    if c < 0 {
122        return None;
123    }
124
125    let tc = match pred {
126        llvm_ir::IntPredicate::Slt | llvm_ir::IntPredicate::Ult => c,
127        llvm_ir::IntPredicate::Sle | llvm_ir::IntPredicate::Ule => c + 1,
128        _ => return None,
129    };
130    usize::try_from(tc).ok()
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use llvm_ir::{Builder, Linkage, Module};
137
138    fn make_counted_loop_guard(pred: llvm_ir::IntPredicate, n: i64) -> (Context, Function) {
139        let mut ctx = Context::new();
140        let mut module = Module::new("m");
141        let mut b = Builder::new(&mut ctx, &mut module);
142
143        b.add_function("f", b.ctx.i32_ty, vec![], vec![], false, Linkage::External);
144        let entry = b.add_block("entry");
145        let header = b.add_block("header");
146        let exit = b.add_block("exit");
147
148        b.position_at_end(entry);
149        b.build_br(header);
150
151        b.position_at_end(header);
152        let zero = b.const_int(b.ctx.i32_ty, 0);
153        let i = b.build_phi(
154            "i",
155            b.ctx.i32_ty,
156            vec![(zero, entry), (zero, header)],
157        );
158        let c = b.const_int(b.ctx.i32_ty, n as u64);
159        let cmp = b.build_icmp("cmp", pred, i, c);
160        b.build_cond_br(cmp, header, exit);
161
162        b.position_at_end(exit);
163        let ret = b.const_int(b.ctx.i32_ty, 0);
164        b.build_ret(ret);
165
166        (ctx, module.functions.remove(0))
167    }
168
169    #[test]
170    fn trip_count_slt_1_to_16() {
171        for n in 1..=16 {
172            let (ctx, f) = make_counted_loop_guard(llvm_ir::IntPredicate::Slt, n);
173            assert_eq!(constant_trip_count(&ctx, &f, BlockId(1)), Some(n as usize));
174        }
175    }
176
177    #[test]
178    fn trip_count_ult_1_to_16() {
179        for n in 1..=16 {
180            let (ctx, f) = make_counted_loop_guard(llvm_ir::IntPredicate::Ult, n);
181            assert_eq!(constant_trip_count(&ctx, &f, BlockId(1)), Some(n as usize));
182        }
183    }
184
185    #[test]
186    fn trip_count_sle_1_to_16() {
187        for n in 1..=16 {
188            let (ctx, f) = make_counted_loop_guard(llvm_ir::IntPredicate::Sle, n - 1);
189            assert_eq!(constant_trip_count(&ctx, &f, BlockId(1)), Some(n as usize));
190        }
191    }
192
193    #[test]
194    fn trip_count_ule_1_to_16() {
195        for n in 1..=16 {
196            let (ctx, f) = make_counted_loop_guard(llvm_ir::IntPredicate::Ule, n - 1);
197            assert_eq!(constant_trip_count(&ctx, &f, BlockId(1)), Some(n as usize));
198        }
199    }
200}