makepad/libs/relax/codegen/tests/volatile.rs

173 lines
7.1 KiB
Rust

//! Volatile accesses (AtomicLoad / AtomicStore with AtomOrd::Volatile) through every optimiser pass
//! family: never removed (DCE, dead stores), merged (CSE), forwarded (slot store/load/copy forwarding,
//! SROA), hoisted or speculated (LICM, if-conversion, layout) or reordered with each other.
use relax_backend::passes;
use relax_codegen::opt;
use relax_backend::rir::*;
const U64: IntTy = IntTy { bits: 64, signed: false };
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);
}
/// Every pass the JIT runs, in pipeline order, twice over (a later pass can undo an earlier one's care).
fn all_passes(f: &mut Func) {
for _ in 0..2 {
passes::simplify(&relax_session::Options::default(), f, None, true, true);
opt::forward_slot_copies(f);
opt::forward_slot_dests(f);
opt::forward_slot_loads(f);
opt::fold_small_sets(f);
opt::stores_for_copies(f);
passes::sroa(f);
passes::simplify(&relax_session::Options::default(), f, None, true, true);
passes::licm(&relax_session::Options::default(), f);
passes::live_dce(f);
passes::sink_consts(f);
passes::relayout(f);
}
passes::addr_modes(f);
passes::rotate_loops(f);
f.verify().unwrap();
}
fn volatile_ops(f: &Func) -> Vec<(usize, Inst)> {
let mut v = Vec::new();
for (bi, b) in f.blocks.iter().enumerate() {
for i in &b.insts {
if matches!(i, Inst::AtomicLoad(_, AtomOrd::Volatile, ..) | Inst::AtomicStore(_, AtomOrd::Volatile, ..)) {
v.push((bi, i.clone()));
}
}
}
v
}
fn func(name: &str, nparams: usize) -> Func {
let mut f = Func::new(name.to_string(), 0);
for _ in 0..nparams {
let p = f.vreg(Cls::I);
f.params.push(p);
}
f
}
#[test]
fn dce_keeps_unused_volatile_load() {
let mut f = func("dce", 1);
let p = f.params[0];
let b = f.block();
let d = f.vreg(Cls::I);
push(&mut f, b, Inst::AtomicLoad(Mem::Int(4, false), AtomOrd::Volatile, d, p));
f.blocks[b as usize].term = Term::Ret(vec![]);
all_passes(&mut f);
assert_eq!(volatile_ops(&f).len(), 1, "{}", f.dump());
}
#[test]
fn cse_keeps_both_volatile_loads_in_order() {
let mut f = func("cse", 1);
f.rets = vec![Cls::I];
let p = f.params[0];
let b = f.block();
let (d1, d2, s) = (f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I));
push(&mut f, b, Inst::AtomicLoad(Mem::Int(8, false), AtomOrd::Volatile, d1, p));
push(&mut f, b, Inst::AtomicLoad(Mem::Int(8, false), AtomOrd::Volatile, d2, p));
push(&mut f, b, Inst::IBin(IOp::Sub, U64, s, d1, d2));
f.blocks[b as usize].term = Term::Ret(vec![s]);
all_passes(&mut f);
let v = volatile_ops(&f);
assert_eq!(v.len(), 2, "{}", f.dump());
// the first defines the subtraction's left operand: same order as written
let sub = f.blocks.iter().flat_map(|b| b.insts.iter()).find_map(|i| if let Inst::IBin(IOp::Sub, _, _, a, b2) = i { Some((*a, *b2)) } else { None });
if let (Inst::AtomicLoad(_, _, x, _), Inst::AtomicLoad(_, _, y, _), Some((a, b2))) = (&v[0].1, &v[1].1, sub) {
assert_eq!((*x, *y), (a, b2), "{}", f.dump());
}
}
#[test]
fn slot_forwarding_and_sroa_keep_volatile_accesses() {
// a slot written plainly then read volatile, written volatile then read plainly, and a
// volatile store nobody reads: every volatile access stays, none becomes a register value
let mut f = func("slots", 0);
f.rets = vec![Cls::I];
let s = f.slot(8, 8);
let b = f.block();
let (a, c5, c9, d1, d2, sum) = (f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I));
push(&mut f, b, Inst::SlotAddr(a, s));
push(&mut f, b, Inst::Iconst(c5, 5));
push(&mut f, b, Inst::Store(Mem::Int(8, false), a, 0, c5));
push(&mut f, b, Inst::AtomicLoad(Mem::Int(8, false), AtomOrd::Volatile, d1, a));
push(&mut f, b, Inst::Iconst(c9, 9));
push(&mut f, b, Inst::AtomicStore(Mem::Int(8, false), AtomOrd::Volatile, a, c9));
push(&mut f, b, Inst::Load(Mem::Int(8, false), d2, a, 0));
push(&mut f, b, Inst::IBin(IOp::Add, U64, sum, d1, d2));
push(&mut f, b, Inst::AtomicStore(Mem::Int(8, false), AtomOrd::Volatile, a, c5));
f.blocks[b as usize].term = Term::Ret(vec![sum]);
all_passes(&mut f);
let v = volatile_ops(&f);
assert_eq!(v.len(), 3, "{}", f.dump());
assert!(matches!(v[0].1, Inst::AtomicLoad(..)) && matches!(v[1].1, Inst::AtomicStore(..)) && matches!(v[2].1, Inst::AtomicStore(..)), "{}", f.dump());
}
#[test]
fn licm_keeps_volatile_load_in_loop() {
// loop: i = 0; do { x += *p (volatile); i += 1 } while i < 10
let mut f = func("licm", 1);
f.rets = vec![Cls::I];
let p = f.params[0];
let (b0, b1, b2) = (f.block(), f.block(), f.block());
let (i, x, d, c) = (f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I));
push(&mut f, b0, Inst::Iconst(i, 0));
push(&mut f, b0, Inst::Iconst(x, 0));
f.blocks[b0 as usize].term = Term::Jump(b1);
push(&mut f, b1, Inst::AtomicLoad(Mem::Int(8, false), AtomOrd::Volatile, d, p));
push(&mut f, b1, Inst::IBin(IOp::Add, U64, x, x, d));
push(&mut f, b1, Inst::IBinI(IOp::Add, U64, i, i, 1));
push(&mut f, b1, Inst::ICmpI(Cond::Lt, false, c, i, 10));
f.blocks[b1 as usize].term = Term::Branch(c, b1, b2);
f.blocks[b2 as usize].term = Term::Ret(vec![x]);
all_passes(&mut f);
let v = volatile_ops(&f);
assert_eq!(v.len(), 1, "{}", f.dump());
// its block is still in the loop: it branches back to itself or reaches itself again
let lb = v[0].0 as u32;
let mut seen = vec![false; f.blocks.len()];
let mut work: Vec<u32> = f.blocks[lb as usize].term.succs().collect();
let mut cyc = false;
while let Some(x) = work.pop() {
if x == lb {
cyc = true;
break;
}
if !std::mem::replace(&mut seen[x as usize], true) {
work.extend(f.blocks[x as usize].term.succs());
}
}
assert!(cyc, "volatile load left the loop:\n{}", f.dump());
}
#[test]
fn if_conversion_does_not_speculate_volatile_load() {
// r = if c { *p (volatile) } else { 0 }: the load stays on its own path (it may fault)
let mut f = func("ifconv", 2);
f.rets = vec![Cls::I];
let (p, cnd) = (f.params[0], f.params[1]);
let (b0, b1, b2, b3) = (f.block(), f.block(), f.block(), f.block());
let (r, d, z) = (f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I));
f.blocks[b0 as usize].term = Term::Branch(cnd, b1, b2);
push(&mut f, b1, Inst::AtomicLoad(Mem::Int(8, false), AtomOrd::Volatile, d, p));
push(&mut f, b1, Inst::Mov(r, d));
f.blocks[b1 as usize].term = Term::Jump(b3);
push(&mut f, b2, Inst::Iconst(z, 0));
push(&mut f, b2, Inst::Mov(r, z));
f.blocks[b2 as usize].term = Term::Jump(b3);
f.blocks[b3 as usize].term = Term::Ret(vec![r]);
all_passes(&mut f);
let v = volatile_ops(&f);
assert_eq!(v.len(), 1, "{}", f.dump());
assert_ne!(v[0].0, 0, "volatile load speculated into the entry:\n{}", f.dump());
assert!(!f.blocks[0].insts.iter().any(|i| matches!(i, Inst::AtomicLoad(..))), "{}", f.dump());
}