Skip to content

Commit b1e431a

Browse files
authored
Merge pull request #185 from acgetchell/perf/155-vector-dot-norm2
perf(vector): improve dot and norm2_sq throughput
2 parents df7a358 + ece54d7 commit b1e431a

1 file changed

Lines changed: 155 additions & 3 deletions

File tree

src/vector.rs

Lines changed: 155 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,14 +191,42 @@ impl<const D: usize> Vector<D> {
191191
let mut acc = 0.0;
192192
let mut i = 0;
193193
while i < D {
194+
acc = lhs[i].mul_add(rhs[i], acc);
195+
i += 1;
196+
}
197+
if acc.is_finite() {
198+
Ok(acc)
199+
} else {
200+
cold_path();
201+
Err(Self::dot_non_finite_error(lhs, rhs, operation))
202+
}
203+
}
204+
205+
/// Replay a non-finite dot product to locate the first failing step.
206+
///
207+
/// This runs only after the success-path traversal has produced a non-finite
208+
/// final accumulator. Stored entries are finite, so once a fused multiply-add
209+
/// produces a non-finite accumulator, later steps cannot make it finite again.
210+
/// Replaying the same left-to-right operations must therefore find the first
211+
/// failing index.
212+
#[cold]
213+
const fn dot_non_finite_error(
214+
lhs: &[f64; D],
215+
rhs: &[f64; D],
216+
operation: ArithmeticOperation,
217+
) -> LaError {
218+
let mut acc = 0.0;
219+
let mut i = 0;
220+
let last = D.saturating_sub(1);
221+
while i < last {
194222
acc = lhs[i].mul_add(rhs[i], acc);
195223
if !acc.is_finite() {
196-
cold_path();
197-
return Err(LaError::non_finite_computation_step(operation, i));
224+
return LaError::non_finite_computation_step(operation, i);
198225
}
199226
i += 1;
200227
}
201-
Ok(acc)
228+
229+
LaError::non_finite_computation_step(operation, last)
202230
}
203231

204232
/// Squared Euclidean norm.
@@ -425,6 +453,130 @@ mod tests {
425453
gen_vector_tests!(4);
426454
gen_vector_tests!(5);
427455

456+
macro_rules! gen_vector_replay_tests {
457+
($d:literal) => {
458+
paste! {
459+
#[test]
460+
fn [<vector_dot_and_norm2_sq_report_last_overflowing_step_ $d d>]() {
461+
let mut dot_lhs = [1.0f64; $d];
462+
dot_lhs[$d - 1] = f64::MAX;
463+
let mut dot_rhs = [1.0f64; $d];
464+
dot_rhs[$d - 1] = 2.0;
465+
let dot_lhs = Vector::<$d>::new(dot_lhs);
466+
let dot_rhs = Vector::<$d>::new(dot_rhs);
467+
468+
assert_eq!(
469+
dot_lhs.dot(&dot_rhs),
470+
Err(LaError::non_finite_computation_step(
471+
ArithmeticOperation::VectorDotProduct,
472+
$d - 1,
473+
))
474+
);
475+
476+
let mut norm_data = [1.0f64; $d];
477+
norm_data[$d - 1] = f64::MAX;
478+
let vector = Vector::<$d>::new(norm_data);
479+
480+
assert_eq!(
481+
vector.norm2_sq(),
482+
Err(LaError::non_finite_computation_step(
483+
ArithmeticOperation::VectorSquaredNorm,
484+
$d - 1,
485+
))
486+
);
487+
}
488+
}
489+
};
490+
}
491+
492+
gen_vector_replay_tests!(2);
493+
gen_vector_replay_tests!(3);
494+
gen_vector_replay_tests!(4);
495+
gen_vector_replay_tests!(5);
496+
497+
macro_rules! gen_vector_const_eval_tests {
498+
($d:literal, $dot:literal, $norm2_sq:literal) => {
499+
paste! {
500+
#[test]
501+
fn [<vector_dot_and_norm2_sq_const_eval_ $d d>]() {
502+
const DOT: Result<f64, LaError> = Vector::<$d>::new([1.0; $d])
503+
.dot(&Vector::<$d>::new([2.0; $d]));
504+
const NORM2_SQ: Result<f64, LaError> =
505+
Vector::<$d>::new([1.0; $d]).norm2_sq();
506+
507+
assert_eq!(DOT, Ok($dot));
508+
assert_eq!(NORM2_SQ, Ok($norm2_sq));
509+
}
510+
}
511+
};
512+
}
513+
514+
gen_vector_const_eval_tests!(2, 4.0, 2.0);
515+
gen_vector_const_eval_tests!(3, 6.0, 3.0);
516+
gen_vector_const_eval_tests!(4, 8.0, 4.0);
517+
gen_vector_const_eval_tests!(5, 10.0, 5.0);
518+
519+
#[test]
520+
fn vector_dot_and_norm2_sq_overflow_const_eval() {
521+
const DOT: Result<f64, LaError> =
522+
Vector::<2>::new([f64::MAX; 2]).dot(&Vector::<2>::new([1.0; 2]));
523+
const NORM2_SQ: Result<f64, LaError> = Vector::<2>::new([f64::MAX; 2]).norm2_sq();
524+
525+
assert_eq!(
526+
DOT,
527+
Err(LaError::non_finite_computation_step(
528+
ArithmeticOperation::VectorDotProduct,
529+
1,
530+
))
531+
);
532+
assert_eq!(
533+
NORM2_SQ,
534+
Err(LaError::non_finite_computation_step(
535+
ArithmeticOperation::VectorSquaredNorm,
536+
0,
537+
))
538+
);
539+
}
540+
541+
#[test]
542+
fn vector_dot_and_norm2_sq_preserve_fma_and_left_to_right_order() {
543+
let dot_large = 9_007_199_254_740_992.0;
544+
let dot_lhs = Vector::<4>::new([dot_large, 1.0, 1.0, 1.0]);
545+
let dot_rhs = Vector::<4>::new([1.0; 4]);
546+
assert_eq!(dot_lhs.dot(&dot_rhs), Ok(dot_large));
547+
548+
let fused_lhs = Vector::<2>::new([f64::MAX, f64::MAX]);
549+
let fused_rhs = Vector::<2>::new([-1.0, 2.0]);
550+
assert_eq!(fused_lhs.dot(&fused_rhs), Ok(f64::MAX));
551+
552+
let norm_large = 134_217_728.0;
553+
let vector = Vector::<4>::new([norm_large, 1.0, 1.0, 1.0]);
554+
assert_eq!(vector.norm2_sq(), Ok(norm_large * norm_large));
555+
}
556+
557+
#[test]
558+
fn vector_dot_and_norm2_sq_report_first_middle_overflowing_step() {
559+
let dot_lhs = Vector::<3>::new([f64::MAX, f64::MAX, 1.0]);
560+
let dot_rhs = Vector::<3>::new([1.0; 3]);
561+
assert_eq!(
562+
dot_lhs.dot(&dot_rhs),
563+
Err(LaError::non_finite_computation_step(
564+
ArithmeticOperation::VectorDotProduct,
565+
1,
566+
))
567+
);
568+
569+
let norm_large = 1.0e154;
570+
let vector = Vector::<3>::new([norm_large, norm_large, 1.0]);
571+
assert_eq!(
572+
vector.norm2_sq(),
573+
Err(LaError::non_finite_computation_step(
574+
ArithmeticOperation::VectorSquaredNorm,
575+
1,
576+
))
577+
);
578+
}
579+
428580
#[test]
429581
fn zero_dimension_vector_has_zero_dot_and_norm() {
430582
let vector = Vector::<0>::try_new([]).unwrap();

0 commit comments

Comments
 (0)