Skip to main content

tenflowers_core/ops/
optimized_binary.rs

1//! Optimized CPU binary operations for maximum performance
2//!
3//! This module provides highly optimized CPU implementations of binary operations
4//! that aim to match NumPy's performance through:
5//! - SIMD-friendly memory access patterns
6//! - Vectorized operations using rayon
7//! - Zero-copy optimizations where possible
8//! - Specialized fast paths for common cases
9
10use 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
18/// Threshold for switching to parallel processing (number of elements)
19const PARALLEL_THRESHOLD: usize = 10000;
20
21/// SIMD-friendly chunk size (should be multiple of cache line size)
22const SIMD_CHUNK_SIZE: usize = 64;
23
24/// Optimized binary operation trait with vectorized implementations
25pub trait OptimizedBinaryOp<T> {
26    fn apply(&self, a: T, b: T) -> T;
27    fn name(&self) -> &str;
28
29    /// Vectorized operation on array chunks - default implementation falls back to element-wise
30    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    /// Optimized chunk operation for Copy types (avoids cloning)
44    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    /// Check if this operation supports vectorized chunks
58    fn supports_vectorization(&self) -> bool {
59        false
60    }
61}
62
63/// Optimized addition operation
64#[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        // Use iterator for better compiler optimization
81        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        // Optimized for Copy types - no cloning needed
95        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/// Optimized multiplication operation
106#[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        // Optimized for Copy types - no cloning needed
136        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/// Optimized subtraction operation
147#[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        // Optimized for Copy types - no cloning needed
177        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/// Optimized division operation
188#[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        // Optimized for Copy types - no cloning needed
218        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
228/// Fast path for contiguous same-shape tensors (most common case)
229fn 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    // Use parallel processing for large arrays
237    if len > PARALLEL_THRESHOLD && op.supports_vectorization() {
238        // Process in parallel chunks
239        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        // Sequential vectorized processing for smaller arrays
248        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        // Fallback to element-wise
257        for i in 0..len {
258            output_data[i] = op.apply(a_data[i].clone(), b_data[i].clone());
259        }
260    }
261}
262
263/// Optimized fast path for Copy types (avoids cloning)
264fn 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    // Use parallel processing for large arrays
272    if len > PARALLEL_THRESHOLD && op.supports_vectorization() {
273        // Process in parallel chunks using optimized copy path
274        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        // Sequential vectorized processing for smaller arrays
283        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        // Fallback to element-wise - no cloning needed for Copy types
292        for i in 0..len {
293            output_data[i] = op.apply(a_data[i], b_data[i]);
294        }
295    }
296}
297
298/// Optimized binary operation with fast paths for common cases
299pub 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    // Check device compatibility
305    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            // Fast path 1: Same shape, contiguous arrays
316            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                // Use raw data slices for maximum performance
320                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            // Fast path 2: Scalar operations (broadcasting with scalar)
329            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            // General case: broadcasting required
334            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            // Broadcast arrays to common shape
339            let a_broadcast = broadcast_array(arr_a, &broadcast_shape)?;
340            let b_broadcast = broadcast_array(arr_b, &broadcast_shape)?;
341
342            // Apply operation with optimized loop
343            let mut result = ArrayD::zeros(a_broadcast.raw_dim());
344
345            // Check if we can use the fast contiguous path after broadcasting
346            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            // Fallback to ndarray's Zip for complex broadcasting cases
358            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            // For GPU tensors, fall back to the existing GPU implementation
370            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
377/// Optimized binary operation with fast paths for Copy types (avoids cloning)
378pub 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    // Check device compatibility
384    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            // Fast path 1: Same shape, contiguous arrays - use Copy optimization
395            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                // Use raw data slices for maximum performance with Copy optimization
399                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            // Fast path 2: Scalar operations (broadcasting with scalar)
408            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            // General case: broadcasting required
413            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            // Broadcast arrays to common shape
422            let a_broadcast = broadcast_array(arr_a, &broadcast_shape)?;
423            let b_broadcast = broadcast_array(arr_b, &broadcast_shape)?;
424
425            // Apply operation with optimized loop
426            let mut result = ArrayD::zeros(a_broadcast.raw_dim());
427
428            // Check if we can use the fast contiguous path after broadcasting
429            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            // Fallback to ndarray's Zip for complex broadcasting cases - no cloning for Copy
441            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            // For GPU tensors, fall back to the existing GPU implementation
453            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
460/// Optimized scalar broadcasting operation
461fn 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        // a is scalar, b is array
474        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            // Fallback for non-contiguous arrays
496            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        // b is scalar, a is array
504        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            // Fallback for non-contiguous arrays
526            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        // Both are scalars
534        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
552/// Optimized scalar broadcasting operation for Copy types (avoids cloning)
553fn 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        // a is scalar, b is array
566        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            // Fallback for non-contiguous arrays
587            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        // b is scalar, a is array
595        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            // Fallback for non-contiguous arrays
616            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        // Both are scalars
624        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
640/// Broadcast an array to a target shape (unchanged from original)
641fn broadcast_array<T: Clone>(array: &ArrayD<T>, target_shape: &Shape) -> Result<ArrayD<T>> {
642    let target_dims = IxDyn(target_shape.dims());
643
644    // If shapes match, just clone
645    if array.shape() == target_shape.dims() {
646        return Ok(array.clone());
647    }
648
649    // Convert array shape to Shape object for standardized error messages
650    let array_shape = Shape::from_slice(array.shape());
651
652    // Use ndarray's broadcast functionality
653    array
654        .broadcast(target_dims)
655        .ok_or_else(|| {
656            // Use standardized broadcast error message
657            ShapeErrorUtils::broadcast_incompatible("broadcast_cpu", &array_shape, target_shape)
658        })
659        .map(|view| view.to_owned())
660}
661
662/// Wrapper to bridge OptimizedBinaryOp to BinaryOp for GPU fallback
663#[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
683// Optimized public functions
684pub 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        // Check a few elements
751        assert_eq!(result_data[0], 0.0); // 0 * 0
752        assert_eq!(result_data[1], 2.0); // 1 * 2
753        assert_eq!(result_data[10], 200.0); // 10 * 20
754    }
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        // Time optimized version
785        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        // Time original version
790        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        // Optimized version should be faster (though this is not guaranteed in all test environments)
802        // This test serves as a performance indicator
803    }
804}