Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion library/core/src/num/complex.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::ops::{Add, Neg, Sub};
use crate::num::imp::libm::complex::*;
use crate::ops::{Add, Div, Mul, Neg, Sub};

/// A complex number.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -91,3 +92,54 @@ impl<T: Sub<Output = T>> Sub<T> for Complex<T> {
Complex::new(self.re - rhs, self.im)
}
}

macro_rules! impl_complex_mul_div {
($ty:ty, $mul:ident, $div:ident) => {
#[unstable(feature = "complex_numbers", issue = "154023")]
impl Mul for Complex<$ty> {
type Output = Self;

#[inline]
fn mul(self, rhs: Self) -> Self::Output {
let Complex { re: a, im: b } = self;
let Complex { re: c, im: d } = rhs;

let ac = a * c;
let bd = b * d;
let ad = a * d;
let bc = b * c;

let z = Complex::new(ac - bd, ad + bc);

// Only call the libcall when both components are NaN.
//
// The naive algorithm would return NaN + NaNi for an input like
// (1 + 0i) * (inf + infi). The libcall instead returns inf + infi.
//
// We duplicate the fast path here so that it can be inlined. We use a libcall
// for the NaN correction to reduce the size of `core`.
if z.re.is_nan() && z.im.is_nan() {
crate::hint::cold_path();
$mul(a, b, c, d)
} else {
z
}
}
Comment on lines +102 to +127

@tgross35 tgross35 Sep 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says pretty much the same thing as the code, it would be helpful to explain why. Also mild preference for cold_path over unlikely since it's stable.

I think the optimization may pay off but it's unfortunate it means repeated work. Not too bad for hard floats but this means e.g. 12 additional function calls if you hit a NaN with f16. I wonder if there's a case to be made that these (and possibly other) libcalls should be preserve_most or preserve_all since stashing the registers is likely the most expensive part of a call.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote something, and now usecold_path. I'm not sure how much it helps given that the branch is small, but it also serves as documentation that this is an unlikely branch.

And, this ABI etc was designed way before f16 entered the picture, I don't think this will matter much in practice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did mean more of a comment about the optimization going on here, i.e. why are we doing some things inline rather than going directly to the libcall

}

#[unstable(feature = "complex_numbers", issue = "154023")]
impl Div for Complex<$ty> {
type Output = Self;

#[inline]
fn div(self, rhs: Self) -> Self::Output {
$div(self.re, self.im, rhs.re, rhs.im)
}
}
};
}

impl_complex_mul_div!(f16, __rust_mulhc3, __rust_divhc3);
impl_complex_mul_div!(f32, __mulsc3, __divsc3);
impl_complex_mul_div!(f64, __muldc3, __divdc3);
impl_complex_mul_div!(f128, __rust_multc3, __rust_divtc3);
21 changes: 21 additions & 0 deletions library/core/src/num/imp/libm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,27 @@ unsafe extern "C" {
pub(crate) safe fn truncf16(x: f16) -> f16;
}

/// These symbols are always provided by compiler-builtins.
pub(crate) mod complex {
use crate::num::Complex;

unsafe extern "C" {
pub(crate) safe fn __mulsc3(a: f32, b: f32, c: f32, d: f32) -> Complex<f32>;
pub(crate) safe fn __muldc3(a: f64, b: f64, c: f64, d: f64) -> Complex<f64>;

pub(crate) safe fn __divsc3(a: f32, b: f32, c: f32, d: f32) -> Complex<f32>;
pub(crate) safe fn __divdc3(a: f64, b: f64, c: f64, d: f64) -> Complex<f64>;
}

unsafe extern "Rust" {
pub(crate) safe fn __rust_mulhc3(a: f16, b: f16, c: f16, d: f16) -> Complex<f16>;
pub(crate) safe fn __rust_multc3(a: f128, b: f128, c: f128, d: f128) -> Complex<f128>;

pub(crate) safe fn __rust_divhc3(a: f16, b: f16, c: f16, d: f16) -> Complex<f16>;
pub(crate) safe fn __rust_divtc3(a: f128, b: f128, c: f128, d: f128) -> Complex<f128>;
}
}

/// These symbols will be available when `std` is available, and on many no-std platforms. However,
/// since this isn't a guarantee, we cannot rely on them for stable implementations.
pub(crate) mod likely_available {
Expand Down
60 changes: 60 additions & 0 deletions library/coretests/tests/num/complex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,63 @@ fn complex_negation() {
assert_eq!(-Complex::new(1.0, -2.0), Complex::new(-1.0, 2.0));
assert_eq!(-Complex::new(1.0, f32::INFINITY), Complex::new(-1.0, f32::NEG_INFINITY),);
}

#[test]
fn complex_multiplication() {
#[cfg(target_has_reliable_f16)]
assert_eq!(Complex::new(1.0f16, 2.0) * Complex::new(3.0, 4.0), Complex::new(-5.0, 10.0));
assert_eq!(Complex::new(1.0f32, 2.0) * Complex::new(3.0, 4.0), Complex::new(-5.0, 10.0));
assert_eq!(Complex::new(1.0f64, 2.0) * Complex::new(3.0, 4.0), Complex::new(-5.0, 10.0));
#[cfg(target_has_reliable_f128)]
assert_eq!(Complex::new(1.0f128, 2.0) * Complex::new(3.0, 4.0), Complex::new(-5.0, 10.0));

// The naive algorithm would return NaN + NaNi for these inputs, but the libcall handles it.
#[cfg(target_has_reliable_f16)]
assert_eq!(
Complex::new(1.0, 0.0) * Complex::new(f16::INFINITY, f16::INFINITY),
Complex::new(f16::INFINITY, f16::INFINITY)
);
assert_eq!(
Complex::new(1.0, 0.0) * Complex::new(f32::INFINITY, f32::INFINITY),
Complex::new(f32::INFINITY, f32::INFINITY)
);
assert_eq!(
Complex::new(1.0, 0.0) * Complex::new(f64::INFINITY, f64::INFINITY),
Complex::new(f64::INFINITY, f64::INFINITY)
);
#[cfg(target_has_reliable_f128)]
assert_eq!(
Complex::new(1.0, 0.0) * Complex::new(f128::INFINITY, f128::INFINITY),
Complex::new(f128::INFINITY, f128::INFINITY)
);
}

#[test]
fn div() {
#[cfg(target_has_reliable_f16)]
assert_eq!(Complex::new(2.0f16, 11.0) / Complex::new(2.0, 1.0), Complex::new(3.0, 4.0));
assert_eq!(Complex::new(2.0f32, 11.0) / Complex::new(2.0, 1.0), Complex::new(3.0, 4.0));
assert_eq!(Complex::new(2.0f64, 11.0) / Complex::new(2.0, 1.0), Complex::new(3.0, 4.0));
#[cfg(target_has_reliable_f128)]
assert_eq!(Complex::new(2.0f128, 11.0) / Complex::new(2.0, 1.0), Complex::new(3.0, 4.0));

// The naive algorithm would return NaN + NaNi for these inputs, but the libcall handles it.
#[cfg(target_has_reliable_f16)]
assert_eq!(
Complex::new(f16::INFINITY, 0.0) / Complex::new(1.0, 1.0),
Complex::new(f16::INFINITY, f16::NEG_INFINITY)
);
assert_eq!(
Complex::new(f32::INFINITY, 0.0) / Complex::new(1.0, 1.0),
Complex::new(f32::INFINITY, f32::NEG_INFINITY)
);
assert_eq!(
Complex::new(f64::INFINITY, 0.0) / Complex::new(1.0, 1.0),
Complex::new(f64::INFINITY, f64::NEG_INFINITY)
);
#[cfg(target_has_reliable_f128)]
assert_eq!(
Complex::new(f128::INFINITY, 0.0) / Complex::new(1.0, 1.0),
Complex::new(f128::INFINITY, f128::NEG_INFINITY)
);
}
Loading