Skip to content

Commit 38c7ba0

Browse files
committed
Handle subnormal numbers exactly
1 parent dc0ba78 commit 38c7ba0

File tree

5 files changed

+181
-24
lines changed

5 files changed

+181
-24
lines changed

crates/core_simd/tests/ops_macros.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ macro_rules! impl_unary_op_test {
66
{ $scalar:ty, $trait:ident :: $fn:ident, $scalar_fn:expr } => {
77
test_helpers::test_lanes! {
88
fn $fn<const LANES: usize>() {
9-
test_helpers::test_unary_elementwise(
9+
test_helpers::test_unary_elementwise_flush_subnormals(
1010
&<core_simd::simd::Simd<$scalar, LANES> as core::ops::$trait>::$fn,
1111
&$scalar_fn,
1212
&|_| true,
@@ -31,15 +31,15 @@ macro_rules! impl_binary_op_test {
3131

3232
test_helpers::test_lanes! {
3333
fn normal<const LANES: usize>() {
34-
test_helpers::test_binary_elementwise(
34+
test_helpers::test_binary_elementwise_flush_subnormals(
3535
&<Simd<$scalar, LANES> as core::ops::$trait>::$fn,
3636
&$scalar_fn,
3737
&|_, _| true,
3838
);
3939
}
4040

4141
fn assign<const LANES: usize>() {
42-
test_helpers::test_binary_elementwise(
42+
test_helpers::test_binary_elementwise_flush_subnormals(
4343
&|mut a, b| { <Simd<$scalar, LANES> as core::ops::$trait_assign>::$fn_assign(&mut a, b); a },
4444
&$scalar_fn,
4545
&|_, _| true,
@@ -433,15 +433,15 @@ macro_rules! impl_float_tests {
433433
}
434434

435435
fn to_degrees<const LANES: usize>() {
436-
test_helpers::test_unary_elementwise(
436+
test_helpers::test_unary_elementwise_flush_subnormals(
437437
&Vector::<LANES>::to_degrees,
438438
&Scalar::to_degrees,
439439
&|_| true,
440440
)
441441
}
442442

443443
fn to_radians<const LANES: usize>() {
444-
test_helpers::test_unary_elementwise(
444+
test_helpers::test_unary_elementwise_flush_subnormals(
445445
&Vector::<LANES>::to_radians,
446446
&Scalar::to_radians,
447447
&|_| true,
@@ -512,6 +512,7 @@ macro_rules! impl_float_tests {
512512

513513
fn simd_clamp<const LANES: usize>() {
514514
test_helpers::test_3(&|value: [Scalar; LANES], mut min: [Scalar; LANES], mut max: [Scalar; LANES]| {
515+
use test_helpers::subnormals::FlushSubnormals;
515516
for (min, max) in min.iter_mut().zip(max.iter_mut()) {
516517
if max < min {
517518
core::mem::swap(min, max);
@@ -528,8 +529,18 @@ macro_rules! impl_float_tests {
528529
for i in 0..LANES {
529530
result_scalar[i] = value[i].clamp(min[i], max[i]);
530531
}
532+
let mut result_scalar_flush = [Scalar::default(); LANES];
533+
for i in 0..LANES {
534+
result_scalar_flush[i] = value[i];
535+
if FlushSubnormals::flush(value[i]) < FlushSubnormals::flush(min[i]) {
536+
result_scalar_flush[i] = min[i];
537+
}
538+
if FlushSubnormals::flush(value[i]) > FlushSubnormals::flush(max[i]) {
539+
result_scalar_flush[i] = max[i];
540+
}
541+
}
531542
let result_vector = Vector::from_array(value).simd_clamp(min.into(), max.into()).to_array();
532-
test_helpers::prop_assert_biteq!(result_scalar, result_vector);
543+
test_helpers::prop_assert_biteq!(result_vector, result_scalar, result_scalar_flush);
533544
Ok(())
534545
})
535546
}

crates/test_helpers/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ edition = "2021"
55
publish = false
66

77
[dependencies]
8-
float_eq = "1.0"
98
proptest = { version = "0.10", default-features = false, features = ["alloc"] }
109

1110
[features]

crates/test_helpers/src/biteq.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ macro_rules! impl_float_biteq {
4040
fn biteq(&self, other: &Self) -> bool {
4141
if self.is_nan() && other.is_nan() {
4242
true // exact nan bits don't matter
43-
} else if crate::flush_subnormals::<Self>() {
44-
self.to_bits() == other.to_bits() || float_eq::float_eq!(self, other, abs <= 2. * <$type>::EPSILON)
4543
} else {
4644
self.to_bits() == other.to_bits()
4745
}
@@ -115,6 +113,27 @@ impl<T: BitEq> core::fmt::Debug for BitEqWrapper<'_, T> {
115113
}
116114
}
117115

116+
#[doc(hidden)]
117+
pub struct BitEqEitherWrapper<'a, T>(pub &'a T, pub &'a T);
118+
119+
impl<T: BitEq> PartialEq<BitEqEitherWrapper<'_, T>> for BitEqWrapper<'_, T> {
120+
fn eq(&self, other: &BitEqEitherWrapper<'_, T>) -> bool {
121+
self.0.biteq(other.0) || self.0.biteq(other.1)
122+
}
123+
}
124+
125+
impl<T: BitEq> core::fmt::Debug for BitEqEitherWrapper<'_, T> {
126+
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
127+
if self.0.biteq(self.1) {
128+
self.0.fmt(f)
129+
} else {
130+
self.0.fmt(f)?;
131+
write!(f, " or ")?;
132+
self.1.fmt(f)
133+
}
134+
}
135+
}
136+
118137
#[macro_export]
119138
macro_rules! prop_assert_biteq {
120139
{ $a:expr, $b:expr $(,)? } => {
@@ -124,5 +143,14 @@ macro_rules! prop_assert_biteq {
124143
let b = $b;
125144
proptest::prop_assert_eq!(BitEqWrapper(&a), BitEqWrapper(&b));
126145
}
127-
}
146+
};
147+
{ $a:expr, $b:expr, $c:expr $(,)? } => {
148+
{
149+
use $crate::biteq::{BitEqWrapper, BitEqEitherWrapper};
150+
let a = $a;
151+
let b = $b;
152+
let c = $c;
153+
proptest::prop_assert_eq!(BitEqWrapper(&a), BitEqEitherWrapper(&b, &c));
154+
}
155+
};
128156
}

crates/test_helpers/src/lib.rs

Lines changed: 94 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,8 @@ pub mod wasm;
66
#[macro_use]
77
pub mod biteq;
88

9-
/// Indicates if subnormal floats are flushed to zero.
10-
pub fn flush_subnormals<T>() -> bool {
11-
let is_f32 = core::mem::size_of::<T>() == 4;
12-
let ppc_flush = is_f32
13-
&& cfg!(all(
14-
target_arch = "powerpc64",
15-
target_endian = "big",
16-
not(target_feature = "vsx")
17-
));
18-
let arm_flush = is_f32 && cfg!(all(target_arch = "arm", target_feature = "neon"));
19-
ppc_flush || arm_flush
20-
}
9+
pub mod subnormals;
10+
use subnormals::FlushSubnormals;
2111

2212
/// Specifies the default strategy for testing a type.
2313
///
@@ -164,7 +154,6 @@ pub fn test_3<
164154
}
165155

166156
/// Test a unary vector function against a unary scalar function, applied elementwise.
167-
#[inline(never)]
168157
pub fn test_unary_elementwise<Scalar, ScalarResult, Vector, VectorResult, const LANES: usize>(
169158
fv: &dyn Fn(Vector) -> VectorResult,
170159
fs: &dyn Fn(Scalar) -> ScalarResult,
@@ -190,6 +179,48 @@ pub fn test_unary_elementwise<Scalar, ScalarResult, Vector, VectorResult, const
190179
});
191180
}
192181

182+
/// Test a unary vector function against a unary scalar function, applied elementwise.
183+
///
184+
/// Where subnormals are flushed, use approximate equality.
185+
pub fn test_unary_elementwise_flush_subnormals<
186+
Scalar,
187+
ScalarResult,
188+
Vector,
189+
VectorResult,
190+
const LANES: usize,
191+
>(
192+
fv: &dyn Fn(Vector) -> VectorResult,
193+
fs: &dyn Fn(Scalar) -> ScalarResult,
194+
check: &dyn Fn([Scalar; LANES]) -> bool,
195+
) where
196+
Scalar: Copy + core::fmt::Debug + DefaultStrategy + FlushSubnormals,
197+
ScalarResult: Copy + biteq::BitEq + core::fmt::Debug + DefaultStrategy + FlushSubnormals,
198+
Vector: Into<[Scalar; LANES]> + From<[Scalar; LANES]> + Copy,
199+
VectorResult: Into<[ScalarResult; LANES]> + From<[ScalarResult; LANES]> + Copy,
200+
{
201+
let flush = |x: Scalar| FlushSubnormals::flush(fs(FlushSubnormals::flush(x)));
202+
test_1(&|x: [Scalar; LANES]| {
203+
proptest::prop_assume!(check(x));
204+
let result_v: [ScalarResult; LANES] = fv(x.into()).into();
205+
let result_s: [ScalarResult; LANES] = x
206+
.iter()
207+
.copied()
208+
.map(fs)
209+
.collect::<Vec<_>>()
210+
.try_into()
211+
.unwrap();
212+
let result_sf: [ScalarResult; LANES] = x
213+
.iter()
214+
.copied()
215+
.map(flush)
216+
.collect::<Vec<_>>()
217+
.try_into()
218+
.unwrap();
219+
crate::prop_assert_biteq!(result_v, result_s, result_sf);
220+
Ok(())
221+
});
222+
}
223+
193224
/// Test a unary vector function against a unary scalar function, applied elementwise.
194225
#[inline(never)]
195226
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>(
217248
}
218249

219250
/// Test a binary vector function against a binary scalar function, applied elementwise.
220-
#[inline(never)]
221251
pub fn test_binary_elementwise<
222252
Scalar1,
223253
Scalar2,
@@ -254,6 +284,56 @@ pub fn test_binary_elementwise<
254284
});
255285
}
256286

287+
/// Test a binary vector function against a binary scalar function, applied elementwise.
288+
///
289+
/// Where subnormals are flushed, use approximate equality.
290+
pub fn test_binary_elementwise_flush_subnormals<
291+
Scalar1,
292+
Scalar2,
293+
ScalarResult,
294+
Vector1,
295+
Vector2,
296+
VectorResult,
297+
const LANES: usize,
298+
>(
299+
fv: &dyn Fn(Vector1, Vector2) -> VectorResult,
300+
fs: &dyn Fn(Scalar1, Scalar2) -> ScalarResult,
301+
check: &dyn Fn([Scalar1; LANES], [Scalar2; LANES]) -> bool,
302+
) where
303+
Scalar1: Copy + core::fmt::Debug + DefaultStrategy + FlushSubnormals,
304+
Scalar2: Copy + core::fmt::Debug + DefaultStrategy + FlushSubnormals,
305+
ScalarResult: Copy + biteq::BitEq + core::fmt::Debug + DefaultStrategy + FlushSubnormals,
306+
Vector1: Into<[Scalar1; LANES]> + From<[Scalar1; LANES]> + Copy,
307+
Vector2: Into<[Scalar2; LANES]> + From<[Scalar2; LANES]> + Copy,
308+
VectorResult: Into<[ScalarResult; LANES]> + From<[ScalarResult; LANES]> + Copy,
309+
{
310+
let flush = |x: Scalar1, y: Scalar2| {
311+
FlushSubnormals::flush(fs(FlushSubnormals::flush(x), FlushSubnormals::flush(y)))
312+
};
313+
test_2(&|x: [Scalar1; LANES], y: [Scalar2; LANES]| {
314+
proptest::prop_assume!(check(x, y));
315+
let result_v: [ScalarResult; LANES] = fv(x.into(), y.into()).into();
316+
let result_s: [ScalarResult; LANES] = x
317+
.iter()
318+
.copied()
319+
.zip(y.iter().copied())
320+
.map(|(x, y)| fs(x, y))
321+
.collect::<Vec<_>>()
322+
.try_into()
323+
.unwrap();
324+
let result_sf: [ScalarResult; LANES] = x
325+
.iter()
326+
.copied()
327+
.zip(y.iter().copied())
328+
.map(|(x, y)| flush(x, y))
329+
.collect::<Vec<_>>()
330+
.try_into()
331+
.unwrap();
332+
crate::prop_assert_biteq!(result_v, result_s, result_sf);
333+
Ok(())
334+
});
335+
}
336+
257337
/// Test a binary vector-scalar function against a binary scalar function, applied elementwise.
258338
#[inline(never)]
259339
pub fn test_binary_scalar_rhs_elementwise<

crates/test_helpers/src/subnormals.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
pub trait FlushSubnormals: Sized {
2+
fn flush(self) -> Self {
3+
self
4+
}
5+
}
6+
7+
impl<T> FlushSubnormals for *const T {}
8+
impl<T> FlushSubnormals for *mut T {}
9+
10+
macro_rules! impl_float {
11+
{ $($ty:ty),* } => {
12+
$(
13+
impl FlushSubnormals for $ty {
14+
fn flush(self) -> Self {
15+
let is_f32 = core::mem::size_of::<Self>() == 4;
16+
let ppc_flush = is_f32 && cfg!(all(target_arch = "powerpc64", target_endian = "big", not(target_feature = "vsx")));
17+
let arm_flush = is_f32 && cfg!(all(target_arch = "arm", target_feature = "neon"));
18+
let flush = ppc_flush || arm_flush;
19+
if flush && self.is_subnormal() {
20+
<$ty>::copysign(0., self)
21+
} else {
22+
self
23+
}
24+
}
25+
}
26+
)*
27+
}
28+
}
29+
30+
macro_rules! impl_else {
31+
{ $($ty:ty),* } => {
32+
$(
33+
impl FlushSubnormals for $ty {}
34+
)*
35+
}
36+
}
37+
38+
impl_float! { f32, f64 }
39+
impl_else! { i8, i16, i32, i64, isize, u8, u16, u32, u64, usize }

0 commit comments

Comments
 (0)