1use crate::jit::{Cmp, MicroOp, Slot};
19use std::collections::HashSet;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct MapPlan {
25 pub induction: Slot,
27 pub limit: Slot,
29 pub cmp: Cmp,
32 pub body_start: usize,
34 pub body_end: usize,
35}
36
37pub fn recognize_elementwise_map(ops: &[MicroOp]) -> Option<MapPlan> {
53 if ops.len() < 5 {
55 return None;
56 }
57
58 let (induction, limit, cmp) = match &ops[0] {
61 MicroOp::Branch {
62 cmp: c @ (Cmp::Lt | Cmp::LtEq),
63 lhs,
64 rhs,
65 ..
66 } => (*lhs, *rhs, *c),
67 _ => return None,
68 };
69
70 let j = ops
72 .iter()
73 .position(|o| matches!(o, MicroOp::Jump { target: 0 }))?;
74 if j < 4 {
75 return None; }
77
78 let step = match &ops[j - 1] {
80 MicroOp::Add { dst, lhs, rhs } if *dst == induction && *lhs == induction => *rhs,
81 _ => return None,
82 };
83 match &ops[j - 2] {
86 MicroOp::LoadConst { dst, value } if *dst == step && *value == 1 => {}
87 _ => return None,
88 }
89
90 let (body_start, body_end) = (1usize, j - 2);
93 let body = &ops[body_start..body_end];
94 if body.is_empty() {
95 return None;
96 }
97
98 let mut defined: HashSet<Slot> = HashSet::new();
103 defined.insert(induction);
104 let mut wrote_an_array = false;
105
106 let known = |s: &Slot, defined: &HashSet<Slot>| defined.contains(s);
107
108 for op in body {
109 match op {
110 MicroOp::LoadConst { dst, .. } => {
111 defined.insert(*dst);
112 }
113 MicroOp::ArrLoad { dst, idx, checked, .. } => {
114 if *idx != induction || *checked {
115 return None;
120 }
121 defined.insert(*dst);
122 }
123 MicroOp::AddF { dst, lhs, rhs }
124 | MicroOp::SubF { dst, lhs, rhs }
125 | MicroOp::MulF { dst, lhs, rhs }
126 | MicroOp::DivF { dst, lhs, rhs } => {
127 if !known(lhs, &defined) || !known(rhs, &defined) {
128 return None; }
130 defined.insert(*dst);
131 }
132 MicroOp::SqrtF { dst, src } => {
133 if !known(src, &defined) {
134 return None;
135 }
136 defined.insert(*dst);
137 }
138 MicroOp::ArrStore { src, idx, checked, .. } => {
139 if *idx != induction || *checked || !known(src, &defined) {
140 return None;
141 }
142 wrote_an_array = true;
143 }
144 _ => return None,
148 }
149 }
150
151 if !wrote_an_array {
153 return None;
154 }
155
156 Some(MapPlan {
157 induction,
158 limit,
159 cmp,
160 body_start,
161 body_end,
162 })
163}
164
165#[cfg(target_arch = "x86_64")]
173pub fn emit_packed_arith(
174 asm: &mut crate::x64asm::Asm,
175 op: &MicroOp,
176 xmm: impl Fn(Slot) -> crate::x64asm::Xmm,
177 scratch: crate::x64asm::Xmm,
178) {
179 use crate::x64asm::Asm;
180 match op {
181 MicroOp::AddF { dst, lhs, rhs } => {
182 emit_bin(asm, xmm(*dst), xmm(*lhs), xmm(*rhs), scratch, Asm::addpd_rr, true)
183 }
184 MicroOp::MulF { dst, lhs, rhs } => {
185 emit_bin(asm, xmm(*dst), xmm(*lhs), xmm(*rhs), scratch, Asm::mulpd_rr, true)
186 }
187 MicroOp::SubF { dst, lhs, rhs } => {
188 emit_bin(asm, xmm(*dst), xmm(*lhs), xmm(*rhs), scratch, Asm::subpd_rr, false)
189 }
190 MicroOp::DivF { dst, lhs, rhs } => {
191 emit_bin(asm, xmm(*dst), xmm(*lhs), xmm(*rhs), scratch, Asm::divpd_rr, false)
192 }
193 MicroOp::SqrtF { dst, src } => asm.sqrtpd_rr(xmm(*dst), xmm(*src)),
194 _ => {}
195 }
196}
197
198#[cfg(target_arch = "x86_64")]
201fn emit_bin(
202 asm: &mut crate::x64asm::Asm,
203 d: crate::x64asm::Xmm,
204 l: crate::x64asm::Xmm,
205 r: crate::x64asm::Xmm,
206 scratch: crate::x64asm::Xmm,
207 op: fn(&mut crate::x64asm::Asm, crate::x64asm::Xmm, crate::x64asm::Xmm),
208 commutative: bool,
209) {
210 if d == l {
211 op(asm, d, r); } else if d == r {
213 if commutative {
214 op(asm, d, l); } else {
216 asm.movupd_rr(scratch, l);
217 op(asm, scratch, r); asm.movupd_rr(d, scratch);
219 }
220 } else {
221 asm.movupd_rr(d, l);
222 op(asm, d, r);
223 }
224}
225
226#[cfg(target_arch = "x86_64")]
241pub fn emit_map_kernel(body: &[MicroOp], plan: &MapPlan) -> Option<Vec<u8>> {
242 use crate::x64asm::{Asm, Cond, Reg, Xmm};
243 const ARRAY_REGS: [Reg; 4] = [Reg::R8, Reg::R9, Reg::R10, Reg::R11];
244 const LANES: [Xmm; 14] = [
245 Xmm::Xmm0, Xmm::Xmm1, Xmm::Xmm2, Xmm::Xmm3, Xmm::Xmm4, Xmm::Xmm5, Xmm::Xmm6,
246 Xmm::Xmm7, Xmm::Xmm8, Xmm::Xmm9, Xmm::Xmm10, Xmm::Xmm11, Xmm::Xmm12, Xmm::Xmm13,
247 ];
248 let scratch = Xmm::Xmm15;
249
250 let mut arr_reg: Vec<(Slot, Reg)> = Vec::new();
252 for op in body {
253 let ptr = match op {
254 MicroOp::ArrLoad { ptr_slot, .. } | MicroOp::ArrStore { ptr_slot, .. } => Some(*ptr_slot),
255 _ => None,
256 };
257 if let Some(p) = ptr {
258 if !arr_reg.iter().any(|(s, _)| *s == p) {
259 if arr_reg.len() >= ARRAY_REGS.len() {
260 return None;
261 }
262 arr_reg.push((p, ARRAY_REGS[arr_reg.len()]));
263 }
264 }
265 }
266 let reg_of = |slot: Slot| arr_reg.iter().find(|(s, _)| *s == slot).map(|(_, r)| *r);
267
268 let mut lane: Vec<(Slot, Xmm)> = Vec::new();
270 let mut add_lane = |slot: Slot, lane: &mut Vec<(Slot, Xmm)>| -> Option<()> {
271 if !lane.iter().any(|(s, _)| *s == slot) {
272 if lane.len() >= LANES.len() {
273 return None;
274 }
275 lane.push((slot, LANES[lane.len()]));
276 }
277 Some(())
278 };
279 for op in body {
280 match op {
281 MicroOp::ArrLoad { dst, .. }
282 | MicroOp::AddF { dst, .. }
283 | MicroOp::SubF { dst, .. }
284 | MicroOp::MulF { dst, .. }
285 | MicroOp::DivF { dst, .. }
286 | MicroOp::SqrtF { dst, .. } => add_lane(*dst, &mut lane)?,
287 MicroOp::ArrStore { .. } => {}
288 _ => return None, }
290 }
291 let xmm_of = |slot: Slot| lane.iter().find(|(s, _)| *s == slot).map(|(_, x)| *x);
292
293 let off = |slot: Slot| (slot as i32) * 8;
294 let mut a = Asm::new();
295 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);
301 a.sub_ri(Reg::Rcx, 1);
302 a.shl_ri(Reg::Rcx, 3); for (slot, reg) in &arr_reg {
304 a.mov_rm(*reg, Reg::Rdi, off(*slot)); a.add_rr(*reg, Reg::Rcx); }
307
308 let emit_body = |a: &mut Asm, packed: bool| {
312 for op in body {
313 match op {
314 MicroOp::ArrLoad { dst, ptr_slot, .. } => {
315 let p = reg_of(*ptr_slot).unwrap();
316 let x = xmm_of(*dst).unwrap();
317 if packed {
318 a.movupd_rm(x, p, 0);
319 } else {
320 a.movsd_rm(x, p, 0);
321 }
322 }
323 MicroOp::ArrStore { src, ptr_slot, .. } => {
324 let p = reg_of(*ptr_slot).unwrap();
325 let x = xmm_of(*src).unwrap();
326 if packed {
327 a.movupd_mr(p, 0, x);
328 } else {
329 a.movsd_mr(p, 0, x);
330 }
331 }
332 _ => emit_packed_arith(a, op, |s| xmm_of(s).unwrap(), scratch),
333 }
334 }
335 };
336
337 let exit_cond = if plan.cmp == Cmp::LtEq { Cond::Gt } else { Cond::Ge };
340
341 let pair_top = a.new_label();
343 let tail = a.new_label();
344 let done = a.new_label();
345 a.bind(pair_top);
346 a.mov_rr(Reg::Rax, Reg::Rsi);
347 a.add_ri(Reg::Rax, 1); a.cmp_rr(Reg::Rax, Reg::Rdx);
349 a.jcc(exit_cond, tail); emit_body(&mut a, true);
351 for (_, reg) in &arr_reg {
352 a.add_ri(*reg, 16); }
354 a.add_ri(Reg::Rsi, 2);
355 a.jmp(pair_top);
356
357 a.bind(tail);
359 a.cmp_rr(Reg::Rsi, Reg::Rdx);
360 a.jcc(exit_cond, done);
361 emit_body(&mut a, false);
362
363 a.bind(done);
364 a.mov_rr(Reg::Rax, Reg::Rdx);
368 if plan.cmp == Cmp::LtEq {
369 a.add_ri(Reg::Rax, 1);
370 }
371 a.mov_mr(Reg::Rdi, off(plan.induction), Reg::Rax);
372 a.xor_rr(Reg::Rax, Reg::Rax);
373 a.ret();
374 Some(a.resolve())
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380
381 fn load(dst: Slot, idx: Slot, ptr_slot: Slot, len_slot: Slot) -> MicroOp {
384 MicroOp::ArrLoad { dst, idx, ptr_slot, len_slot, byte: false, narrow32: false, checked: false }
385 }
386 fn store(src: Slot, idx: Slot, ptr_slot: Slot, len_slot: Slot) -> MicroOp {
387 MicroOp::ArrStore { src, idx, ptr_slot, len_slot, byte: false, narrow32: false, checked: false }
388 }
389 fn load_checked(dst: Slot, idx: Slot, ptr_slot: Slot, len_slot: Slot) -> MicroOp {
390 MicroOp::ArrLoad { dst, idx, ptr_slot, len_slot, byte: false, narrow32: false, checked: true }
391 }
392
393 fn loop_region(body: Vec<MicroOp>, cmp: Cmp, step_val: i64) -> Vec<MicroOp> {
397 let mut ops = vec![MicroOp::Branch { cmp, lhs: 0, rhs: 1, target: 0 }];
398 ops.extend(body);
399 ops.push(MicroOp::LoadConst { dst: 90, value: step_val });
400 ops.push(MicroOp::Add { dst: 0, lhs: 0, rhs: 90 });
401 let jump_idx = ops.len();
402 ops.push(MicroOp::Jump { target: 0 });
403 ops.push(MicroOp::Return { src: 0 });
404 if let MicroOp::Branch { target, .. } = &mut ops[0] {
405 *target = jump_idx + 1; }
407 ops
408 }
409
410 fn map_add_region() -> Vec<MicroOp> {
411 loop_region(
412 vec![
413 load(8, 0, 2, 5),
414 load(9, 0, 3, 6),
415 MicroOp::AddF { dst: 10, lhs: 8, rhs: 9 },
416 store(10, 0, 4, 7),
417 ],
418 Cmp::LtEq,
419 1,
420 )
421 }
422
423 #[test]
424 fn recognizes_elementwise_add_map() {
425 let ops = map_add_region();
426 let plan = recognize_elementwise_map(&ops).expect("c[i]=a[i]+b[i] is a map");
427 assert_eq!(plan.induction, 0);
428 assert_eq!(plan.limit, 1);
429 assert_eq!(plan.cmp, Cmp::LtEq);
430 assert_eq!((plan.body_start, plan.body_end), (1, 5));
432 }
433
434 #[test]
435 fn recognizes_multiop_map() {
436 let ops = loop_region(
438 vec![
439 load(8, 0, 2, 5),
440 load(9, 0, 3, 6),
441 MicroOp::MulF { dst: 10, lhs: 8, rhs: 9 },
442 MicroOp::AddF { dst: 10, lhs: 10, rhs: 8 },
443 store(10, 0, 4, 7),
444 ],
445 Cmp::Lt,
446 1,
447 );
448 assert!(recognize_elementwise_map(&ops).is_some());
449 }
450
451 #[test]
452 fn rejects_reduction_loop_carried_accumulator() {
453 let ops = loop_region(
456 vec![
457 load(8, 0, 2, 5),
458 MicroOp::AddF { dst: 9, lhs: 9, rhs: 8 }, store(9, 0, 4, 7),
460 ],
461 Cmp::Lt,
462 1,
463 );
464 assert_eq!(recognize_elementwise_map(&ops), None);
465 }
466
467 #[test]
468 fn rejects_cross_index_access_scan() {
469 let ops = loop_region(
471 vec![load(8, 9, 2, 5), store(8, 0, 4, 7)], Cmp::Lt,
473 1,
474 );
475 assert_eq!(recognize_elementwise_map(&ops), None);
476 }
477
478 #[test]
479 fn rejects_body_with_load_only_no_store() {
480 let ops = loop_region(vec![load(8, 0, 2, 5)], Cmp::Lt, 1);
482 assert_eq!(recognize_elementwise_map(&ops), None);
483 }
484
485 #[test]
486 fn rejects_non_unit_stride() {
487 let ops = loop_region(vec![load(8, 0, 2, 5), store(8, 0, 4, 7)], Cmp::Lt, 2);
490 assert_eq!(recognize_elementwise_map(&ops), None);
491 }
492
493 #[test]
494 fn rejects_bounds_checked_access() {
495 let ops = loop_region(vec![load_checked(8, 0, 2, 5), store(8, 0, 4, 7)], Cmp::Lt, 1);
498 assert_eq!(recognize_elementwise_map(&ops), None);
499 }
500
501 #[test]
502 fn rejects_non_back_edge_region() {
503 let ops = vec![
505 MicroOp::Branch { cmp: Cmp::Lt, lhs: 0, rhs: 1, target: 5 },
506 load(8, 0, 2, 5),
507 store(8, 0, 4, 7),
508 MicroOp::LoadConst { dst: 90, value: 1 },
509 MicroOp::Add { dst: 0, lhs: 0, rhs: 90 },
510 ];
511 assert_eq!(recognize_elementwise_map(&ops), None);
512 }
513
514 #[cfg(target_arch = "x86_64")]
515 fn run_frame(code: &[u8], frame: &mut [i64]) -> i64 {
516 let page = crate::JitPage::new(code).unwrap();
517 let f: extern "C" fn(*mut i64) -> i64 =
518 unsafe { std::mem::transmute(page.as_ptr()) };
519 f(frame.as_mut_ptr())
520 }
521
522 #[cfg(target_arch = "x86_64")]
523 #[test]
524 fn packed_body_lowering_matches_scalar_both_lanes() {
525 use crate::x64asm::{Asm, Reg, Xmm};
526 let lane = |s: Slot| match s {
528 0 => Xmm::Xmm0,
529 1 => Xmm::Xmm1,
530 2 => Xmm::Xmm2,
531 _ => unreachable!(),
532 };
533 let body = [
535 MicroOp::MulF { dst: 2, lhs: 0, rhs: 1 },
536 MicroOp::AddF { dst: 2, lhs: 2, rhs: 0 },
537 ];
538 let cases: [(f64, f64, f64, f64); 3] = [
539 (2.5, 0.5, 1.0, 3.0),
540 (-1.0, 7.25, 4.0, -2.5),
541 (0.1, 0.2, 1.0 / 3.0, 9.0),
542 ];
543 for (a0, a1, b0, b1) in cases {
544 let mut asm = Asm::new();
545 asm.movupd_rm(Xmm::Xmm0, Reg::Rdi, 0); asm.movupd_rm(Xmm::Xmm1, Reg::Rdi, 16); for op in &body {
548 emit_packed_arith(&mut asm, op, lane, Xmm::Xmm15);
549 }
550 asm.movupd_mr(Reg::Rdi, 32, Xmm::Xmm2); asm.ret();
552 let mut frame = [
553 a0.to_bits() as i64, a1.to_bits() as i64,
554 b0.to_bits() as i64, b1.to_bits() as i64,
555 0, 0,
556 ];
557 run_frame(&asm.resolve(), &mut frame);
558 assert_eq!(f64::from_bits(frame[4] as u64).to_bits(), (a0 * b0 + a0).to_bits(), "lane0");
559 assert_eq!(f64::from_bits(frame[5] as u64).to_bits(), (a1 * b1 + a1).to_bits(), "lane1");
560 }
561 }
562
563 #[cfg(target_arch = "x86_64")]
564 fn run_map(body: &[MicroOp], plan: &MapPlan, a: &[f64], b: &[f64]) -> Vec<f64> {
565 let code = emit_map_kernel(body, plan).expect("kernel emits");
568 let mut c = vec![0.0f64; a.len()];
569 let mut frame = [
570 1i64,
571 a.len() as i64,
572 a.as_ptr() as i64,
573 b.as_ptr() as i64,
574 c.as_mut_ptr() as i64,
575 0, 0, 0,
576 ];
577 run_frame(&code, &mut frame);
578 assert_eq!(frame[0], a.len() as i64 + 1, "induction terminal n+1");
580 c
581 }
582
583 #[cfg(target_arch = "x86_64")]
584 #[test]
585 fn map_kernel_add_matches_scalar_all_lengths() {
586 let body = vec![
589 load(5, 0, 2, 0),
590 load(6, 0, 3, 0),
591 MicroOp::AddF { dst: 7, lhs: 5, rhs: 6 },
592 store(7, 0, 4, 0),
593 ];
594 let plan = MapPlan { induction: 0, limit: 1, cmp: Cmp::LtEq, body_start: 0, body_end: 4 };
595 for n in [0usize, 1, 2, 3, 7, 8, 15] {
596 let a: Vec<f64> = (0..n).map(|i| i as f64 * 1.5 - 3.0).collect();
597 let b: Vec<f64> = (0..n).map(|i| i as f64 * -0.25 + 1.0).collect();
598 let got = run_map(&body, &plan, &a, &b);
599 for i in 0..n {
600 assert_eq!(got[i].to_bits(), (a[i] + b[i]).to_bits(), "n={n} i={i}");
601 }
602 }
603 }
604
605 #[cfg(target_arch = "x86_64")]
606 #[test]
607 fn map_kernel_multiop_matches_scalar() {
608 let body = vec![
610 load(5, 0, 2, 0),
611 load(6, 0, 3, 0),
612 MicroOp::MulF { dst: 7, lhs: 5, rhs: 6 },
613 MicroOp::AddF { dst: 7, lhs: 7, rhs: 5 },
614 store(7, 0, 4, 0),
615 ];
616 let plan = MapPlan { induction: 0, limit: 1, cmp: Cmp::LtEq, body_start: 0, body_end: 5 };
617 for n in [1usize, 4, 5, 16, 17] {
618 let a: Vec<f64> = (0..n).map(|i| 0.1 * i as f64 + 0.3).collect();
619 let b: Vec<f64> = (0..n).map(|i| 2.0 - 0.07 * i as f64).collect();
620 let got = run_map(&body, &plan, &a, &b);
621 for i in 0..n {
622 assert_eq!(got[i].to_bits(), (a[i] * b[i] + a[i]).to_bits(), "n={n} i={i}");
623 }
624 }
625 }
626
627 #[cfg(target_arch = "x86_64")]
628 #[test]
629 fn map_kernel_bails_on_unsupported_loadconst() {
630 let body = vec![
633 load(5, 0, 2, 0),
634 MicroOp::LoadConst { dst: 6, value: 2 },
635 MicroOp::MulF { dst: 7, lhs: 5, rhs: 6 },
636 store(7, 0, 4, 0),
637 ];
638 let plan = MapPlan { induction: 0, limit: 1, cmp: Cmp::LtEq, body_start: 0, body_end: 4 };
639 assert!(emit_map_kernel(&body, &plan).is_none());
640 }
641
642 #[cfg(target_arch = "x86_64")]
643 #[test]
644 fn packed_body_lowering_non_commutative_dst_eq_rhs_uses_scratch() {
645 use crate::x64asm::{Asm, Reg, Xmm};
646 let lane = |s: Slot| match s {
650 0 => Xmm::Xmm0,
651 1 => Xmm::Xmm1,
652 _ => unreachable!(),
653 };
654 let op = MicroOp::SubF { dst: 1, lhs: 0, rhs: 1 };
655 let cases: [(f64, f64, f64, f64); 2] = [(2.5, 0.5, 1.0, 3.0), (-1.0, 3.0, 4.0, -2.5)];
656 for (a0, a1, b0, b1) in cases {
657 let mut asm = Asm::new();
658 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);
661 asm.movupd_mr(Reg::Rdi, 32, Xmm::Xmm1);
662 asm.ret();
663 let mut frame = [
664 a0.to_bits() as i64, a1.to_bits() as i64,
665 b0.to_bits() as i64, b1.to_bits() as i64,
666 0, 0,
667 ];
668 run_frame(&asm.resolve(), &mut frame);
669 assert_eq!(f64::from_bits(frame[4] as u64).to_bits(), (a0 - b0).to_bits(), "lane0 a-b");
670 assert_eq!(f64::from_bits(frame[5] as u64).to_bits(), (a1 - b1).to_bits(), "lane1 a-b");
671 }
672 }
673}