Handle subnormal numbers exactly
This commit is contained in:
parent
dc0ba78365
commit
38c7ba09dd
@ -6,7 +6,7 @@ macro_rules! impl_unary_op_test {
|
|||||||
{ $scalar:ty, $trait:ident :: $fn:ident, $scalar_fn:expr } => {
|
{ $scalar:ty, $trait:ident :: $fn:ident, $scalar_fn:expr } => {
|
||||||
test_helpers::test_lanes! {
|
test_helpers::test_lanes! {
|
||||||
fn $fn<const LANES: usize>() {
|
fn $fn<const LANES: usize>() {
|
||||||
test_helpers::test_unary_elementwise(
|
test_helpers::test_unary_elementwise_flush_subnormals(
|
||||||
&<core_simd::simd::Simd<$scalar, LANES> as core::ops::$trait>::$fn,
|
&<core_simd::simd::Simd<$scalar, LANES> as core::ops::$trait>::$fn,
|
||||||
&$scalar_fn,
|
&$scalar_fn,
|
||||||
&|_| true,
|
&|_| true,
|
||||||
@ -31,7 +31,7 @@ macro_rules! impl_binary_op_test {
|
|||||||
|
|
||||||
test_helpers::test_lanes! {
|
test_helpers::test_lanes! {
|
||||||
fn normal<const LANES: usize>() {
|
fn normal<const LANES: usize>() {
|
||||||
test_helpers::test_binary_elementwise(
|
test_helpers::test_binary_elementwise_flush_subnormals(
|
||||||
&<Simd<$scalar, LANES> as core::ops::$trait>::$fn,
|
&<Simd<$scalar, LANES> as core::ops::$trait>::$fn,
|
||||||
&$scalar_fn,
|
&$scalar_fn,
|
||||||
&|_, _| true,
|
&|_, _| true,
|
||||||
@ -39,7 +39,7 @@ macro_rules! impl_binary_op_test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn assign<const LANES: usize>() {
|
fn assign<const LANES: usize>() {
|
||||||
test_helpers::test_binary_elementwise(
|
test_helpers::test_binary_elementwise_flush_subnormals(
|
||||||
&|mut a, b| { <Simd<$scalar, LANES> as core::ops::$trait_assign>::$fn_assign(&mut a, b); a },
|
&|mut a, b| { <Simd<$scalar, LANES> as core::ops::$trait_assign>::$fn_assign(&mut a, b); a },
|
||||||
&$scalar_fn,
|
&$scalar_fn,
|
||||||
&|_, _| true,
|
&|_, _| true,
|
||||||
@ -433,7 +433,7 @@ macro_rules! impl_float_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn to_degrees<const LANES: usize>() {
|
fn to_degrees<const LANES: usize>() {
|
||||||
test_helpers::test_unary_elementwise(
|
test_helpers::test_unary_elementwise_flush_subnormals(
|
||||||
&Vector::<LANES>::to_degrees,
|
&Vector::<LANES>::to_degrees,
|
||||||
&Scalar::to_degrees,
|
&Scalar::to_degrees,
|
||||||
&|_| true,
|
&|_| true,
|
||||||
@ -441,7 +441,7 @@ macro_rules! impl_float_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn to_radians<const LANES: usize>() {
|
fn to_radians<const LANES: usize>() {
|
||||||
test_helpers::test_unary_elementwise(
|
test_helpers::test_unary_elementwise_flush_subnormals(
|
||||||
&Vector::<LANES>::to_radians,
|
&Vector::<LANES>::to_radians,
|
||||||
&Scalar::to_radians,
|
&Scalar::to_radians,
|
||||||
&|_| true,
|
&|_| true,
|
||||||
@ -512,6 +512,7 @@ macro_rules! impl_float_tests {
|
|||||||
|
|
||||||
fn simd_clamp<const LANES: usize>() {
|
fn simd_clamp<const LANES: usize>() {
|
||||||
test_helpers::test_3(&|value: [Scalar; LANES], mut min: [Scalar; LANES], mut max: [Scalar; LANES]| {
|
test_helpers::test_3(&|value: [Scalar; LANES], mut min: [Scalar; LANES], mut max: [Scalar; LANES]| {
|
||||||
|
use test_helpers::subnormals::FlushSubnormals;
|
||||||
for (min, max) in min.iter_mut().zip(max.iter_mut()) {
|
for (min, max) in min.iter_mut().zip(max.iter_mut()) {
|
||||||
if max < min {
|
if max < min {
|
||||||
core::mem::swap(min, max);
|
core::mem::swap(min, max);
|
||||||
@ -528,8 +529,18 @@ macro_rules! impl_float_tests {
|
|||||||
for i in 0..LANES {
|
for i in 0..LANES {
|
||||||
result_scalar[i] = value[i].clamp(min[i], max[i]);
|
result_scalar[i] = value[i].clamp(min[i], max[i]);
|
||||||
}
|
}
|
||||||
|
let mut result_scalar_flush = [Scalar::default(); LANES];
|
||||||
|
for i in 0..LANES {
|
||||||
|
result_scalar_flush[i] = value[i];
|
||||||
|
if FlushSubnormals::flush(value[i]) < FlushSubnormals::flush(min[i]) {
|
||||||
|
result_scalar_flush[i] = min[i];
|
||||||
|
}
|
||||||
|
if FlushSubnormals::flush(value[i]) > FlushSubnormals::flush(max[i]) {
|
||||||
|
result_scalar_flush[i] = max[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
let result_vector = Vector::from_array(value).simd_clamp(min.into(), max.into()).to_array();
|
let result_vector = Vector::from_array(value).simd_clamp(min.into(), max.into()).to_array();
|
||||||
test_helpers::prop_assert_biteq!(result_scalar, result_vector);
|
test_helpers::prop_assert_biteq!(result_vector, result_scalar, result_scalar_flush);
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ edition = "2021"
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
float_eq = "1.0"
|
|
||||||
proptest = { version = "0.10", default-features = false, features = ["alloc"] }
|
proptest = { version = "0.10", default-features = false, features = ["alloc"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -40,8 +40,6 @@ macro_rules! impl_float_biteq {
|
|||||||
fn biteq(&self, other: &Self) -> bool {
|
fn biteq(&self, other: &Self) -> bool {
|
||||||
if self.is_nan() && other.is_nan() {
|
if self.is_nan() && other.is_nan() {
|
||||||
true // exact nan bits don't matter
|
true // exact nan bits don't matter
|
||||||
} else if crate::flush_subnormals::<Self>() {
|
|
||||||
self.to_bits() == other.to_bits() || float_eq::float_eq!(self, other, abs <= 2. * <$type>::EPSILON)
|
|
||||||
} else {
|
} else {
|
||||||
self.to_bits() == other.to_bits()
|
self.to_bits() == other.to_bits()
|
||||||
}
|
}
|
||||||
@ -115,6 +113,27 @@ impl<T: BitEq> core::fmt::Debug for BitEqWrapper<'_, T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub struct BitEqEitherWrapper<'a, T>(pub &'a T, pub &'a T);
|
||||||
|
|
||||||
|
impl<T: BitEq> PartialEq<BitEqEitherWrapper<'_, T>> for BitEqWrapper<'_, T> {
|
||||||
|
fn eq(&self, other: &BitEqEitherWrapper<'_, T>) -> bool {
|
||||||
|
self.0.biteq(other.0) || self.0.biteq(other.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: BitEq> core::fmt::Debug for BitEqEitherWrapper<'_, T> {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||||
|
if self.0.biteq(self.1) {
|
||||||
|
self.0.fmt(f)
|
||||||
|
} else {
|
||||||
|
self.0.fmt(f)?;
|
||||||
|
write!(f, " or ")?;
|
||||||
|
self.1.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! prop_assert_biteq {
|
macro_rules! prop_assert_biteq {
|
||||||
{ $a:expr, $b:expr $(,)? } => {
|
{ $a:expr, $b:expr $(,)? } => {
|
||||||
@ -124,5 +143,14 @@ macro_rules! prop_assert_biteq {
|
|||||||
let b = $b;
|
let b = $b;
|
||||||
proptest::prop_assert_eq!(BitEqWrapper(&a), BitEqWrapper(&b));
|
proptest::prop_assert_eq!(BitEqWrapper(&a), BitEqWrapper(&b));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
{ $a:expr, $b:expr, $c:expr $(,)? } => {
|
||||||
|
{
|
||||||
|
use $crate::biteq::{BitEqWrapper, BitEqEitherWrapper};
|
||||||
|
let a = $a;
|
||||||
|
let b = $b;
|
||||||
|
let c = $c;
|
||||||
|
proptest::prop_assert_eq!(BitEqWrapper(&a), BitEqEitherWrapper(&b, &c));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -6,18 +6,8 @@ pub mod wasm;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod biteq;
|
pub mod biteq;
|
||||||
|
|
||||||
/// Indicates if subnormal floats are flushed to zero.
|
pub mod subnormals;
|
||||||
pub fn flush_subnormals<T>() -> bool {
|
use subnormals::FlushSubnormals;
|
||||||
let is_f32 = core::mem::size_of::<T>() == 4;
|
|
||||||
let ppc_flush = is_f32
|
|
||||||
&& cfg!(all(
|
|
||||||
target_arch = "powerpc64",
|
|
||||||
target_endian = "big",
|
|
||||||
not(target_feature = "vsx")
|
|
||||||
));
|
|
||||||
let arm_flush = is_f32 && cfg!(all(target_arch = "arm", target_feature = "neon"));
|
|
||||||
ppc_flush || arm_flush
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Specifies the default strategy for testing a type.
|
/// Specifies the default strategy for testing a type.
|
||||||
///
|
///
|
||||||
@ -164,7 +154,6 @@ pub fn test_3<
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Test a unary vector function against a unary scalar function, applied elementwise.
|
/// Test a unary vector function against a unary scalar function, applied elementwise.
|
||||||
#[inline(never)]
|
|
||||||
pub fn test_unary_elementwise<Scalar, ScalarResult, Vector, VectorResult, const LANES: usize>(
|
pub fn test_unary_elementwise<Scalar, ScalarResult, Vector, VectorResult, const LANES: usize>(
|
||||||
fv: &dyn Fn(Vector) -> VectorResult,
|
fv: &dyn Fn(Vector) -> VectorResult,
|
||||||
fs: &dyn Fn(Scalar) -> ScalarResult,
|
fs: &dyn Fn(Scalar) -> ScalarResult,
|
||||||
@ -190,6 +179,48 @@ pub fn test_unary_elementwise<Scalar, ScalarResult, Vector, VectorResult, const
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test a unary vector function against a unary scalar function, applied elementwise.
|
||||||
|
///
|
||||||
|
/// Where subnormals are flushed, use approximate equality.
|
||||||
|
pub fn test_unary_elementwise_flush_subnormals<
|
||||||
|
Scalar,
|
||||||
|
ScalarResult,
|
||||||
|
Vector,
|
||||||
|
VectorResult,
|
||||||
|
const LANES: usize,
|
||||||
|
>(
|
||||||
|
fv: &dyn Fn(Vector) -> VectorResult,
|
||||||
|
fs: &dyn Fn(Scalar) -> ScalarResult,
|
||||||
|
check: &dyn Fn([Scalar; LANES]) -> bool,
|
||||||
|
) where
|
||||||
|
Scalar: Copy + core::fmt::Debug + DefaultStrategy + FlushSubnormals,
|
||||||
|
ScalarResult: Copy + biteq::BitEq + core::fmt::Debug + DefaultStrategy + FlushSubnormals,
|
||||||
|
Vector: Into<[Scalar; LANES]> + From<[Scalar; LANES]> + Copy,
|
||||||
|
VectorResult: Into<[ScalarResult; LANES]> + From<[ScalarResult; LANES]> + Copy,
|
||||||
|
{
|
||||||
|
let flush = |x: Scalar| FlushSubnormals::flush(fs(FlushSubnormals::flush(x)));
|
||||||
|
test_1(&|x: [Scalar; LANES]| {
|
||||||
|
proptest::prop_assume!(check(x));
|
||||||
|
let result_v: [ScalarResult; LANES] = fv(x.into()).into();
|
||||||
|
let result_s: [ScalarResult; LANES] = x
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(fs)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
|
let result_sf: [ScalarResult; LANES] = x
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(flush)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
|
crate::prop_assert_biteq!(result_v, result_s, result_sf);
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Test a unary vector function against a unary scalar function, applied elementwise.
|
/// Test a unary vector function against a unary scalar function, applied elementwise.
|
||||||
#[inline(never)]
|
#[inline(never)]
|
||||||
pub fn test_unary_mask_elementwise<Scalar, Vector, Mask, const LANES: usize>(
|
pub fn test_unary_mask_elementwise<Scalar, Vector, Mask, const LANES: usize>(
|
||||||
@ -217,7 +248,6 @@ pub fn test_unary_mask_elementwise<Scalar, Vector, Mask, const LANES: usize>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Test a binary vector function against a binary scalar function, applied elementwise.
|
/// Test a binary vector function against a binary scalar function, applied elementwise.
|
||||||
#[inline(never)]
|
|
||||||
pub fn test_binary_elementwise<
|
pub fn test_binary_elementwise<
|
||||||
Scalar1,
|
Scalar1,
|
||||||
Scalar2,
|
Scalar2,
|
||||||
@ -254,6 +284,56 @@ pub fn test_binary_elementwise<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test a binary vector function against a binary scalar function, applied elementwise.
|
||||||
|
///
|
||||||
|
/// Where subnormals are flushed, use approximate equality.
|
||||||
|
pub fn test_binary_elementwise_flush_subnormals<
|
||||||
|
Scalar1,
|
||||||
|
Scalar2,
|
||||||
|
ScalarResult,
|
||||||
|
Vector1,
|
||||||
|
Vector2,
|
||||||
|
VectorResult,
|
||||||
|
const LANES: usize,
|
||||||
|
>(
|
||||||
|
fv: &dyn Fn(Vector1, Vector2) -> VectorResult,
|
||||||
|
fs: &dyn Fn(Scalar1, Scalar2) -> ScalarResult,
|
||||||
|
check: &dyn Fn([Scalar1; LANES], [Scalar2; LANES]) -> bool,
|
||||||
|
) where
|
||||||
|
Scalar1: Copy + core::fmt::Debug + DefaultStrategy + FlushSubnormals,
|
||||||
|
Scalar2: Copy + core::fmt::Debug + DefaultStrategy + FlushSubnormals,
|
||||||
|
ScalarResult: Copy + biteq::BitEq + core::fmt::Debug + DefaultStrategy + FlushSubnormals,
|
||||||
|
Vector1: Into<[Scalar1; LANES]> + From<[Scalar1; LANES]> + Copy,
|
||||||
|
Vector2: Into<[Scalar2; LANES]> + From<[Scalar2; LANES]> + Copy,
|
||||||
|
VectorResult: Into<[ScalarResult; LANES]> + From<[ScalarResult; LANES]> + Copy,
|
||||||
|
{
|
||||||
|
let flush = |x: Scalar1, y: Scalar2| {
|
||||||
|
FlushSubnormals::flush(fs(FlushSubnormals::flush(x), FlushSubnormals::flush(y)))
|
||||||
|
};
|
||||||
|
test_2(&|x: [Scalar1; LANES], y: [Scalar2; LANES]| {
|
||||||
|
proptest::prop_assume!(check(x, y));
|
||||||
|
let result_v: [ScalarResult; LANES] = fv(x.into(), y.into()).into();
|
||||||
|
let result_s: [ScalarResult; LANES] = x
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.zip(y.iter().copied())
|
||||||
|
.map(|(x, y)| fs(x, y))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
|
let result_sf: [ScalarResult; LANES] = x
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.zip(y.iter().copied())
|
||||||
|
.map(|(x, y)| flush(x, y))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
|
crate::prop_assert_biteq!(result_v, result_s, result_sf);
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Test a binary vector-scalar function against a binary scalar function, applied elementwise.
|
/// Test a binary vector-scalar function against a binary scalar function, applied elementwise.
|
||||||
#[inline(never)]
|
#[inline(never)]
|
||||||
pub fn test_binary_scalar_rhs_elementwise<
|
pub fn test_binary_scalar_rhs_elementwise<
|
||||||
|
39
crates/test_helpers/src/subnormals.rs
Normal file
39
crates/test_helpers/src/subnormals.rs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
pub trait FlushSubnormals: Sized {
|
||||||
|
fn flush(self) -> Self {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> FlushSubnormals for *const T {}
|
||||||
|
impl<T> FlushSubnormals for *mut T {}
|
||||||
|
|
||||||
|
macro_rules! impl_float {
|
||||||
|
{ $($ty:ty),* } => {
|
||||||
|
$(
|
||||||
|
impl FlushSubnormals for $ty {
|
||||||
|
fn flush(self) -> Self {
|
||||||
|
let is_f32 = core::mem::size_of::<Self>() == 4;
|
||||||
|
let ppc_flush = is_f32 && cfg!(all(target_arch = "powerpc64", target_endian = "big", not(target_feature = "vsx")));
|
||||||
|
let arm_flush = is_f32 && cfg!(all(target_arch = "arm", target_feature = "neon"));
|
||||||
|
let flush = ppc_flush || arm_flush;
|
||||||
|
if flush && self.is_subnormal() {
|
||||||
|
<$ty>::copysign(0., self)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_else {
|
||||||
|
{ $($ty:ty),* } => {
|
||||||
|
$(
|
||||||
|
impl FlushSubnormals for $ty {}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_float! { f32, f64 }
|
||||||
|
impl_else! { i8, i16, i32, i64, isize, u8, u16, u32, u64, usize }
|
Loading…
x
Reference in New Issue
Block a user