use crate::jit::{Cmp, MicroOp, Slot};
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MapPlan {
pub induction: Slot,
pub limit: Slot,
pub cmp: Cmp,
pub body_start: usize,
pub body_end: usize,
}
pub fn recognize_elementwise_map(ops: &[MicroOp]) -> Option<MapPlan> {
if ops.len() < 5 {
return None;
}
let (induction, limit, cmp) = match &ops[0] {
MicroOp::Branch {
cmp: c @ (Cmp::Lt | Cmp::LtEq),
lhs,
rhs,
..
} => (*lhs, *rhs, *c),
_ => return None,
};
let j = ops
.iter()
.position(|o| matches!(o, MicroOp::Jump { target: 0 }))?;
if j < 4 {
return None; }
let step = match &ops[j - 1] {
MicroOp::Add { dst, lhs, rhs } if *dst == induction && *lhs == induction => *rhs,
_ => return None,
};
match &ops[j - 2] {
MicroOp::LoadConst { dst, value } if *dst == step && *value == 1 => {}
_ => return None,
}
let (body_start, body_end) = (1usize, j - 2);
let body = &ops[body_start..body_end];
if body.is_empty() {
return None;
}
let mut defined: HashSet<Slot> = HashSet::new();
defined.insert(induction);
let mut wrote_an_array = false;
let known = |s: &Slot, defined: &HashSet<Slot>| defined.contains(s);
for op in body {
match op {
MicroOp::LoadConst { dst, .. } => {
defined.insert(*dst);
}
MicroOp::ArrLoad { dst, idx, checked, .. } => {
if *idx != induction || *checked {
return None;
}
defined.insert(*dst);
}
MicroOp::AddF { dst, lhs, rhs }
| MicroOp::SubF { dst, lhs, rhs }
| MicroOp::MulF { dst, lhs, rhs }
| MicroOp::DivF { dst, lhs, rhs } => {
if !known(lhs, &defined) || !known(rhs, &defined) {
return None; }
defined.insert(*dst);
}
MicroOp::SqrtF { dst, src } => {
if !known(src, &defined) {
return None;
}
defined.insert(*dst);
}
MicroOp::ArrStore { src, idx, checked, .. } => {
if *idx != induction || *checked || !known(src, &defined) {
return None;
}
wrote_an_array = true;
}
_ => return None,
}
}
if !wrote_an_array {
return None;
}
Some(MapPlan {
induction,
limit,
cmp,
body_start,
body_end,
})
}
#[cfg(target_arch = "x86_64")]
pub fn emit_packed_arith(
asm: &mut crate::x64asm::Asm,
op: &MicroOp,
xmm: impl Fn(Slot) -> crate::x64asm::Xmm,
scratch: crate::x64asm::Xmm,
) {
use crate::x64asm::Asm;
match op {
MicroOp::AddF { dst, lhs, rhs } => {
emit_bin(asm, xmm(*dst), xmm(*lhs), xmm(*rhs), scratch, Asm::addpd_rr, true)
}
MicroOp::MulF { dst, lhs, rhs } => {
emit_bin(asm, xmm(*dst), xmm(*lhs), xmm(*rhs), scratch, Asm::mulpd_rr, true)
}
MicroOp::SubF { dst, lhs, rhs } => {
emit_bin(asm, xmm(*dst), xmm(*lhs), xmm(*rhs), scratch, Asm::subpd_rr, false)
}
MicroOp::DivF { dst, lhs, rhs } => {
emit_bin(asm, xmm(*dst), xmm(*lhs), xmm(*rhs), scratch, Asm::divpd_rr, false)
}
MicroOp::SqrtF { dst, src } => asm.sqrtpd_rr(xmm(*dst), xmm(*src)),
_ => {}
}
}
#[cfg(target_arch = "x86_64")]
fn emit_bin(
asm: &mut crate::x64asm::Asm,
d: crate::x64asm::Xmm,
l: crate::x64asm::Xmm,
r: crate::x64asm::Xmm,
scratch: crate::x64asm::Xmm,
op: fn(&mut crate::x64asm::Asm, crate::x64asm::Xmm, crate::x64asm::Xmm),
commutative: bool,
) {
if d == l {
op(asm, d, r); } else if d == r {
if commutative {
op(asm, d, l); } else {
asm.movupd_rr(scratch, l);
op(asm, scratch, r); asm.movupd_rr(d, scratch);
}
} else {
asm.movupd_rr(d, l);
op(asm, d, r);
}
}
#[cfg(target_arch = "x86_64")]
pub fn emit_map_kernel(body: &[MicroOp], plan: &MapPlan) -> Option<Vec<u8>> {
use crate::x64asm::{Asm, Cond, Reg, Xmm};
const ARRAY_REGS: [Reg; 4] = [Reg::R8, Reg::R9, Reg::R10, Reg::R11];
const LANES: [Xmm; 14] = [
Xmm::Xmm0, Xmm::Xmm1, Xmm::Xmm2, Xmm::Xmm3, Xmm::Xmm4, Xmm::Xmm5, Xmm::Xmm6,
Xmm::Xmm7, Xmm::Xmm8, Xmm::Xmm9, Xmm::Xmm10, Xmm::Xmm11, Xmm::Xmm12, Xmm::Xmm13,
];
let scratch = Xmm::Xmm15;
let mut arr_reg: Vec<(Slot, Reg)> = Vec::new();
for op in body {
let ptr = match op {
MicroOp::ArrLoad { ptr_slot, .. } | MicroOp::ArrStore { ptr_slot, .. } => Some(*ptr_slot),
_ => None,
};
if let Some(p) = ptr {
if !arr_reg.iter().any(|(s, _)| *s == p) {
if arr_reg.len() >= ARRAY_REGS.len() {
return None;
}
arr_reg.push((p, ARRAY_REGS[arr_reg.len()]));
}
}
}
let reg_of = |slot: Slot| arr_reg.iter().find(|(s, _)| *s == slot).map(|(_, r)| *r);
let mut lane: Vec<(Slot, Xmm)> = Vec::new();
let mut add_lane = |slot: Slot, lane: &mut Vec<(Slot, Xmm)>| -> Option<()> {
if !lane.iter().any(|(s, _)| *s == slot) {
if lane.len() >= LANES.len() {
return None;
}
lane.push((slot, LANES[lane.len()]));
}
Some(())
};
for op in body {
match op {
MicroOp::ArrLoad { dst, .. }
| MicroOp::AddF { dst, .. }
| MicroOp::SubF { dst, .. }
| MicroOp::MulF { dst, .. }
| MicroOp::DivF { dst, .. }
| MicroOp::SqrtF { dst, .. } => add_lane(*dst, &mut lane)?,
MicroOp::ArrStore { .. } => {}
_ => return None, }
}
let xmm_of = |slot: Slot| lane.iter().find(|(s, _)| *s == slot).map(|(_, x)| *x);
let off = |slot: Slot| (slot as i32) * 8;
let mut a = Asm::new();
a.mov_rm(Reg::Rsi, Reg::Rdi, off(plan.induction)); a.mov_rm(Reg::Rdx, Reg::Rdi, off(plan.limit)); a.mov_rr(Reg::Rcx, Reg::Rsi);
a.sub_ri(Reg::Rcx, 1);
a.shl_ri(Reg::Rcx, 3); for (slot, reg) in &arr_reg {
a.mov_rm(*reg, Reg::Rdi, off(*slot)); a.add_rr(*reg, Reg::Rcx); }
let emit_body = |a: &mut Asm, packed: bool| {
for op in body {
match op {
MicroOp::ArrLoad { dst, ptr_slot, .. } => {
let p = reg_of(*ptr_slot).unwrap();
let x = xmm_of(*dst).unwrap();
if packed {
a.movupd_rm(x, p, 0);
} else {
a.movsd_rm(x, p, 0);
}
}
MicroOp::ArrStore { src, ptr_slot, .. } => {
let p = reg_of(*ptr_slot).unwrap();
let x = xmm_of(*src).unwrap();
if packed {
a.movupd_mr(p, 0, x);
} else {
a.movsd_mr(p, 0, x);
}
}
_ => emit_packed_arith(a, op, |s| xmm_of(s).unwrap(), scratch),
}
}
};
let exit_cond = if plan.cmp == Cmp::LtEq { Cond::Gt } else { Cond::Ge };
let pair_top = a.new_label();
let tail = a.new_label();
let done = a.new_label();
a.bind(pair_top);
a.mov_rr(Reg::Rax, Reg::Rsi);
a.add_ri(Reg::Rax, 1); a.cmp_rr(Reg::Rax, Reg::Rdx);
a.jcc(exit_cond, tail); emit_body(&mut a, true);
for (_, reg) in &arr_reg {
a.add_ri(*reg, 16); }
a.add_ri(Reg::Rsi, 2);
a.jmp(pair_top);
a.bind(tail);
a.cmp_rr(Reg::Rsi, Reg::Rdx);
a.jcc(exit_cond, done);
emit_body(&mut a, false);
a.bind(done);
a.mov_rr(Reg::Rax, Reg::Rdx);
if plan.cmp == Cmp::LtEq {
a.add_ri(Reg::Rax, 1);
}
a.mov_mr(Reg::Rdi, off(plan.induction), Reg::Rax);
a.xor_rr(Reg::Rax, Reg::Rax);
a.ret();
Some(a.resolve())
}
#[cfg(test)]
mod tests {
use super::*;
fn load(dst: Slot, idx: Slot, ptr_slot: Slot, len_slot: Slot) -> MicroOp {
MicroOp::ArrLoad { dst, idx, ptr_slot, len_slot, byte: false, narrow32: false, checked: false }
}
fn store(src: Slot, idx: Slot, ptr_slot: Slot, len_slot: Slot) -> MicroOp {
MicroOp::ArrStore { src, idx, ptr_slot, len_slot, byte: false, narrow32: false, checked: false }
}
fn load_checked(dst: Slot, idx: Slot, ptr_slot: Slot, len_slot: Slot) -> MicroOp {
MicroOp::ArrLoad { dst, idx, ptr_slot, len_slot, byte: false, narrow32: false, checked: true }
}
fn loop_region(body: Vec<MicroOp>, cmp: Cmp, step_val: i64) -> Vec<MicroOp> {
let mut ops = vec![MicroOp::Branch { cmp, lhs: 0, rhs: 1, target: 0 }];
ops.extend(body);
ops.push(MicroOp::LoadConst { dst: 90, value: step_val });
ops.push(MicroOp::Add { dst: 0, lhs: 0, rhs: 90 });
let jump_idx = ops.len();
ops.push(MicroOp::Jump { target: 0 });
ops.push(MicroOp::Return { src: 0 });
if let MicroOp::Branch { target, .. } = &mut ops[0] {
*target = jump_idx + 1; }
ops
}
fn map_add_region() -> Vec<MicroOp> {
loop_region(
vec![
load(8, 0, 2, 5),
load(9, 0, 3, 6),
MicroOp::AddF { dst: 10, lhs: 8, rhs: 9 },
store(10, 0, 4, 7),
],
Cmp::LtEq,
1,
)
}
#[test]
fn recognizes_elementwise_add_map() {
let ops = map_add_region();
let plan = recognize_elementwise_map(&ops).expect("c[i]=a[i]+b[i] is a map");
assert_eq!(plan.induction, 0);
assert_eq!(plan.limit, 1);
assert_eq!(plan.cmp, Cmp::LtEq);
assert_eq!((plan.body_start, plan.body_end), (1, 5));
}
#[test]
fn recognizes_multiop_map() {
let ops = loop_region(
vec![
load(8, 0, 2, 5),
load(9, 0, 3, 6),
MicroOp::MulF { dst: 10, lhs: 8, rhs: 9 },
MicroOp::AddF { dst: 10, lhs: 10, rhs: 8 },
store(10, 0, 4, 7),
],
Cmp::Lt,
1,
);
assert!(recognize_elementwise_map(&ops).is_some());
}
#[test]
fn rejects_reduction_loop_carried_accumulator() {
let ops = loop_region(
vec![
load(8, 0, 2, 5),
MicroOp::AddF { dst: 9, lhs: 9, rhs: 8 }, store(9, 0, 4, 7),
],
Cmp::Lt,
1,
);
assert_eq!(recognize_elementwise_map(&ops), None);
}
#[test]
fn rejects_cross_index_access_scan() {
let ops = loop_region(
vec![load(8, 9, 2, 5), store(8, 0, 4, 7)], Cmp::Lt,
1,
);
assert_eq!(recognize_elementwise_map(&ops), None);
}
#[test]
fn rejects_body_with_load_only_no_store() {
let ops = loop_region(vec![load(8, 0, 2, 5)], Cmp::Lt, 1);
assert_eq!(recognize_elementwise_map(&ops), None);
}
#[test]
fn rejects_non_unit_stride() {
let ops = loop_region(vec![load(8, 0, 2, 5), store(8, 0, 4, 7)], Cmp::Lt, 2);
assert_eq!(recognize_elementwise_map(&ops), None);
}
#[test]
fn rejects_bounds_checked_access() {
let ops = loop_region(vec![load_checked(8, 0, 2, 5), store(8, 0, 4, 7)], Cmp::Lt, 1);
assert_eq!(recognize_elementwise_map(&ops), None);
}
#[test]
fn rejects_non_back_edge_region() {
let ops = vec![
MicroOp::Branch { cmp: Cmp::Lt, lhs: 0, rhs: 1, target: 5 },
load(8, 0, 2, 5),
store(8, 0, 4, 7),
MicroOp::LoadConst { dst: 90, value: 1 },
MicroOp::Add { dst: 0, lhs: 0, rhs: 90 },
];
assert_eq!(recognize_elementwise_map(&ops), None);
}
#[cfg(target_arch = "x86_64")]
fn run_frame(code: &[u8], frame: &mut [i64]) -> i64 {
let page = crate::JitPage::new(code).unwrap();
let f: extern "C" fn(*mut i64) -> i64 =
unsafe { std::mem::transmute(page.as_ptr()) };
f(frame.as_mut_ptr())
}
#[cfg(target_arch = "x86_64")]
#[test]
fn packed_body_lowering_matches_scalar_both_lanes() {
use crate::x64asm::{Asm, Reg, Xmm};
let lane = |s: Slot| match s {
0 => Xmm::Xmm0,
1 => Xmm::Xmm1,
2 => Xmm::Xmm2,
_ => unreachable!(),
};
let body = [
MicroOp::MulF { dst: 2, lhs: 0, rhs: 1 },
MicroOp::AddF { dst: 2, lhs: 2, rhs: 0 },
];
let cases: [(f64, f64, f64, f64); 3] = [
(2.5, 0.5, 1.0, 3.0),
(-1.0, 7.25, 4.0, -2.5),
(0.1, 0.2, 1.0 / 3.0, 9.0),
];
for (a0, a1, b0, b1) in cases {
let mut asm = Asm::new();
asm.movupd_rm(Xmm::Xmm0, Reg::Rdi, 0); asm.movupd_rm(Xmm::Xmm1, Reg::Rdi, 16); for op in &body {
emit_packed_arith(&mut asm, op, lane, Xmm::Xmm15);
}
asm.movupd_mr(Reg::Rdi, 32, Xmm::Xmm2); asm.ret();
let mut frame = [
a0.to_bits() as i64, a1.to_bits() as i64,
b0.to_bits() as i64, b1.to_bits() as i64,
0, 0,
];
run_frame(&asm.resolve(), &mut frame);
assert_eq!(f64::from_bits(frame[4] as u64).to_bits(), (a0 * b0 + a0).to_bits(), "lane0");
assert_eq!(f64::from_bits(frame[5] as u64).to_bits(), (a1 * b1 + a1).to_bits(), "lane1");
}
}
#[cfg(target_arch = "x86_64")]
fn run_map(body: &[MicroOp], plan: &MapPlan, a: &[f64], b: &[f64]) -> Vec<f64> {
let code = emit_map_kernel(body, plan).expect("kernel emits");
let mut c = vec![0.0f64; a.len()];
let mut frame = [
1i64,
a.len() as i64,
a.as_ptr() as i64,
b.as_ptr() as i64,
c.as_mut_ptr() as i64,
0, 0, 0,
];
run_frame(&code, &mut frame);
assert_eq!(frame[0], a.len() as i64 + 1, "induction terminal n+1");
c
}
#[cfg(target_arch = "x86_64")]
#[test]
fn map_kernel_add_matches_scalar_all_lengths() {
let body = vec![
load(5, 0, 2, 0),
load(6, 0, 3, 0),
MicroOp::AddF { dst: 7, lhs: 5, rhs: 6 },
store(7, 0, 4, 0),
];
let plan = MapPlan { induction: 0, limit: 1, cmp: Cmp::LtEq, body_start: 0, body_end: 4 };
for n in [0usize, 1, 2, 3, 7, 8, 15] {
let a: Vec<f64> = (0..n).map(|i| i as f64 * 1.5 - 3.0).collect();
let b: Vec<f64> = (0..n).map(|i| i as f64 * -0.25 + 1.0).collect();
let got = run_map(&body, &plan, &a, &b);
for i in 0..n {
assert_eq!(got[i].to_bits(), (a[i] + b[i]).to_bits(), "n={n} i={i}");
}
}
}
#[cfg(target_arch = "x86_64")]
#[test]
fn map_kernel_multiop_matches_scalar() {
let body = vec![
load(5, 0, 2, 0),
load(6, 0, 3, 0),
MicroOp::MulF { dst: 7, lhs: 5, rhs: 6 },
MicroOp::AddF { dst: 7, lhs: 7, rhs: 5 },
store(7, 0, 4, 0),
];
let plan = MapPlan { induction: 0, limit: 1, cmp: Cmp::LtEq, body_start: 0, body_end: 5 };
for n in [1usize, 4, 5, 16, 17] {
let a: Vec<f64> = (0..n).map(|i| 0.1 * i as f64 + 0.3).collect();
let b: Vec<f64> = (0..n).map(|i| 2.0 - 0.07 * i as f64).collect();
let got = run_map(&body, &plan, &a, &b);
for i in 0..n {
assert_eq!(got[i].to_bits(), (a[i] * b[i] + a[i]).to_bits(), "n={n} i={i}");
}
}
}
#[cfg(target_arch = "x86_64")]
#[test]
fn map_kernel_bails_on_unsupported_loadconst() {
let body = vec![
load(5, 0, 2, 0),
MicroOp::LoadConst { dst: 6, value: 2 },
MicroOp::MulF { dst: 7, lhs: 5, rhs: 6 },
store(7, 0, 4, 0),
];
let plan = MapPlan { induction: 0, limit: 1, cmp: Cmp::LtEq, body_start: 0, body_end: 4 };
assert!(emit_map_kernel(&body, &plan).is_none());
}
#[cfg(target_arch = "x86_64")]
#[test]
fn packed_body_lowering_non_commutative_dst_eq_rhs_uses_scratch() {
use crate::x64asm::{Asm, Reg, Xmm};
let lane = |s: Slot| match s {
0 => Xmm::Xmm0,
1 => Xmm::Xmm1,
_ => unreachable!(),
};
let op = MicroOp::SubF { dst: 1, lhs: 0, rhs: 1 };
let cases: [(f64, f64, f64, f64); 2] = [(2.5, 0.5, 1.0, 3.0), (-1.0, 3.0, 4.0, -2.5)];
for (a0, a1, b0, b1) in cases {
let mut asm = Asm::new();
asm.movupd_rm(Xmm::Xmm0, Reg::Rdi, 0); asm.movupd_rm(Xmm::Xmm1, Reg::Rdi, 16); emit_packed_arith(&mut asm, &op, lane, Xmm::Xmm15);
asm.movupd_mr(Reg::Rdi, 32, Xmm::Xmm1);
asm.ret();
let mut frame = [
a0.to_bits() as i64, a1.to_bits() as i64,
b0.to_bits() as i64, b1.to_bits() as i64,
0, 0,
];
run_frame(&asm.resolve(), &mut frame);
assert_eq!(f64::from_bits(frame[4] as u64).to_bits(), (a0 - b0).to_bits(), "lane0 a-b");
assert_eq!(f64::from_bits(frame[5] as u64).to_bits(), (a1 - b1).to_bits(), "lane1 a-b");
}
}
}