1use crate::shape_error_taxonomy::ShapeErrorUtils;
11use crate::tensor::TensorStorage;
12use crate::{Result, Shape, Tensor, TensorError};
13use rayon::prelude::*;
14use scirs2_core::ndarray::{ArrayD, IxDyn, Zip};
15use scirs2_core::numeric::Zero;
16use std::ops::{Add as StdAdd, Div as StdDiv, Mul as StdMul, Sub as StdSub};
17
18const PARALLEL_THRESHOLD: usize = 10000;
20
21const SIMD_CHUNK_SIZE: usize = 64;
23
24pub trait OptimizedBinaryOp<T> {
26 fn apply(&self, a: T, b: T) -> T;
27 fn name(&self) -> &str;
28
29 fn apply_chunk(&self, a_chunk: &[T], b_chunk: &[T], output_chunk: &mut [T])
31 where
32 T: Clone,
33 {
34 for ((a_val, b_val), out) in a_chunk
35 .iter()
36 .zip(b_chunk.iter())
37 .zip(output_chunk.iter_mut())
38 {
39 *out = self.apply(a_val.clone(), b_val.clone());
40 }
41 }
42
43 fn apply_chunk_copy(&self, a_chunk: &[T], b_chunk: &[T], output_chunk: &mut [T])
45 where
46 T: Copy,
47 {
48 for ((a_val, b_val), out) in a_chunk
49 .iter()
50 .zip(b_chunk.iter())
51 .zip(output_chunk.iter_mut())
52 {
53 *out = self.apply(*a_val, *b_val);
54 }
55 }
56
57 fn supports_vectorization(&self) -> bool {
59 false
60 }
61}
62
63#[derive(Clone)]
65pub struct OptimizedAddOp;
66
67impl<T: StdAdd<Output = T> + Clone> OptimizedBinaryOp<T> for OptimizedAddOp {
68 fn apply(&self, a: T, b: T) -> T {
69 a + b
70 }
71 fn name(&self) -> &str {
72 "Add"
73 }
74
75 fn supports_vectorization(&self) -> bool {
76 true
77 }
78
79 fn apply_chunk(&self, a_chunk: &[T], b_chunk: &[T], output_chunk: &mut [T]) {
80 a_chunk
82 .iter()
83 .zip(b_chunk.iter())
84 .zip(output_chunk.iter_mut())
85 .for_each(|((a, b), out)| {
86 *out = a.clone() + b.clone();
87 });
88 }
89
90 fn apply_chunk_copy(&self, a_chunk: &[T], b_chunk: &[T], output_chunk: &mut [T])
91 where
92 T: Copy,
93 {
94 a_chunk
96 .iter()
97 .zip(b_chunk.iter())
98 .zip(output_chunk.iter_mut())
99 .for_each(|((a, b), out)| {
100 *out = *a + *b;
101 });
102 }
103}
104
105#[derive(Clone)]
107pub struct OptimizedMulOp;
108
109impl<T: StdMul<Output = T> + Clone> OptimizedBinaryOp<T> for OptimizedMulOp {
110 fn apply(&self, a: T, b: T) -> T {
111 a * b
112 }
113 fn name(&self) -> &str {
114 "Mul"
115 }
116
117 fn supports_vectorization(&self) -> bool {
118 true
119 }
120
121 fn apply_chunk(&self, a_chunk: &[T], b_chunk: &[T], output_chunk: &mut [T]) {
122 a_chunk
123 .iter()
124 .zip(b_chunk.iter())
125 .zip(output_chunk.iter_mut())
126 .for_each(|((a, b), out)| {
127 *out = a.clone() * b.clone();
128 });
129 }
130
131 fn apply_chunk_copy(&self, a_chunk: &[T], b_chunk: &[T], output_chunk: &mut [T])
132 where
133 T: Copy,
134 {
135 a_chunk
137 .iter()
138 .zip(b_chunk.iter())
139 .zip(output_chunk.iter_mut())
140 .for_each(|((a, b), out)| {
141 *out = *a * *b;
142 });
143 }
144}
145
146#[derive(Clone)]
148pub struct OptimizedSubOp;
149
150impl<T: StdSub<Output = T> + Clone> OptimizedBinaryOp<T> for OptimizedSubOp {
151 fn apply(&self, a: T, b: T) -> T {
152 a - b
153 }
154 fn name(&self) -> &str {
155 "Sub"
156 }
157
158 fn supports_vectorization(&self) -> bool {
159 true
160 }
161
162 fn apply_chunk(&self, a_chunk: &[T], b_chunk: &[T], output_chunk: &mut [T]) {
163 a_chunk
164 .iter()
165 .zip(b_chunk.iter())
166 .zip(output_chunk.iter_mut())
167 .for_each(|((a, b), out)| {
168 *out = a.clone() - b.clone();
169 });
170 }
171
172 fn apply_chunk_copy(&self, a_chunk: &[T], b_chunk: &[T], output_chunk: &mut [T])
173 where
174 T: Copy,
175 {
176 a_chunk
178 .iter()
179 .zip(b_chunk.iter())
180 .zip(output_chunk.iter_mut())
181 .for_each(|((a, b), out)| {
182 *out = *a - *b;
183 });
184 }
185}
186
187#[derive(Clone)]
189pub struct OptimizedDivOp;
190
191impl<T: StdDiv<Output = T> + Clone> OptimizedBinaryOp<T> for OptimizedDivOp {
192 fn apply(&self, a: T, b: T) -> T {
193 a / b
194 }
195 fn name(&self) -> &str {
196 "Div"
197 }
198
199 fn supports_vectorization(&self) -> bool {
200 true
201 }
202
203 fn apply_chunk(&self, a_chunk: &[T], b_chunk: &[T], output_chunk: &mut [T]) {
204 a_chunk
205 .iter()
206 .zip(b_chunk.iter())
207 .zip(output_chunk.iter_mut())
208 .for_each(|((a, b), out)| {
209 *out = a.clone() / b.clone();
210 });
211 }
212
213 fn apply_chunk_copy(&self, a_chunk: &[T], b_chunk: &[T], output_chunk: &mut [T])
214 where
215 T: Copy,
216 {
217 a_chunk
219 .iter()
220 .zip(b_chunk.iter())
221 .zip(output_chunk.iter_mut())
222 .for_each(|((a, b), out)| {
223 *out = *a / *b;
224 });
225 }
226}
227
228fn fast_binary_op_contiguous<T, Op>(a_data: &[T], b_data: &[T], output_data: &mut [T], op: &Op)
230where
231 T: Clone + Send + Sync,
232 Op: OptimizedBinaryOp<T> + Sync,
233{
234 let len = a_data.len();
235
236 if len > PARALLEL_THRESHOLD && op.supports_vectorization() {
238 output_data
240 .par_chunks_mut(SIMD_CHUNK_SIZE)
241 .zip(a_data.par_chunks(SIMD_CHUNK_SIZE))
242 .zip(b_data.par_chunks(SIMD_CHUNK_SIZE))
243 .for_each(|((out_chunk, a_chunk), b_chunk)| {
244 op.apply_chunk(a_chunk, b_chunk, out_chunk);
245 });
246 } else if op.supports_vectorization() {
247 for ((out_chunk, a_chunk), b_chunk) in output_data
249 .chunks_mut(SIMD_CHUNK_SIZE)
250 .zip(a_data.chunks(SIMD_CHUNK_SIZE))
251 .zip(b_data.chunks(SIMD_CHUNK_SIZE))
252 {
253 op.apply_chunk(a_chunk, b_chunk, out_chunk);
254 }
255 } else {
256 for i in 0..len {
258 output_data[i] = op.apply(a_data[i].clone(), b_data[i].clone());
259 }
260 }
261}
262
263fn fast_binary_op_contiguous_copy<T, Op>(a_data: &[T], b_data: &[T], output_data: &mut [T], op: &Op)
265where
266 T: Copy + Send + Sync,
267 Op: OptimizedBinaryOp<T> + Sync,
268{
269 let len = a_data.len();
270
271 if len > PARALLEL_THRESHOLD && op.supports_vectorization() {
273 output_data
275 .par_chunks_mut(SIMD_CHUNK_SIZE)
276 .zip(a_data.par_chunks(SIMD_CHUNK_SIZE))
277 .zip(b_data.par_chunks(SIMD_CHUNK_SIZE))
278 .for_each(|((out_chunk, a_chunk), b_chunk)| {
279 op.apply_chunk_copy(a_chunk, b_chunk, out_chunk);
280 });
281 } else if op.supports_vectorization() {
282 for ((out_chunk, a_chunk), b_chunk) in output_data
284 .chunks_mut(SIMD_CHUNK_SIZE)
285 .zip(a_data.chunks(SIMD_CHUNK_SIZE))
286 .zip(b_data.chunks(SIMD_CHUNK_SIZE))
287 {
288 op.apply_chunk_copy(a_chunk, b_chunk, out_chunk);
289 }
290 } else {
291 for i in 0..len {
293 output_data[i] = op.apply(a_data[i], b_data[i]);
294 }
295 }
296}
297
298pub fn optimized_binary_op<T, Op>(a: &Tensor<T>, b: &Tensor<T>, op: Op) -> Result<Tensor<T>>
300where
301 T: Clone + Default + Zero + Send + Sync + 'static + bytemuck::Pod + bytemuck::Zeroable,
302 Op: OptimizedBinaryOp<T> + Sync,
303{
304 if a.device() != b.device() {
306 return Err(TensorError::device_mismatch(
307 "optimized_binary_op",
308 &a.device().to_string(),
309 &b.device().to_string(),
310 ));
311 }
312
313 match (&a.storage, &b.storage) {
314 (TensorStorage::Cpu(arr_a), TensorStorage::Cpu(arr_b)) => {
315 if a.shape() == b.shape() && arr_a.is_standard_layout() && arr_b.is_standard_layout() {
317 let mut result = ArrayD::zeros(arr_a.raw_dim());
318
319 if let (Some(a_slice), Some(b_slice), Some(out_slice)) =
321 (arr_a.as_slice(), arr_b.as_slice(), result.as_slice_mut())
322 {
323 fast_binary_op_contiguous(a_slice, b_slice, out_slice, &op);
324 return Ok(Tensor::from_array(result));
325 }
326 }
327
328 if a.shape().size() == 1 || b.shape().size() == 1 {
330 return scalar_broadcast_op(arr_a, arr_b, a.shape(), b.shape(), op);
331 }
332
333 let broadcast_shape = a.shape().broadcast_shape(b.shape()).ok_or_else(|| {
335 ShapeErrorUtils::broadcast_incompatible("optimized_binary_op", a.shape(), b.shape())
336 })?;
337
338 let a_broadcast = broadcast_array(arr_a, &broadcast_shape)?;
340 let b_broadcast = broadcast_array(arr_b, &broadcast_shape)?;
341
342 let mut result = ArrayD::zeros(a_broadcast.raw_dim());
344
345 if a_broadcast.is_standard_layout() && b_broadcast.is_standard_layout() {
347 if let (Some(a_slice), Some(b_slice), Some(out_slice)) = (
348 a_broadcast.as_slice(),
349 b_broadcast.as_slice(),
350 result.as_slice_mut(),
351 ) {
352 fast_binary_op_contiguous(a_slice, b_slice, out_slice, &op);
353 return Ok(Tensor::from_array(result));
354 }
355 }
356
357 Zip::from(&mut result)
359 .and(&a_broadcast)
360 .and(&b_broadcast)
361 .for_each(|r, a_val, b_val| {
362 *r = op.apply(*a_val, *b_val);
363 });
364
365 Ok(Tensor::from_array(result))
366 }
367 #[cfg(feature = "gpu")]
368 (TensorStorage::Gpu(_), TensorStorage::Gpu(_)) => {
369 super::binary::binary_op(a, b, GPUOpWrapper { op })
371 }
372 #[allow(unreachable_patterns)]
373 _ => unreachable!("Device mismatch should have been caught earlier"),
374 }
375}
376
377pub fn optimized_binary_op_copy<T, Op>(a: &Tensor<T>, b: &Tensor<T>, op: Op) -> Result<Tensor<T>>
379where
380 T: Copy + Default + Zero + Send + Sync + 'static + bytemuck::Pod + bytemuck::Zeroable,
381 Op: OptimizedBinaryOp<T> + Sync,
382{
383 if a.device() != b.device() {
385 return Err(TensorError::device_mismatch(
386 "optimized_binary_op_copy",
387 &a.device().to_string(),
388 &b.device().to_string(),
389 ));
390 }
391
392 match (&a.storage, &b.storage) {
393 (TensorStorage::Cpu(arr_a), TensorStorage::Cpu(arr_b)) => {
394 if a.shape() == b.shape() && arr_a.is_standard_layout() && arr_b.is_standard_layout() {
396 let mut result = ArrayD::zeros(arr_a.raw_dim());
397
398 if let (Some(a_slice), Some(b_slice), Some(out_slice)) =
400 (arr_a.as_slice(), arr_b.as_slice(), result.as_slice_mut())
401 {
402 fast_binary_op_contiguous_copy(a_slice, b_slice, out_slice, &op);
403 return Ok(Tensor::from_array(result));
404 }
405 }
406
407 if a.shape().size() == 1 || b.shape().size() == 1 {
409 return scalar_broadcast_op_copy(arr_a, arr_b, a.shape(), b.shape(), op);
410 }
411
412 let broadcast_shape = a.shape().broadcast_shape(b.shape()).ok_or_else(|| {
414 ShapeErrorUtils::broadcast_incompatible(
415 "optimized_binary_op_copy",
416 a.shape(),
417 b.shape(),
418 )
419 })?;
420
421 let a_broadcast = broadcast_array(arr_a, &broadcast_shape)?;
423 let b_broadcast = broadcast_array(arr_b, &broadcast_shape)?;
424
425 let mut result = ArrayD::zeros(a_broadcast.raw_dim());
427
428 if a_broadcast.is_standard_layout() && b_broadcast.is_standard_layout() {
430 if let (Some(a_slice), Some(b_slice), Some(out_slice)) = (
431 a_broadcast.as_slice(),
432 b_broadcast.as_slice(),
433 result.as_slice_mut(),
434 ) {
435 fast_binary_op_contiguous_copy(a_slice, b_slice, out_slice, &op);
436 return Ok(Tensor::from_array(result));
437 }
438 }
439
440 Zip::from(&mut result)
442 .and(&a_broadcast)
443 .and(&b_broadcast)
444 .for_each(|r, a_val, b_val| {
445 *r = op.apply(*a_val, *b_val);
446 });
447
448 Ok(Tensor::from_array(result))
449 }
450 #[cfg(feature = "gpu")]
451 (TensorStorage::Gpu(_), TensorStorage::Gpu(_)) => {
452 super::binary::binary_op(a, b, GPUOpWrapper { op })
454 }
455 #[allow(unreachable_patterns)]
456 _ => unreachable!("Device mismatch should have been caught earlier"),
457 }
458}
459
460fn scalar_broadcast_op<T, Op>(
462 arr_a: &ArrayD<T>,
463 arr_b: &ArrayD<T>,
464 shape_a: &Shape,
465 shape_b: &Shape,
466 op: Op,
467) -> Result<Tensor<T>>
468where
469 T: Clone + Default + Zero + Send + Sync + 'static,
470 Op: OptimizedBinaryOp<T> + Sync,
471{
472 if shape_a.size() == 1 && shape_b.size() > 1 {
473 let scalar_val = &arr_a
475 .iter()
476 .next()
477 .expect("scalar tensor must have at least one element")
478 .clone();
479 let mut result = ArrayD::zeros(arr_b.raw_dim());
480
481 if let (Some(b_slice), Some(out_slice)) = (arr_b.as_slice(), result.as_slice_mut()) {
482 if b_slice.len() > PARALLEL_THRESHOLD {
483 out_slice
484 .par_iter_mut()
485 .zip(b_slice.par_iter())
486 .for_each(|(out, b_val)| {
487 *out = op.apply(scalar_val.clone(), b_val.clone());
488 });
489 } else {
490 for (out, b_val) in out_slice.iter_mut().zip(b_slice.iter()) {
491 *out = op.apply(scalar_val.clone(), b_val.clone());
492 }
493 }
494 } else {
495 Zip::from(&mut result).and(arr_b).for_each(|r, b_val| {
497 *r = op.apply(scalar_val.clone(), b_val.clone());
498 });
499 }
500
501 Ok(Tensor::from_array(result))
502 } else if shape_b.size() == 1 && shape_a.size() > 1 {
503 let scalar_val = &arr_b
505 .iter()
506 .next()
507 .expect("scalar tensor must have at least one element")
508 .clone();
509 let mut result = ArrayD::zeros(arr_a.raw_dim());
510
511 if let (Some(a_slice), Some(out_slice)) = (arr_a.as_slice(), result.as_slice_mut()) {
512 if a_slice.len() > PARALLEL_THRESHOLD {
513 out_slice
514 .par_iter_mut()
515 .zip(a_slice.par_iter())
516 .for_each(|(out, a_val)| {
517 *out = op.apply(a_val.clone(), scalar_val.clone());
518 });
519 } else {
520 for (out, a_val) in out_slice.iter_mut().zip(a_slice.iter()) {
521 *out = op.apply(a_val.clone(), scalar_val.clone());
522 }
523 }
524 } else {
525 Zip::from(&mut result).and(arr_a).for_each(|r, a_val| {
527 *r = op.apply(a_val.clone(), scalar_val.clone());
528 });
529 }
530
531 Ok(Tensor::from_array(result))
532 } else {
533 let a_val = arr_a
535 .iter()
536 .next()
537 .expect("scalar tensor must have at least one element")
538 .clone();
539 let b_val = arr_b
540 .iter()
541 .next()
542 .expect("scalar tensor must have at least one element")
543 .clone();
544 let result_val = op.apply(a_val, b_val);
545 Ok(Tensor::from_array(ArrayD::from_elem(
546 IxDyn(&[1]),
547 result_val,
548 )))
549 }
550}
551
552fn scalar_broadcast_op_copy<T, Op>(
554 arr_a: &ArrayD<T>,
555 arr_b: &ArrayD<T>,
556 shape_a: &Shape,
557 shape_b: &Shape,
558 op: Op,
559) -> Result<Tensor<T>>
560where
561 T: Copy + Default + Zero + Send + Sync + 'static,
562 Op: OptimizedBinaryOp<T> + Sync,
563{
564 if shape_a.size() == 1 && shape_b.size() > 1 {
565 let scalar_val = *arr_a
567 .iter()
568 .next()
569 .expect("scalar tensor must have at least one element");
570 let mut result = ArrayD::zeros(arr_b.raw_dim());
571
572 if let (Some(b_slice), Some(out_slice)) = (arr_b.as_slice(), result.as_slice_mut()) {
573 if b_slice.len() > PARALLEL_THRESHOLD {
574 out_slice
575 .par_iter_mut()
576 .zip(b_slice.par_iter())
577 .for_each(|(out, b_val)| {
578 *out = op.apply(scalar_val, *b_val);
579 });
580 } else {
581 for (out, b_val) in out_slice.iter_mut().zip(b_slice.iter()) {
582 *out = op.apply(scalar_val, *b_val);
583 }
584 }
585 } else {
586 Zip::from(&mut result).and(arr_b).for_each(|r, b_val| {
588 *r = op.apply(scalar_val, *b_val);
589 });
590 }
591
592 Ok(Tensor::from_array(result))
593 } else if shape_b.size() == 1 && shape_a.size() > 1 {
594 let scalar_val = *arr_b
596 .iter()
597 .next()
598 .expect("scalar tensor must have at least one element");
599 let mut result = ArrayD::zeros(arr_a.raw_dim());
600
601 if let (Some(a_slice), Some(out_slice)) = (arr_a.as_slice(), result.as_slice_mut()) {
602 if a_slice.len() > PARALLEL_THRESHOLD {
603 out_slice
604 .par_iter_mut()
605 .zip(a_slice.par_iter())
606 .for_each(|(out, a_val)| {
607 *out = op.apply(*a_val, scalar_val);
608 });
609 } else {
610 for (out, a_val) in out_slice.iter_mut().zip(a_slice.iter()) {
611 *out = op.apply(*a_val, scalar_val);
612 }
613 }
614 } else {
615 Zip::from(&mut result).and(arr_a).for_each(|r, a_val| {
617 *r = op.apply(*a_val, scalar_val);
618 });
619 }
620
621 Ok(Tensor::from_array(result))
622 } else {
623 let a_val = *arr_a
625 .iter()
626 .next()
627 .expect("scalar tensor must have at least one element");
628 let b_val = *arr_b
629 .iter()
630 .next()
631 .expect("scalar tensor must have at least one element");
632 let result_val = op.apply(a_val, b_val);
633 Ok(Tensor::from_array(ArrayD::from_elem(
634 IxDyn(&[1]),
635 result_val,
636 )))
637 }
638}
639
640fn broadcast_array<T: Clone>(array: &ArrayD<T>, target_shape: &Shape) -> Result<ArrayD<T>> {
642 let target_dims = IxDyn(target_shape.dims());
643
644 if array.shape() == target_shape.dims() {
646 return Ok(array.clone());
647 }
648
649 let array_shape = Shape::from_slice(array.shape());
651
652 array
654 .broadcast(target_dims)
655 .ok_or_else(|| {
656 ShapeErrorUtils::broadcast_incompatible("broadcast_cpu", &array_shape, target_shape)
658 })
659 .map(|view| view.to_owned())
660}
661
662#[cfg(feature = "gpu")]
664struct GPUOpWrapper<Op> {
665 op: Op,
666}
667
668#[cfg(feature = "gpu")]
669impl<T, Op> super::binary::BinaryOp<T> for GPUOpWrapper<Op>
670where
671 Op: OptimizedBinaryOp<T>,
672 T: Clone,
673{
674 fn apply(&self, a: T, b: T) -> T {
675 self.op.apply(a, b)
676 }
677
678 fn name(&self) -> &str {
679 self.op.name()
680 }
681}
682
683pub fn optimized_add<T>(a: &Tensor<T>, b: &Tensor<T>) -> Result<Tensor<T>>
685where
686 T: Clone + Default + Zero + StdAdd<Output = T> + Send + Sync + 'static + bytemuck::Pod,
687{
688 optimized_binary_op(a, b, OptimizedAddOp)
689}
690
691pub fn optimized_sub<T>(a: &Tensor<T>, b: &Tensor<T>) -> Result<Tensor<T>>
692where
693 T: Clone + Default + Zero + StdSub<Output = T> + Send + Sync + 'static + bytemuck::Pod,
694{
695 optimized_binary_op(a, b, OptimizedSubOp)
696}
697
698pub fn optimized_mul<T>(a: &Tensor<T>, b: &Tensor<T>) -> Result<Tensor<T>>
699where
700 T: Clone + Default + Zero + StdMul<Output = T> + Send + Sync + 'static + bytemuck::Pod,
701{
702 optimized_binary_op(a, b, OptimizedMulOp)
703}
704
705pub fn optimized_div<T>(a: &Tensor<T>, b: &Tensor<T>) -> Result<Tensor<T>>
706where
707 T: Clone + Default + Zero + StdDiv<Output = T> + Send + Sync + 'static + bytemuck::Pod,
708{
709 optimized_binary_op(a, b, OptimizedDivOp)
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715 use crate::ops::binary;
716 use crate::Tensor;
717
718 #[test]
719 fn test_optimized_add_contiguous() {
720 let a = Tensor::from_vec(vec![1.0f32, 2.0, 3.0, 4.0], &[4])
721 .expect("test: from_vec should succeed");
722 let b = Tensor::from_vec(vec![5.0f32, 6.0, 7.0, 8.0], &[4])
723 .expect("test: from_vec should succeed");
724
725 let result = optimized_add(&a, &b).expect("test: optimized_add should succeed");
726 let expected = vec![6.0f32, 8.0, 10.0, 12.0];
727
728 assert_eq!(
729 result
730 .to_vec()
731 .expect("test: tensor data should be convertible to vec"),
732 expected
733 );
734 }
735
736 #[test]
737 fn test_optimized_mul_large_array() {
738 let size = PARALLEL_THRESHOLD + 100;
739 let a_data: Vec<f32> = (0..size).map(|i| i as f32).collect();
740 let b_data: Vec<f32> = (0..size).map(|i| (i as f32) * 2.0).collect();
741
742 let a = Tensor::from_vec(a_data.clone(), &[size]).expect("test: operation should succeed");
743 let b = Tensor::from_vec(b_data.clone(), &[size]).expect("test: operation should succeed");
744
745 let result = optimized_mul(&a, &b).expect("test: optimized_mul should succeed");
746 let result_data = result
747 .to_vec()
748 .expect("test: tensor data should be convertible to vec");
749
750 assert_eq!(result_data[0], 0.0); assert_eq!(result_data[1], 2.0); assert_eq!(result_data[10], 200.0); }
755
756 #[test]
757 fn test_scalar_broadcast() {
758 let a = Tensor::from_vec(vec![2.0f32], &[1]).expect("test: from_vec should succeed");
759 let b =
760 Tensor::from_vec(vec![1.0f32, 2.0, 3.0], &[3]).expect("test: from_vec should succeed");
761
762 let result = optimized_add(&a, &b).expect("test: optimized_add should succeed");
763 let expected = vec![3.0f32, 4.0, 5.0];
764
765 assert_eq!(
766 result
767 .to_vec()
768 .expect("test: tensor data should be convertible to vec"),
769 expected
770 );
771 }
772
773 #[test]
774 fn test_performance_benchmark() {
775 use std::time::Instant;
776
777 let size = 1_000_000;
778 let a_data: Vec<f32> = (0..size).map(|i| i as f32).collect();
779 let b_data: Vec<f32> = (0..size).map(|i| (i as f32) + 1.0).collect();
780
781 let a = Tensor::from_vec(a_data, &[size]).expect("test: from_vec should succeed");
782 let b = Tensor::from_vec(b_data, &[size]).expect("test: from_vec should succeed");
783
784 let start = Instant::now();
786 let _result = optimized_add(&a, &b).expect("test: optimized_add should succeed");
787 let optimized_time = start.elapsed();
788
789 let start = Instant::now();
791 let _result = binary::add(&a, &b).expect("test: add should succeed");
792 let original_time = start.elapsed();
793
794 println!("Original time: {:?}", original_time);
795 println!("Optimized time: {:?}", optimized_time);
796 println!(
797 "Speedup: {:.2}x",
798 original_time.as_nanos() as f64 / optimized_time.as_nanos() as f64
799 );
800
801 }
804}