//! LICM hoists loads of an uncaptured noalias param's dereferenceable memory (`&mut self` //! fields) out of a loop that does not write that param's memory (rustc's `noalias`, the roots //! local_cse uses), and leaves them when the loop stores through the param, hands it to a call, //! stores through a pointer of unknown root, when the param is captured, withheld, or noalias is //! turned off. use relax_backend::passes; use relax_backend::rir::*; const U64: IntTy = IntTy { bits: 64, signed: false }; #[derive(Clone, Copy, PartialEq)] enum Body { /// store through another pointer parameter OtherStore, /// store through the noalias param itself ParamStore, /// store through a pointer derived from the param by address arithmetic DerivedStore, /// call handed the param CallWithParam, /// call handed only the other pointer CallWithoutParam, /// store through a pointer that is redefined (unknown root) UnknownStore, } fn push(f: &mut Func, b: u32, i: Inst) { f.blocks[b as usize].insts.push(i); f.blocks[b as usize].pos.push(0); } /// `fn(p: &mut S (16 bytes, noalias), q: *mut u64, n: u64) -> u64`: /// `acc = 0; i = 0; while i < n { x = p.f8; ; acc += x; i += 1 } acc`. /// Returns the function and the load's destination. fn build(body: Body, capture: bool) -> (Func, VReg) { let mut f = Func::new("licm_noalias".into(), 0); let (p, q, n) = (f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I)); f.params = vec![p, q, n]; f.rets = vec![Cls::I]; f.noalias = vec![p]; f.deref = vec![(p, 16)]; let b: Vec = (0..4).map(|_| f.block()).collect(); let (i, acc, c, x, one) = (f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I)); push(&mut f, b[0], Inst::Iconst(i, 0)); push(&mut f, b[0], Inst::Iconst(acc, 0)); f.blocks[b[0] as usize].term = Term::Jump(b[1]); push(&mut f, b[1], Inst::ICmp(Cond::Lt, false, c, i, n)); f.blocks[b[1] as usize].term = Term::Branch(c, b[2], b[3]); push(&mut f, b[2], Inst::Load(Mem::Int(8, false), x, p, 8)); match body { Body::OtherStore => push(&mut f, b[2], Inst::Store(Mem::Int(8, false), q, 0, i)), Body::ParamStore => push(&mut f, b[2], Inst::Store(Mem::Int(8, false), p, 0, i)), Body::DerivedStore => { let d = f.vreg(Cls::I); push(&mut f, b[2], Inst::IBinI(IOp::Add, U64, d, p, 8)); push(&mut f, b[2], Inst::Store(Mem::Int(8, false), d, 0, i)); } Body::CallWithParam => push(&mut f, b[2], Inst::Call(Callee::Fn(0), vec![p, q], vec![])), Body::CallWithoutParam => push(&mut f, b[2], Inst::Call(Callee::Fn(0), vec![q], vec![])), Body::UnknownStore => { // `r` has two defs (q, then q + 8): no single root let r = f.vreg(Cls::I); push(&mut f, b[0], Inst::Mov(r, q)); push(&mut f, b[2], Inst::IBinI(IOp::Add, U64, r, r, 8)); push(&mut f, b[2], Inst::Store(Mem::Int(8, false), r, 0, i)); } } if capture { // the param's address leaves the function through memory push(&mut f, b[0], Inst::Store(Mem::Int(8, false), q, 0, p)); } push(&mut f, b[2], Inst::IBin(IOp::Add, U64, acc, acc, x)); push(&mut f, b[2], Inst::Iconst(one, 1)); push(&mut f, b[2], Inst::IBin(IOp::Add, U64, i, i, one)); f.blocks[b[2] as usize].term = Term::Jump(b[1]); f.blocks[b[3] as usize].term = Term::Ret(vec![acc]); f.verify().unwrap(); (f, x) } /// The block holding the instruction that defines `x`. fn load_block(f: &Func, x: VReg) -> usize { for (bi, b) in f.blocks.iter().enumerate() { for i in &b.insts { if let Inst::Load(_, d, ..) = i { if *d == x { return bi; } } } } panic!("load vanished"); } fn run(f: &mut Func, options: &relax_session::Options) -> usize { let x = f.blocks.iter().flat_map(|b| &b.insts).find_map(|i| if let Inst::Load(_, d, ..) = i { Some(*d) } else { None }).unwrap(); passes::licm(options, f); f.verify().unwrap(); load_block(f, x) } #[test] fn load_of_param_field_is_hoisted_past_a_store_through_another_pointer() { let (mut f, _) = build(Body::OtherStore, false); assert_eq!(run(&mut f, &relax_session::Options::default()), 0, "the load stays in the loop"); } #[test] fn load_is_hoisted_past_a_call_not_handed_the_param() { let (mut f, _) = build(Body::CallWithoutParam, false); assert_eq!(run(&mut f, &relax_session::Options::default()), 0); } #[test] fn load_stays_when_the_loop_stores_through_the_param() { for body in [Body::ParamStore, Body::DerivedStore] { let (mut f, _) = build(body, false); assert_eq!(run(&mut f, &relax_session::Options::default()), 2); } } #[test] fn load_stays_when_the_loop_passes_the_param_to_a_call() { let (mut f, _) = build(Body::CallWithParam, false); assert_eq!(run(&mut f, &relax_session::Options::default()), 2); } #[test] fn load_stays_when_a_store_has_no_known_root() { let (mut f, _) = build(Body::UnknownStore, false); assert_eq!(run(&mut f, &relax_session::Options::default()), 2); } #[test] fn load_stays_when_the_param_is_captured() { let (mut f, _) = build(Body::OtherStore, true); assert_eq!(run(&mut f, &relax_session::Options::default()), 2); } #[test] fn load_stays_without_dereferenceable() { let (mut f, _) = build(Body::OtherStore, false); f.deref.clear(); assert_eq!(run(&mut f, &relax_session::Options::default()), 2); // and beyond the dereferenceable size let (mut f, _) = build(Body::OtherStore, false); f.deref = vec![(f.params[0], 12)]; assert_eq!(run(&mut f, &relax_session::Options::default()), 2); } #[test] fn withheld_noalias_hoists_nothing() { // code that runs before its borrow-check verdict: noalias is withheld let (mut f, _) = build(Body::OtherStore, false); f.withheld_noalias = std::mem::take(&mut f.noalias); assert_eq!(run(&mut f, &relax_session::Options::default()), 2); } #[test] fn no_noalias_option_hoists_nothing() { let (mut f, _) = build(Body::OtherStore, false); let mut o = relax_session::Options::default(); o.no_noalias = true; assert_eq!(run(&mut f, &o), 2); } /// A call before the loop handed the param may keep it; the loop's store through `q` (another /// pointer, which could be the kept one) may then write the param's memory. #[test] fn load_stays_when_a_call_before_the_loop_is_handed_the_param() { let (mut f, _) = build(Body::OtherStore, false); let at = f.blocks[0].insts.len(); let p = p_of(&f); f.blocks[0].insts.insert(at, Inst::Call(Callee::Fn(0), vec![p], vec![])); f.blocks[0].pos.insert(at, 0); assert_eq!(run(&mut f, &relax_session::Options::default()), 2); } fn p_of(f: &Func) -> VReg { f.params[0] } /// The same call when the callee provably keeps no pointer it is handed there (O3's /// param_nocapture oracle, LLVM's `nocapture`): nothing can reach the param's memory through `q`, /// so the load is hoisted; a callee that may keep it (oracle false, or another argument /// position) still blocks it. #[test] fn load_is_hoisted_past_a_call_before_the_loop_that_keeps_nothing() { for (kept_by_callee, hoisted) in [(false, true), (true, false)] { let (mut f, _) = build(Body::OtherStore, false); let at = f.blocks[0].insts.len(); let p = p_of(&f); f.blocks[0].insts.insert(at, Inst::Call(Callee::Fn(7), vec![p], vec![])); f.blocks[0].pos.insert(at, 0); passes::set_sroa_nocapture(Some(Box::new(move |c: &Callee, k: usize| matches!(c, Callee::Fn(7)) && k == 0 && !kept_by_callee))); let b = run(&mut f, &relax_session::Options::default()); passes::set_sroa_nocapture(None); assert_eq!(b == 0, hoisted, "kept_by_callee {kept_by_callee}: load in b{b}"); } }