1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
// Copyright 2024 the Fearless_SIMD Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! SIMD math operators/kernels.
#![allow(
dead_code,
reason = "Generated backends use different subsets of these helpers"
)]
use crate::{f32x4, f64x2, i32x4, i64x2, prelude::*, u64x2};
#[cfg(all(feature = "libm", not(feature = "std")))]
pub(crate) trait FloatExt {
fn floor(self) -> Self;
fn ceil(self) -> Self;
fn round_ties_even(self) -> Self;
fn round(self) -> Self;
fn fract(self) -> Self;
fn sqrt(self) -> Self;
fn trunc(self) -> Self;
fn mul_add(self, a: Self, b: Self) -> Self;
}
#[cfg(all(feature = "libm", not(feature = "std")))]
impl FloatExt for f32 {
#[inline(always)]
fn floor(self) -> Self {
libm::floorf(self)
}
#[inline(always)]
fn ceil(self) -> Self {
libm::ceilf(self)
}
#[inline(always)]
fn round_ties_even(self) -> Self {
libm::rintf(self)
}
#[inline(always)]
fn round(self) -> Self {
libm::roundf(self)
}
#[inline(always)]
fn sqrt(self) -> Self {
libm::sqrtf(self)
}
#[inline(always)]
fn fract(self) -> Self {
self - self.trunc()
}
#[inline(always)]
fn trunc(self) -> Self {
libm::truncf(self)
}
#[inline(always)]
fn mul_add(self, a: Self, b: Self) -> Self {
libm::fmaf(self, a, b)
}
}
#[cfg(all(feature = "libm", not(feature = "std")))]
impl FloatExt for f64 {
#[inline(always)]
fn floor(self) -> Self {
libm::floor(self)
}
#[inline(always)]
fn ceil(self) -> Self {
libm::ceil(self)
}
#[inline(always)]
fn round_ties_even(self) -> Self {
libm::rint(self)
}
#[inline(always)]
fn round(self) -> Self {
libm::round(self)
}
#[inline(always)]
fn sqrt(self) -> Self {
libm::sqrt(self)
}
#[inline(always)]
fn fract(self) -> Self {
self - self.trunc()
}
#[inline(always)]
fn trunc(self) -> Self {
libm::trunc(self)
}
#[inline(always)]
fn mul_add(self, a: Self, b: Self) -> Self {
libm::fma(self, a, b)
}
}
#[inline(always)]
pub(crate) fn mul_add_precise_f32x4<S: Simd>(
simd: S,
a: f32x4<S>,
b: f32x4<S>,
c: f32x4<S>,
) -> f32x4<S> {
let (a_low, a_high) = a.widen();
let (b_low, b_high) = b.widen();
let (c_low, c_high) = c.widen();
// Every finite f32 product is exactly representable as f64. Every exact finite
// product-plus-add is a multiple of 2^-298 with magnitude below 2^256, so none of
// these operations underflow or overflow in f64. Usually the f64 sum can be
// narrowed directly. For a normal f32 result, the only possible double-rounding
// error is when that sum is exactly halfway between two adjacent f32 values. This
// also recognizes the f32 finite/infinity overflow threshold.
let product_low = a_low * b_low;
let product_high = a_high * b_high;
let mut sum_low = product_low + c_low;
let mut sum_high = product_high + c_high;
const MIDPOINT_FRACTION_MASK: i64 = 0x1fff_ffff;
const MIDPOINT_FRACTION: i64 = 0x1000_0000;
let midpoint_low =
(sum_low.bitcast::<i64x2<_>>() & MIDPOINT_FRACTION_MASK).simd_eq(MIDPOINT_FRACTION);
let midpoint_high =
(sum_high.bitcast::<i64x2<_>>() & MIDPOINT_FRACTION_MASK).simd_eq(MIDPOINT_FRACTION);
// Subnormal f32 values have fewer than 24 significant bits, so their midpoint
// position within the f64 significand varies with the result's exponent. Use a
// provisional narrowing to identify every result whose rounding interval touches
// the subnormal range, including zero and the smallest normal value. Those rare
// lanes take the general round-to-odd path below.
let mut provisional = sum_low.narrow(sum_high);
let abs_provisional_bits = provisional.bitcast::<i32x4<_>>() & 0x7fff_ffff;
let at_most_min_normal = abs_provisional_bits.simd_lt(0x0080_0001);
let (subnormal_low, subnormal_high) = at_most_min_normal.widen();
let round_to_odd_low = midpoint_low | subnormal_low;
let round_to_odd_high = midpoint_high | subnormal_high;
let any_round_to_odd = (round_to_odd_low | round_to_odd_high).any_true();
// Optimization notes:
// On uniform numeric values over the entire range, this branch only fires once per 685k calls,
// and on uniform numeric values in [-1, 1) it fires once in 17k calls (empirically).
// So it is worth putting under an `if` despite the branch misprediction penalty.
// Outlining this into a #[cold] function regresses performance on both fast and slow paths.
// TODO: try using std::hint::cold_path() once MSRV is >= 1.95 and see if that does anything
if any_round_to_odd {
// Knuth's unconditional TwoSum establishes
// `sum + error == product + c` exactly. If a candidate addition was
// inexact and its rounded f64 significand is even, shift it by one ULP toward
// the error. This produces the round-to-odd intermediate from Theorem 3.
let virtual_low = sum_low - product_low;
let error_low = (product_low - (sum_low - virtual_low)) + (c_low - virtual_low);
let virtual_high = sum_high - product_high;
let error_high = (product_high - (sum_high - virtual_high)) + (c_high - virtual_high);
let sum_low_bits = sum_low.bitcast::<i64x2<_>>();
let even_low = (sum_low_bits & 1).simd_eq(0);
// Negate the sign bit and set the low bit: -1 if signs differ, +1 otherwise.
let sign_low = ((sum_low_bits ^ error_low.bitcast::<i64x2<_>>()).bitcast::<u64x2<_>>()
>> 63)
.bitcast::<i64x2<_>>();
let direction_low = (i64x2::<_>::splat(simd, 0) - sign_low) | 1;
sum_low = (round_to_odd_low & even_low & !error_low.simd_eq(0.0)).select(
(sum_low_bits + direction_low).bitcast::<f64x2<_>>(),
sum_low,
);
let sum_high_bits = sum_high.bitcast::<i64x2<_>>();
let even_high = (sum_high_bits & 1).simd_eq(0);
let sign_high = ((sum_high_bits ^ error_high.bitcast::<i64x2<_>>()).bitcast::<u64x2<_>>()
>> 63)
.bitcast::<i64x2<_>>();
let direction_high = (i64x2::<_>::splat(simd, 0) - sign_high) | 1;
sum_high = (round_to_odd_high & even_high & !error_high.simd_eq(0.0)).select(
(sum_high_bits + direction_high).bitcast::<f64x2<_>>(),
sum_high,
);
provisional = sum_low.narrow(sum_high);
}
provisional
}
#[inline(always)]
pub(crate) fn mul_add_precise_f64x2<S: Simd>(
simd: S,
a: f64x2<S>,
b: f64x2<S>,
c: f64x2<S>,
) -> f64x2<S> {
// Graillat and Muller's Algorithm 9 computes an FMA from a Dekker product
// and their correctly rounded addition of a double-word and an FP number:
// https://perso.lip6.fr/Stef.Graillat/papers/NM-2025.pdf
// https://doi.org/10.1007/s00211-025-01487-2
//
// Their proof assumes an unbounded exponent range. Restrict the packed path
// to a conservative range where no nonzero intermediate can underflow or
// overflow, and use scalar FMA for complete IEEE binary64 range coverage.
//
// The vectorized path is taken for values between 2^-400 and 2^400,
// values outside that range are routed to the scalar fallback.
// 2^-400 and 2^400 have biased exponents 623 and 1423. Inclusive
// comparisons are expressed using strict signed qword comparisons;
// absolute-value binary64 bits are always nonnegative as i64 values.
let lower = f64::from_bits(623_u64 << 52); // 2^-400
let upper = f64::from_bits((1423_u64 << 52) + 1); // Next float above 2^400
let a_abs = a.abs();
let b_abs = b.abs();
let c_abs = c.abs();
let a_safe = a_abs.simd_ge(lower) & a_abs.simd_le(upper);
let b_safe = b_abs.simd_ge(lower) & b_abs.simd_le(upper);
let c_safe = (c_abs.simd_ge(lower) & c_abs.simd_le(upper)) | c_abs.simd_eq(0.0);
// Scalarize both lanes if either one is outside the proven exponent-safe range.
// This branch must precede the packed arithmetic so unsafe inactive lanes cannot
// overflow or underflow inside the error-free transforms.
if !(a_safe & b_safe & c_safe).all_true() {
return [a[0].mul_add(b[0], c[0]), a[1].mul_add(b[1], c[1])].simd_into(simd);
}
// Split each normal multiplicand into nonoverlapping high and low parts.
// Adding half of the discarded range before clearing 27 significand bits
// gives the high part at most 26 significant bits and leaves the low part
// at most 27. The addition operates on sign-magnitude binary64 encodings,
// so it rounds the magnitude in the same direction for either sign. The
// exponent guard above prevents the integer addition from wrapping.
let split_rounding_bit = i64x2::<_>::splat(simd, 1 << 26);
let split_high_mask = i64x2::<_>::splat(simd, !((1 << 27) - 1));
let a_high =
((a.bitcast::<i64x2<_>>() + split_rounding_bit) & split_high_mask).bitcast::<f64x2<_>>();
let a_low = a - a_high;
let b_high =
((b.bitcast::<i64x2<_>>() + split_rounding_bit) & split_high_mask).bitcast::<f64x2<_>>();
let b_low = b - b_high;
// Dekker product: product_high + product_low is exactly a * b.
let product_high = a * b;
let product_error_1 = a_high * b_high - product_high;
let product_error_2 = product_error_1 + a_high * b_low;
let product_error_3 = product_error_2 + a_low * b_high;
let product_low = product_error_3 + a_low * b_low;
// Magnitude-sort each addition so Fast2Sum can replace TwoSum.
// This trades packed compares and blends for a shorter floating-point
// dependency chain, as suggested by Graillat and Muller.
let sum_high = product_high + c;
let product_high_larger = c_abs.simd_lt(product_high.abs());
let sum_large = product_high_larger.select(product_high, c);
let sum_small = product_high_larger.select(c, product_high);
let sum_low = sum_small - (sum_high - sum_large);
// Only the rounded sum of product_low and sum_low is needed on the
// overwhelmingly common path. Its exact residual matters only when
// v_high has one of the two significand shapes that can require the
// Graillat-Muller correction, so defer that second Fast2Sum until then.
let v_high = product_low + sum_low;
let v_high_bits = v_high.bitcast::<u64x2<_>>();
let special_fraction = (v_high_bits & 0x0007_ffff_ffff_ffff).simd_eq(0);
// Zero has the same fraction shape but cannot need correction.
// Exclude both signs of zero to avoid unnecessary work for exact arithmetic.
if (special_fraction & !v_high.simd_eq(0.0)).any_true() {
// Fast2Sum(product_low, sum_low), after the same magnitude sort.
let product_low_larger = sum_low.abs().simd_lt(product_low.abs());
let v_large = product_low_larger.select(product_low, sum_low);
let v_small = product_low_larger.select(sum_low, product_low);
let v_low = v_small - (v_high - v_large);
// The default sum is correctly rounded except when v_low is nonzero and
// |v_high| is 2^k or 3 * 2^k. Under the exponent guard, every nonzero
// product or residual is a multiple of at least 2^-904, so v_low != 0
// implies that v_high is finite, normal, and nonzero. The two relevant
// significand shapes therefore differ only in their top fraction bit.
let special = special_fraction & !v_low.simd_eq(0.0);
let different_sign = ((v_high_bits ^ v_low.bitcast::<u64x2<_>>()) >> 63).simd_eq(1);
let factor = different_sign.select(
f64x2::<_>::splat(simd, 7.0 / 8.0),
f64x2::<_>::splat(simd, 9.0 / 8.0),
);
sum_high + special.select(factor * v_high, v_high)
} else {
sum_high + v_high
}
}