use crate::{KernelBody, KernelDescriptor, KernelOp, KernelOpKind, LiteralValue};
use rustc_hash::FxHashMap;
use vyre_foundation::ir::BinOp;
#[must_use]
pub fn strength_reduce(desc: &KernelDescriptor) -> KernelDescriptor {
let mut out = desc.clone();
out.body = strength_reduce_body(out.body);
out
}
fn strength_reduce_body(mut body: KernelBody) -> KernelBody {
let result_to_lit_u32: FxHashMap<u32, u32> = body
.ops
.iter()
.filter_map(|op| match (&op.kind, op.result, op.operands.first()) {
(KernelOpKind::Literal, Some(r), Some(pool_idx)) => {
match body.literals.get(*pool_idx as usize) {
Some(LiteralValue::U32(v)) => Some((r, *v)),
_ => None,
}
}
_ => None,
})
.collect();
let mut next_id: u32 = body
.ops
.iter()
.filter_map(|o| o.result)
.max()
.map(|m| m + 1)
.unwrap_or(0);
enum Rewrite {
Mul(u32, u32, u32), Div(u32, u32, u32), Mod(u32, u32, u32), }
let mut rewrites: Vec<Rewrite> = Vec::new();
for (idx, op) in body.ops.iter().enumerate() {
let bin = match &op.kind {
KernelOpKind::BinOpKind(b) => *b,
_ => continue,
};
if op.operands.len() != 2 {
continue;
}
let lhs = op.operands[0];
let rhs = op.operands[1];
let lhs_lit = result_to_lit_u32.get(&lhs).copied();
let rhs_lit = result_to_lit_u32.get(&rhs).copied();
match bin {
BinOp::Mul => {
if let Some((other_id, lit)) = either_lit(lhs, rhs, lhs_lit, rhs_lit) {
if let Some(log2) = power_of_2_log(lit) {
rewrites.push(Rewrite::Mul(other_id, log2, idx as u32));
}
}
}
BinOp::Div => {
if let Some(lit) = rhs_lit {
if let Some(log2) = power_of_2_log(lit) {
rewrites.push(Rewrite::Div(lhs, log2, idx as u32));
}
}
}
BinOp::Mod => {
if let Some(lit) = rhs_lit {
if power_of_2_log(lit).is_some() {
rewrites.push(Rewrite::Mod(lhs, lit - 1, idx as u32));
}
}
}
_ => {}
}
}
for r in rewrites {
let (kind, other_id, lit_value, op_idx) = match r {
Rewrite::Mul(o, log2, idx) => (BinOp::Shl, o, log2, idx),
Rewrite::Div(lhs, log2, idx) => (BinOp::Shr, lhs, log2, idx),
Rewrite::Mod(lhs, mask, idx) => (BinOp::BitAnd, lhs, mask, idx),
};
let pool_idx = push_lit(&mut body.literals, LiteralValue::U32(lit_value));
let synth_id = next_id;
next_id += 1;
body.ops.push(KernelOp {
kind: KernelOpKind::Literal,
operands: vec![pool_idx],
result: Some(synth_id),
});
body.ops[op_idx as usize].kind = KernelOpKind::BinOpKind(kind);
body.ops[op_idx as usize].operands = vec![other_id, synth_id];
}
body.child_bodies = body
.child_bodies
.into_iter()
.map(strength_reduce_body)
.collect();
body
}
fn either_lit(
lhs: u32,
rhs: u32,
lhs_lit: Option<u32>,
rhs_lit: Option<u32>,
) -> Option<(u32, u32)> {
let lhs_p2 = lhs_lit.and_then(power_of_2_log).is_some();
let rhs_p2 = rhs_lit.and_then(power_of_2_log).is_some();
if rhs_p2 {
rhs_lit.map(|r| (lhs, r))
} else if lhs_p2 {
lhs_lit.map(|l| (rhs, l))
} else {
None
}
}
fn power_of_2_log(v: u32) -> Option<u32> {
if v >= 2 && v.is_power_of_two() {
Some(v.trailing_zeros())
} else {
None
}
}
fn push_lit(literals: &mut Vec<LiteralValue>, lit: LiteralValue) -> u32 {
let idx = literals.len() as u32;
literals.push(lit);
idx
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{BindingLayout, Dispatch, KernelBody, KernelDescriptor, KernelOp};
fn binop_kernel(op: BinOp, lit_lhs: u32, lit_rhs: u32) -> KernelDescriptor {
KernelDescriptor {
id: "k".into(),
bindings: BindingLayout { slots: vec![] },
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![1],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::BinOpKind(op),
operands: vec![0, 1],
result: Some(2),
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(lit_lhs), LiteralValue::U32(lit_rhs)],
},
}
}
#[test]
fn empty_kernel_no_change() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout { slots: vec![] },
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![],
child_bodies: vec![],
literals: vec![],
},
};
let out = strength_reduce(&desc);
assert!(out.body.ops.is_empty());
}
#[test]
fn mul_by_pow2_becomes_shl() {
let out = strength_reduce(&binop_kernel(BinOp::Mul, 5, 8));
let bin_op = out.body.ops.iter().find_map(|o| {
if let KernelOpKind::BinOpKind(b) = &o.kind {
Some(*b)
} else {
None
}
});
assert_eq!(bin_op, Some(BinOp::Shl));
}
#[test]
fn mul_by_non_pow2_unchanged() {
let out = strength_reduce(&binop_kernel(BinOp::Mul, 5, 7));
let bin_op = out.body.ops.iter().find_map(|o| {
if let KernelOpKind::BinOpKind(b) = &o.kind {
Some(*b)
} else {
None
}
});
assert_eq!(bin_op, Some(BinOp::Mul));
}
#[test]
fn mul_by_pow2_lhs_also_works() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout { slots: vec![] },
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![1],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::BinOpKind(BinOp::Mul),
operands: vec![0, 1], result: Some(2),
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(8), LiteralValue::U32(5)],
},
};
let out = strength_reduce(&desc);
let bin_op = out.body.ops.iter().find_map(|o| {
if let KernelOpKind::BinOpKind(b) = &o.kind {
Some(*b)
} else {
None
}
});
assert_eq!(bin_op, Some(BinOp::Shl));
}
#[test]
fn div_by_pow2_becomes_shr() {
let out = strength_reduce(&binop_kernel(BinOp::Div, 100, 16));
let bin_op = out.body.ops.iter().find_map(|o| {
if let KernelOpKind::BinOpKind(b) = &o.kind {
Some(*b)
} else {
None
}
});
assert_eq!(bin_op, Some(BinOp::Shr));
}
#[test]
fn div_constant_on_lhs_unchanged() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout { slots: vec![] },
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![1],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::BinOpKind(BinOp::Div),
operands: vec![0, 1], result: Some(2),
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(16), LiteralValue::U32(7)],
},
};
let out = strength_reduce(&desc);
let bin_op = out.body.ops.iter().find_map(|o| {
if let KernelOpKind::BinOpKind(b) = &o.kind {
Some(*b)
} else {
None
}
});
assert_eq!(bin_op, Some(BinOp::Div));
}
#[test]
fn mod_by_pow2_becomes_bitand() {
let out = strength_reduce(&binop_kernel(BinOp::Mod, 100, 4));
let bin_op = out.body.ops.iter().find_map(|o| {
if let KernelOpKind::BinOpKind(b) = &o.kind {
Some(*b)
} else {
None
}
});
assert_eq!(bin_op, Some(BinOp::BitAnd));
let last_lit = out.body.literals.last().unwrap();
assert_eq!(*last_lit, LiteralValue::U32(3));
}
#[test]
fn mul_by_zero_or_one_not_reduced() {
for lit in [0u32, 1] {
let out = strength_reduce(&binop_kernel(BinOp::Mul, 5, lit));
let bin_op = out.body.ops.iter().find_map(|o| {
if let KernelOpKind::BinOpKind(b) = &o.kind {
Some(*b)
} else {
None
}
});
assert_eq!(
bin_op,
Some(BinOp::Mul),
"lit {lit} should NOT trigger reduction"
);
}
}
#[test]
fn power_of_2_log_helper() {
assert_eq!(power_of_2_log(2), Some(1));
assert_eq!(power_of_2_log(4), Some(2));
assert_eq!(power_of_2_log(8), Some(3));
assert_eq!(power_of_2_log(1024), Some(10));
assert_eq!(power_of_2_log(0), None);
assert_eq!(power_of_2_log(1), None);
assert_eq!(power_of_2_log(3), None);
assert_eq!(power_of_2_log(7), None);
}
#[test]
fn strength_reduce_is_idempotent() {
let desc = binop_kernel(BinOp::Mul, 5, 8);
let once = strength_reduce(&desc);
let twice = strength_reduce(&once);
assert_eq!(once.body.ops.len(), twice.body.ops.len());
assert_eq!(once.body.literals.len(), twice.body.literals.len());
}
}