1use std::sync::OnceLock;
21
22use crate::asm_presets::{
23 abs_f64x2, abs_f64x4, add_f64x2, add_f64x4, cmp_eq_f64x4, coef_merge_f64x4, div_f64x2,
24 div_f64x4, fma_f64x4, mul_f64x2, mul_f64x4, neg_f64x2, neg_f64x4, sqrt_f64x2, sqrt_f64x4,
25 sub_f64x2, sub_f64x4,
26};
27use crate::rssn_error;
28
29#[inline(always)]
34fn scalar_add_lanes(lhs: &[f64], rhs: &[f64], out: &mut [f64]) {
35 debug_assert_eq!(lhs.len(), out.len());
36 debug_assert_eq!(rhs.len(), out.len());
37 for i in 0..out.len() {
38 out[i] = lhs[i] + rhs[i];
39 }
40}
41
42#[inline(always)]
43fn scalar_mul_lanes(lhs: &[f64], rhs: &[f64], out: &mut [f64]) {
44 debug_assert_eq!(lhs.len(), out.len());
45 debug_assert_eq!(rhs.len(), out.len());
46 for i in 0..out.len() {
47 out[i] = lhs[i] * rhs[i];
48 }
49}
50
51#[inline(always)]
52fn scalar_sub_lanes(lhs: &[f64], rhs: &[f64], out: &mut [f64]) {
53 debug_assert_eq!(lhs.len(), out.len());
54 debug_assert_eq!(rhs.len(), out.len());
55 for i in 0..out.len() {
56 out[i] = lhs[i] - rhs[i];
57 }
58}
59
60#[inline(always)]
61fn scalar_div_lanes(lhs: &[f64], rhs: &[f64], out: &mut [f64]) {
62 debug_assert_eq!(lhs.len(), out.len());
63 debug_assert_eq!(rhs.len(), out.len());
64 for i in 0..out.len() {
65 out[i] = lhs[i] / rhs[i];
66 }
67}
68
69#[inline(always)]
70fn scalar_sqrt_lanes(inp: &[f64], out: &mut [f64]) {
71 debug_assert_eq!(inp.len(), out.len());
72 for i in 0..out.len() {
73 out[i] = inp[i].sqrt();
74 }
75}
76
77#[inline(always)]
78fn scalar_neg_lanes(inp: &[f64], out: &mut [f64]) {
79 debug_assert_eq!(inp.len(), out.len());
80 for i in 0..out.len() {
81 out[i] = -inp[i];
82 }
83}
84
85#[inline(always)]
86fn scalar_abs_lanes(inp: &[f64], out: &mut [f64]) {
87 debug_assert_eq!(inp.len(), out.len());
88 for i in 0..out.len() {
89 out[i] = inp[i].abs();
90 }
91}
92
93static HAS_AVX2: OnceLock<bool> = OnceLock::new();
97
98static HAS_NEON: OnceLock<bool> = OnceLock::new();
100
101#[inline]
106fn avx2_available() -> bool {
107 *HAS_AVX2.get_or_init(crate::simd::detect::has_avx2)
108}
109
110#[inline]
112fn neon_available() -> bool {
113 *HAS_NEON.get_or_init(crate::simd::detect::has_neon)
114}
115
116rssn_error! {
117 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
119 pub enum BatchError {
120 LengthMismatch,
122 }
123}
124
125const LANES: usize = 4;
127
128pub fn batch_add(lhs: &[f64], rhs: &[f64], result: &mut [f64]) -> Result<(), BatchError> {
139 let n = lhs.len();
140 if n != rhs.len() || n != result.len() {
141 return cold_batch_error_length_mismatch();
142 }
143
144 let use_avx2 = avx2_available();
146 let use_neon = neon_available();
147 let mut chunk_idx = 0;
148 while chunk_idx + LANES * 2 <= n {
150 let (o1, o2) = result[chunk_idx..chunk_idx + LANES * 2].split_at_mut(LANES);
151 if use_avx2 {
152 add_f64x4::apply(
153 &lhs[chunk_idx..chunk_idx + LANES],
154 &rhs[chunk_idx..chunk_idx + LANES],
155 o1,
156 );
157 add_f64x4::apply(
158 &lhs[chunk_idx + LANES..chunk_idx + LANES * 2],
159 &rhs[chunk_idx + LANES..chunk_idx + LANES * 2],
160 o2,
161 );
162 } else {
163 scalar_add_lanes(
164 &lhs[chunk_idx..chunk_idx + LANES],
165 &rhs[chunk_idx..chunk_idx + LANES],
166 o1,
167 );
168 scalar_add_lanes(
169 &lhs[chunk_idx + LANES..chunk_idx + LANES * 2],
170 &rhs[chunk_idx + LANES..chunk_idx + LANES * 2],
171 o2,
172 );
173 }
174 chunk_idx += LANES * 2;
175 }
176 while chunk_idx + LANES <= n {
178 let o = &mut result[chunk_idx..chunk_idx + LANES];
179 if use_avx2 {
180 add_f64x4::apply(
181 &lhs[chunk_idx..chunk_idx + LANES],
182 &rhs[chunk_idx..chunk_idx + LANES],
183 o,
184 );
185 } else {
186 scalar_add_lanes(
187 &lhs[chunk_idx..chunk_idx + LANES],
188 &rhs[chunk_idx..chunk_idx + LANES],
189 o,
190 );
191 }
192 chunk_idx += LANES;
193 }
194 while chunk_idx + 2 <= n && !use_avx2 && use_neon {
196 if let (Ok(o2), Ok(l2), Ok(r2)) = (
197 <&mut [f64; 2]>::try_from(&mut result[chunk_idx..chunk_idx + 2]),
198 <&[f64; 2]>::try_from(&lhs[chunk_idx..chunk_idx + 2]),
199 <&[f64; 2]>::try_from(&rhs[chunk_idx..chunk_idx + 2]),
200 ) {
201 add_f64x2::apply(l2, r2, o2);
202 }
203 chunk_idx += 2;
204 }
205 while chunk_idx < n {
206 result[chunk_idx] = lhs[chunk_idx] + rhs[chunk_idx];
207 chunk_idx += 1;
208 }
209 Ok(())
210}
211
212pub fn batch_mul(lhs: &[f64], rhs: &[f64], result: &mut [f64]) -> Result<(), BatchError> {
219 let n = lhs.len();
220 if n != rhs.len() || n != result.len() {
221 return cold_batch_error_length_mismatch();
222 }
223
224 let use_avx2 = avx2_available();
225 let use_neon = neon_available();
226 let mut i = 0;
227 while i + LANES * 2 <= n {
228 let (o1, o2) = result[i..i + LANES * 2].split_at_mut(LANES);
229 if use_avx2 {
230 mul_f64x4::apply(&lhs[i..i + LANES], &rhs[i..i + LANES], o1);
231 mul_f64x4::apply(
232 &lhs[i + LANES..i + LANES * 2],
233 &rhs[i + LANES..i + LANES * 2],
234 o2,
235 );
236 } else {
237 scalar_mul_lanes(&lhs[i..i + LANES], &rhs[i..i + LANES], o1);
238 scalar_mul_lanes(
239 &lhs[i + LANES..i + LANES * 2],
240 &rhs[i + LANES..i + LANES * 2],
241 o2,
242 );
243 }
244 i += LANES * 2;
245 }
246 while i + LANES <= n {
247 let o = &mut result[i..i + LANES];
248 if use_avx2 {
249 mul_f64x4::apply(&lhs[i..i + LANES], &rhs[i..i + LANES], o);
250 } else {
251 scalar_mul_lanes(&lhs[i..i + LANES], &rhs[i..i + LANES], o);
252 }
253 i += LANES;
254 }
255 while i + 2 <= n && !use_avx2 && use_neon {
257 if let (Ok(o2), Ok(l2), Ok(r2)) = (
258 <&mut [f64; 2]>::try_from(&mut result[i..i + 2]),
259 <&[f64; 2]>::try_from(&lhs[i..i + 2]),
260 <&[f64; 2]>::try_from(&rhs[i..i + 2]),
261 ) {
262 mul_f64x2::apply(l2, r2, o2);
263 }
264 i += 2;
265 }
266 while i < n {
267 result[i] = lhs[i] * rhs[i];
268 i += 1;
269 }
270 Ok(())
271}
272
273pub fn batch_add_scalar(lhs: &[f64], scalar: f64, result: &mut [f64]) -> Result<(), BatchError> {
280 let n = lhs.len();
281 if n != result.len() {
282 return cold_batch_error_length_mismatch();
283 }
284
285 let use_simd = avx2_available();
286 let rhs_splat = [scalar; LANES];
287 let mut i = 0;
288 while i + LANES * 2 <= n {
289 let (o1, o2) = result[i..i + LANES * 2].split_at_mut(LANES);
290 if use_simd {
291 add_f64x4::apply(&lhs[i..i + LANES], &rhs_splat, o1);
292 add_f64x4::apply(&lhs[i + LANES..i + LANES * 2], &rhs_splat, o2);
293 } else {
294 for j in 0..LANES {
295 o1[j] = lhs[i + j] + scalar;
296 }
297 for j in 0..LANES {
298 o2[j] = lhs[i + LANES + j] + scalar;
299 }
300 }
301 i += LANES * 2;
302 }
303 while i + LANES <= n {
304 let o = &mut result[i..i + LANES];
305 if use_simd {
306 add_f64x4::apply(&lhs[i..i + LANES], &rhs_splat, o);
307 } else {
308 for j in 0..LANES {
309 o[j] = lhs[i + j] + scalar;
310 }
311 }
312 i += LANES;
313 }
314 while i < n {
315 result[i] = lhs[i] + scalar;
316 i += 1;
317 }
318 Ok(())
319}
320
321pub fn batch_sub(lhs: &[f64], rhs: &[f64], result: &mut [f64]) -> Result<(), BatchError> {
332 let n = lhs.len();
333 if n != rhs.len() || n != result.len() {
334 return cold_batch_error_length_mismatch();
335 }
336
337 let use_avx2 = avx2_available();
338 let use_neon = neon_available();
339 let mut i = 0;
340 while i + LANES * 2 <= n {
341 let (o1, o2) = result[i..i + LANES * 2].split_at_mut(LANES);
342 if use_avx2 {
343 sub_f64x4::apply(&lhs[i..i + LANES], &rhs[i..i + LANES], o1);
344 sub_f64x4::apply(
345 &lhs[i + LANES..i + LANES * 2],
346 &rhs[i + LANES..i + LANES * 2],
347 o2,
348 );
349 } else {
350 scalar_sub_lanes(&lhs[i..i + LANES], &rhs[i..i + LANES], o1);
351 scalar_sub_lanes(
352 &lhs[i + LANES..i + LANES * 2],
353 &rhs[i + LANES..i + LANES * 2],
354 o2,
355 );
356 }
357 i += LANES * 2;
358 }
359 while i + LANES <= n {
360 let o = &mut result[i..i + LANES];
361 if use_avx2 {
362 sub_f64x4::apply(&lhs[i..i + LANES], &rhs[i..i + LANES], o);
363 } else {
364 scalar_sub_lanes(&lhs[i..i + LANES], &rhs[i..i + LANES], o);
365 }
366 i += LANES;
367 }
368 while i + 2 <= n && !use_avx2 && use_neon {
369 if let (Ok(o2), Ok(l2), Ok(r2)) = (
370 <&mut [f64; 2]>::try_from(&mut result[i..i + 2]),
371 <&[f64; 2]>::try_from(&lhs[i..i + 2]),
372 <&[f64; 2]>::try_from(&rhs[i..i + 2]),
373 ) {
374 sub_f64x2::apply(l2, r2, o2);
375 }
376 i += 2;
377 }
378 while i < n {
379 result[i] = lhs[i] - rhs[i];
380 i += 1;
381 }
382 Ok(())
383}
384
385pub fn batch_div(lhs: &[f64], rhs: &[f64], result: &mut [f64]) -> Result<(), BatchError> {
394 let n = lhs.len();
395 if n != rhs.len() || n != result.len() {
396 return cold_batch_error_length_mismatch();
397 }
398
399 let use_avx2 = avx2_available();
400 let use_neon = neon_available();
401 let mut i = 0;
402 while i + LANES * 2 <= n {
403 let (o1, o2) = result[i..i + LANES * 2].split_at_mut(LANES);
404 if use_avx2 {
405 div_f64x4::apply(&lhs[i..i + LANES], &rhs[i..i + LANES], o1);
406 div_f64x4::apply(
407 &lhs[i + LANES..i + LANES * 2],
408 &rhs[i + LANES..i + LANES * 2],
409 o2,
410 );
411 } else {
412 scalar_div_lanes(&lhs[i..i + LANES], &rhs[i..i + LANES], o1);
413 scalar_div_lanes(
414 &lhs[i + LANES..i + LANES * 2],
415 &rhs[i + LANES..i + LANES * 2],
416 o2,
417 );
418 }
419 i += LANES * 2;
420 }
421 while i + LANES <= n {
422 let o = &mut result[i..i + LANES];
423 if use_avx2 {
424 div_f64x4::apply(&lhs[i..i + LANES], &rhs[i..i + LANES], o);
425 } else {
426 scalar_div_lanes(&lhs[i..i + LANES], &rhs[i..i + LANES], o);
427 }
428 i += LANES;
429 }
430 while i + 2 <= n && !use_avx2 && use_neon {
431 if let (Ok(o2), Ok(l2), Ok(r2)) = (
432 <&mut [f64; 2]>::try_from(&mut result[i..i + 2]),
433 <&[f64; 2]>::try_from(&lhs[i..i + 2]),
434 <&[f64; 2]>::try_from(&rhs[i..i + 2]),
435 ) {
436 div_f64x2::apply(l2, r2, o2);
437 }
438 i += 2;
439 }
440 while i < n {
441 result[i] = lhs[i] / rhs[i];
442 i += 1;
443 }
444 Ok(())
445}
446
447pub fn batch_sqrt(inp: &[f64], result: &mut [f64]) -> Result<(), BatchError> {
460 let n = inp.len();
461 if n != result.len() {
462 return cold_batch_error_length_mismatch();
463 }
464
465 let use_avx2 = avx2_available();
466 let use_neon = neon_available();
467 let mut i = 0;
468 while i + LANES * 2 <= n {
469 let (o1, o2) = result[i..i + LANES * 2].split_at_mut(LANES);
470 if use_avx2 {
471 sqrt_f64x4::apply(&inp[i..i + LANES], o1);
472 sqrt_f64x4::apply(&inp[i + LANES..i + LANES * 2], o2);
473 } else {
474 scalar_sqrt_lanes(&inp[i..i + LANES], o1);
475 scalar_sqrt_lanes(&inp[i + LANES..i + LANES * 2], o2);
476 }
477 i += LANES * 2;
478 }
479 while i + LANES <= n {
480 let o = &mut result[i..i + LANES];
481 if use_avx2 {
482 sqrt_f64x4::apply(&inp[i..i + LANES], o);
483 } else {
484 scalar_sqrt_lanes(&inp[i..i + LANES], o);
485 }
486 i += LANES;
487 }
488 while i + 2 <= n && !use_avx2 && use_neon {
489 if let (Ok(o2), Ok(l2)) = (
490 <&mut [f64; 2]>::try_from(&mut result[i..i + 2]),
491 <&[f64; 2]>::try_from(&inp[i..i + 2]),
492 ) {
493 sqrt_f64x2::apply(l2, o2);
494 }
495 i += 2;
496 }
497 while i < n {
498 result[i] = inp[i].sqrt();
499 i += 1;
500 }
501 Ok(())
502}
503
504pub fn batch_neg(inp: &[f64], result: &mut [f64]) -> Result<(), BatchError> {
511 let n = inp.len();
512 if n != result.len() {
513 return cold_batch_error_length_mismatch();
514 }
515
516 let use_avx2 = avx2_available();
517 let use_neon = neon_available();
518 let mut i = 0;
519 while i + LANES * 2 <= n {
520 let (o1, o2) = result[i..i + LANES * 2].split_at_mut(LANES);
521 if use_avx2 {
522 neg_f64x4::apply(&inp[i..i + LANES], o1);
523 neg_f64x4::apply(&inp[i + LANES..i + LANES * 2], o2);
524 } else {
525 scalar_neg_lanes(&inp[i..i + LANES], o1);
526 scalar_neg_lanes(&inp[i + LANES..i + LANES * 2], o2);
527 }
528 i += LANES * 2;
529 }
530 while i + LANES <= n {
531 let o = &mut result[i..i + LANES];
532 if use_avx2 {
533 neg_f64x4::apply(&inp[i..i + LANES], o);
534 } else {
535 scalar_neg_lanes(&inp[i..i + LANES], o);
536 }
537 i += LANES;
538 }
539 while i + 2 <= n && !use_avx2 && use_neon {
540 if let (Ok(o2), Ok(l2)) = (
541 <&mut [f64; 2]>::try_from(&mut result[i..i + 2]),
542 <&[f64; 2]>::try_from(&inp[i..i + 2]),
543 ) {
544 neg_f64x2::apply(l2, o2);
545 }
546 i += 2;
547 }
548 while i < n {
549 result[i] = -inp[i];
550 i += 1;
551 }
552 Ok(())
553}
554
555pub fn batch_abs(inp: &[f64], result: &mut [f64]) -> Result<(), BatchError> {
562 let n = inp.len();
563 if n != result.len() {
564 return cold_batch_error_length_mismatch();
565 }
566
567 let use_avx2 = avx2_available();
568 let use_neon = neon_available();
569 let mut i = 0;
570 while i + LANES * 2 <= n {
571 let (o1, o2) = result[i..i + LANES * 2].split_at_mut(LANES);
572 if use_avx2 {
573 abs_f64x4::apply(&inp[i..i + LANES], o1);
574 abs_f64x4::apply(&inp[i + LANES..i + LANES * 2], o2);
575 } else {
576 scalar_abs_lanes(&inp[i..i + LANES], o1);
577 scalar_abs_lanes(&inp[i + LANES..i + LANES * 2], o2);
578 }
579 i += LANES * 2;
580 }
581 while i + LANES <= n {
582 let o = &mut result[i..i + LANES];
583 if use_avx2 {
584 abs_f64x4::apply(&inp[i..i + LANES], o);
585 } else {
586 scalar_abs_lanes(&inp[i..i + LANES], o);
587 }
588 i += LANES;
589 }
590 while i + 2 <= n && !use_avx2 && use_neon {
591 if let (Ok(o2), Ok(l2)) = (
592 <&mut [f64; 2]>::try_from(&mut result[i..i + 2]),
593 <&[f64; 2]>::try_from(&inp[i..i + 2]),
594 ) {
595 abs_f64x2::apply(l2, o2);
596 }
597 i += 2;
598 }
599 while i < n {
600 result[i] = inp[i].abs();
601 i += 1;
602 }
603 Ok(())
604}
605
606pub fn batch_pow(base: &[f64], exp: &[f64], result: &mut [f64]) -> Result<(), BatchError> {
622 let n = base.len();
623 if n != exp.len() || n != result.len() {
624 return cold_batch_error_length_mismatch();
625 }
626 for ((b, e), o) in base.iter().zip(exp.iter()).zip(result.iter_mut()) {
627 *o = b.powf(*e);
628 }
629 Ok(())
630}
631
632pub fn batch_cmp_eq(lhs: &[f64], rhs: &[f64], mask: &mut [u8]) -> Result<(), BatchError> {
640 let n = lhs.len();
641 if n != rhs.len() || n != mask.len() {
642 return cold_batch_error_length_mismatch();
643 }
644
645 let use_simd = avx2_available();
646 let mut i = 0;
647 while i + LANES * 2 <= n {
648 if use_simd {
649 cmp_eq_f64x4::apply(
650 &lhs[i..i + LANES],
651 &rhs[i..i + LANES],
652 &mut mask[i..i + LANES],
653 );
654 cmp_eq_f64x4::apply(
655 &lhs[i + LANES..i + LANES * 2],
656 &rhs[i + LANES..i + LANES * 2],
657 &mut mask[i + LANES..i + LANES * 2],
658 );
659 } else {
660 #[allow(clippy::float_cmp)]
661 for j in 0..LANES * 2 {
662 mask[i + j] = if lhs[i + j] == rhs[i + j] { 0xFF } else { 0x00 };
663 }
664 }
665 i += LANES * 2;
666 }
667 while i + LANES <= n {
668 if use_simd {
669 cmp_eq_f64x4::apply(
670 &lhs[i..i + LANES],
671 &rhs[i..i + LANES],
672 &mut mask[i..i + LANES],
673 );
674 } else {
675 #[allow(clippy::float_cmp)]
676 for j in 0..LANES {
677 mask[i + j] = if lhs[i + j] == rhs[i + j] { 0xFF } else { 0x00 };
678 }
679 }
680 i += LANES;
681 }
682 #[allow(clippy::float_cmp)]
683 while i < n {
684 mask[i] = if lhs[i] == rhs[i] { 0xFF } else { 0x00 };
685 i += 1;
686 }
687 Ok(())
688}
689
690pub fn batch_coef_merge(
701 coef_a: &[f64],
702 coef_b: &[f64],
703 var_x: &[f64],
704 var_y: &[f64],
705 out: &mut [f64],
706) -> Result<(), BatchError> {
707 let n = coef_a.len();
708 if n != coef_b.len() || n != var_x.len() || n != var_y.len() || n != out.len() {
709 return cold_batch_error_length_mismatch();
710 }
711
712 let use_simd = avx2_available();
713 let mut i = 0;
714 while i + LANES <= n {
715 if use_simd {
716 coef_merge_f64x4::apply(
717 &coef_a[i..i + LANES],
718 &coef_b[i..i + LANES],
719 &var_x[i..i + LANES],
720 &var_y[i..i + LANES],
721 &mut out[i..i + LANES],
722 );
723 } else {
724 for j in 0..LANES {
725 out[i + j] = (coef_a[i + j] * coef_b[i + j]) * (var_x[i + j] * var_y[i + j]);
726 }
727 }
728 i += LANES;
729 }
730 while i < n {
731 out[i] = (coef_a[i] * coef_b[i]) * (var_x[i] * var_y[i]);
732 i += 1;
733 }
734 Ok(())
735}
736
737pub fn batch_fma(
745 lhs: &[f64],
746 rhs: &[f64],
747 addend: &[f64],
748 out: &mut [f64],
749) -> Result<(), BatchError> {
750 let n = lhs.len();
751 if n != rhs.len() || n != addend.len() || n != out.len() {
752 return cold_batch_error_length_mismatch();
753 }
754
755 let use_simd = avx2_available();
756 let mut i = 0;
757 while i + LANES * 2 <= n {
758 let (o1, o2) = out[i..i + LANES * 2].split_at_mut(LANES);
759 if use_simd {
760 fma_f64x4::apply(
761 &lhs[i..i + LANES],
762 &rhs[i..i + LANES],
763 &addend[i..i + LANES],
764 o1,
765 );
766 fma_f64x4::apply(
767 &lhs[i + LANES..i + LANES * 2],
768 &rhs[i + LANES..i + LANES * 2],
769 &addend[i + LANES..i + LANES * 2],
770 o2,
771 );
772 } else {
773 for j in 0..LANES {
774 o1[j] = lhs[i + j].mul_add(rhs[i + j], addend[i + j]);
775 }
776 for j in 0..LANES {
777 o2[j] = lhs[i + LANES + j].mul_add(rhs[i + LANES + j], addend[i + LANES + j]);
778 }
779 }
780 i += LANES * 2;
781 }
782 while i + LANES <= n {
783 let o = &mut out[i..i + LANES];
784 if use_simd {
785 fma_f64x4::apply(
786 &lhs[i..i + LANES],
787 &rhs[i..i + LANES],
788 &addend[i..i + LANES],
789 o,
790 );
791 } else {
792 for j in 0..LANES {
793 o[j] = lhs[i + j].mul_add(rhs[i + j], addend[i + j]);
794 }
795 }
796 i += LANES;
797 }
798 while i < n {
799 out[i] = lhs[i].mul_add(rhs[i], addend[i]);
800 i += 1;
801 }
802 Ok(())
803}
804
805#[cfg(feature = "cranelift-jit")]
824pub fn batch_eval(
825 func: crate::jit::compiler::CompiledExprFn,
826 var_sets: &[f64],
827 num_vars: usize,
828 results: &mut [f64],
829) -> Result<(), BatchError> {
830 let n = results.len();
831 if var_sets.len() != n.saturating_mul(num_vars) {
832 return cold_batch_error_length_mismatch();
833 }
834 for (i, out) in results.iter_mut().enumerate() {
835 *out = func(var_sets[i * num_vars..].as_ptr());
836 }
837 Ok(())
838}
839
840#[cfg(not(feature = "cranelift-jit"))]
842pub fn batch_eval(
843 _func: *const (),
844 _var_sets: &[f64],
845 _num_vars: usize,
846 results: &mut [f64],
847) -> Result<(), BatchError> {
848 let _ = results;
849 cold_batch_error_length_mismatch()
850}
851
852#[cfg(feature = "cranelift-jit")]
864pub fn batch_eval_parallel(
865 func: crate::jit::compiler::CompiledExprFn,
866 var_sets: &[f64],
867 num_vars: usize,
868 results: &mut [f64],
869 chunk_size: usize,
870) -> Result<(), BatchError> {
871 let n = results.len();
872 if var_sets.len() != n.saturating_mul(num_vars) {
873 return cold_batch_error_length_mismatch();
874 }
875 if chunk_size == 0 || n <= chunk_size {
876 return batch_eval(func, var_sets, num_vars, results);
877 }
878
879 use std::cell::UnsafeCell;
880 use std::sync::Arc;
881
882 struct SendSync<T>(T);
883 unsafe impl<T> Send for SendSync<T> {}
886 unsafe impl<T> Sync for SendSync<T> {}
887
888 let gate = crate::runtime::ensure_runtime();
889 let results_ptr: Arc<SendSync<UnsafeCell<*mut f64>>> =
890 Arc::new(SendSync(UnsafeCell::new(results.as_mut_ptr())));
891 let var_ptr: *const f64 = var_sets.as_ptr();
892 let var_ptr_send: usize = var_ptr as usize;
893
894 let mut handles = Vec::new();
895 let mut offset = 0usize;
896 while offset < n {
897 let end = (offset + chunk_size).min(n);
898 let count = end - offset;
899 let out_offset = offset;
900 let results_arc = Arc::clone(&results_ptr);
901 handles.push(crate::runtime::spawn_task(gate, move || {
902 for i in 0..count {
903 let row = out_offset + i;
904 let vars = unsafe {
905 std::slice::from_raw_parts(
906 (var_ptr_send as *const f64).add(row * num_vars),
907 num_vars,
908 )
909 };
910 let val = func(vars.as_ptr());
911 unsafe {
912 let base = *results_arc.0.get();
913 *base.add(row) = val;
914 }
915 }
916 }));
917 offset = end;
918 }
919 for h in handles {
920 crate::runtime::join(h);
921 }
922 Ok(())
923}
924
925#[cfg(not(feature = "cranelift-jit"))]
927pub fn batch_eval_parallel(
928 _func: *const (),
929 _var_sets: &[f64],
930 _num_vars: usize,
931 results: &mut [f64],
932 _chunk_size: usize,
933) -> Result<(), BatchError> {
934 let _ = results;
935 cold_batch_error_length_mismatch()
936}
937
938#[cfg(test)]
939mod tests {
940 use super::*;
941
942 #[test]
943 fn batch_add_matches_scalar() {
944 let lhs: Vec<f64> = (0..27).map(|i| f64::from(i) * 0.5).collect();
945 let rhs: Vec<f64> = (0..27).map(|i| f64::from(i) * 1.5).collect();
946 let mut result = vec![0.0_f64; 27];
947 batch_add(&lhs, &rhs, &mut result).expect("ok");
948 for i in 0..27 {
949 assert!((result[i] - (lhs[i] + rhs[i])).abs() < 1e-12);
950 }
951 }
952
953 #[test]
954 fn batch_mul_handles_unaligned_tail() {
955 let lhs = [2.0_f64, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
957 let rhs = [10.0_f64; 7];
958 let mut result = [0.0_f64; 7];
959 batch_mul(&lhs, &rhs, &mut result).expect("ok");
960 assert_eq!(result, [20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0]);
961 }
962
963 #[test]
964 fn batch_add_scalar_splats_correctly() {
965 let lhs: Vec<f64> = (0..16).map(f64::from).collect();
966 let mut result = vec![0.0_f64; 16];
967 batch_add_scalar(&lhs, 1.5, &mut result).expect("ok");
968 for i in 0..16 {
969 assert!((result[i] - (f64::from(i as i32) + 1.5)).abs() < 1e-12);
970 }
971 }
972
973 #[test]
974 fn batch_pow_per_element() {
975 let base = [2.0_f64, 3.0, 4.0, 5.0, 6.0];
976 let exp = [2.0_f64, 3.0, 0.5, 0.0, 1.0];
977 let mut out = [0.0_f64; 5];
978 batch_pow(&base, &exp, &mut out).expect("ok");
979 assert!((out[0] - 4.0).abs() < 1e-12, "2^2");
980 assert!((out[1] - 27.0).abs() < 1e-12, "3^3");
981 assert!((out[2] - 2.0).abs() < 1e-12, "sqrt(4)");
983 assert_eq!(out[3], 1.0, "5^0");
984 assert_eq!(out[4], 6.0, "6^1");
985 }
986
987 #[test]
988 fn batch_cmp_eq_handles_chunks_and_tail() {
989 let lhs = [1.0_f64, 2.0, 3.0, 4.0, 5.0];
991 let rhs = [1.0_f64, 0.0, 3.0, 0.0, 5.0];
992 let mut mask = [0_u8; 5];
993 batch_cmp_eq(&lhs, &rhs, &mut mask).expect("ok");
994 assert_eq!(mask, [0xFF, 0x00, 0xFF, 0x00, 0xFF]);
995 }
996
997 #[test]
998 fn batch_coef_merge_matches_naive() {
999 let coef_a = [2.0_f64, 3.0, 4.0, 5.0, 6.0];
1000 let coef_b = [0.5_f64, 0.5, 0.5, 0.5, 0.5];
1001 let var_x = [10.0_f64, 20.0, 30.0, 40.0, 50.0];
1002 let var_y = [1.0_f64, 1.0, 1.0, 1.0, 1.0];
1003 let mut out = [0.0_f64; 5];
1004 batch_coef_merge(&coef_a, &coef_b, &var_x, &var_y, &mut out).expect("ok");
1005 for i in 0..5 {
1006 let expected = (coef_a[i] * coef_b[i]) * (var_x[i] * var_y[i]);
1007 assert!((out[i] - expected).abs() < 1e-12);
1008 }
1009 }
1010
1011 #[test]
1012 fn batch_fma_single_rounding() {
1013 let lhs = [1.0_f64, 2.0, 3.0, 4.0, 5.0];
1014 let rhs = [10.0_f64; 5];
1015 let addend = [100.0_f64; 5];
1016 let mut out = [0.0_f64; 5];
1017 batch_fma(&lhs, &rhs, &addend, &mut out).expect("ok");
1018 for i in 0..5 {
1019 assert!((out[i] - (lhs[i] * rhs[i] + addend[i])).abs() < 1e-12);
1020 }
1021 }
1022
1023 #[test]
1024 fn length_mismatch_returns_error() {
1025 let lhs = [1.0_f64, 2.0];
1026 let rhs = [1.0_f64; 3];
1027 let mut out = [0.0_f64; 2];
1028 assert_eq!(
1029 batch_add(&lhs, &rhs, &mut out),
1030 Err(BatchError::LengthMismatch)
1031 );
1032 }
1033
1034 #[test]
1035 fn batch_sub_matches_scalar() {
1036 let lhs: Vec<f64> = (0..27).map(|i| f64::from(i) * 2.0).collect();
1037 let rhs: Vec<f64> = (0..27).map(f64::from).collect();
1038 let mut result = vec![0.0_f64; 27];
1039 batch_sub(&lhs, &rhs, &mut result).expect("ok");
1040 for i in 0..27 {
1041 assert!((result[i] - f64::from(i as i32)).abs() < 1e-12);
1042 }
1043 }
1044
1045 #[test]
1046 fn batch_div_matches_scalar() {
1047 let lhs: Vec<f64> = (1..=27).map(|i| f64::from(i) * 2.0).collect();
1048 let rhs = vec![2.0_f64; 27];
1049 let mut result = vec![0.0_f64; 27];
1050 batch_div(&lhs, &rhs, &mut result).expect("ok");
1051 for i in 0..27 {
1052 let expected = f64::from((i + 1) as i32);
1053 assert!((result[i] - expected).abs() < 1e-12, "i={i}");
1054 }
1055 }
1056
1057 #[test]
1058 fn batch_sqrt_known_values() {
1059 let inp: Vec<f64> = [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0].to_vec();
1060 let expected: Vec<f64> = (0..9).map(f64::from).collect();
1061 let mut result = vec![0.0_f64; inp.len()];
1062 batch_sqrt(&inp, &mut result).expect("ok");
1063 for i in 0..inp.len() {
1064 assert!((result[i] - expected[i]).abs() < 1e-12, "i={i}");
1065 }
1066 }
1067
1068 #[test]
1069 fn batch_neg_flips_signs() {
1070 let inp: Vec<f64> = (0..19).map(|i| f64::from(i) - 9.0).collect();
1071 let mut result = vec![0.0_f64; 19];
1072 batch_neg(&inp, &mut result).expect("ok");
1073 for i in 0..19 {
1074 assert!((result[i] + inp[i]).abs() < 1e-12, "i={i}");
1075 }
1076 }
1077
1078 #[test]
1079 fn batch_abs_removes_sign() {
1080 let inp: Vec<f64> = (0..19).map(|i| f64::from(i) - 9.0).collect();
1081 let mut result = vec![0.0_f64; 19];
1082 batch_abs(&inp, &mut result).expect("ok");
1083 for i in 0..19 {
1084 assert!((result[i] - inp[i].abs()).abs() < 1e-12, "i={i}");
1085 }
1086 }
1087}