Skip to content

Commit 6bcea9f

Browse files
committed
BUG: Fix f64 trig accuracy; match SVML rounding DAGs
- Extended path: restructure final reconstruction to follow SVML's per-precision rounding DAG (f32/f64 pair the sin/cos correction terms differently) and regenerate approx.h so sigma is a signed power of two, keeping sigma*r exact - Low path: give f64 cosine its own minimax fit matching __svml_cos_d_la and keep its fast path up to ~8388607*pi - Non-FMA f64: add SplitMul (exact head product via 26-bit split) to replace the FMA product-error idiom in the high/extended paths and reduce with a descending 4-word pi/16, restoring sub-ULP accuracy - Correct trig polynomial/scale/threshold commentsit log
1 parent ca4f3e6 commit 6bcea9f

8 files changed

Lines changed: 1152 additions & 1074 deletions

File tree

npsr/hwy.h

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,23 @@ using hn::TFromV;
3232
using hn::VFromD;
3333
constexpr bool kNativeFMA = HWY_NATIVE_FMA != 0;
3434

35-
inline HWY_ATTR void DummyToSuppressUnusedWarning() {}
35+
// Dekker split product for targets without native FMA, where the
36+
// `MulSub(a, b, a*b)` error idiom degenerates to zero. Masking to 26-bit heads
37+
// makes `head` exact, so a*b == head + rest (rest rounded at ~2^-105).
38+
template <typename V, HWY_IF_F64(TFromV<V>)>
39+
NPSR_INTRIN void SplitMul(V a, V b, V& head, V& rest) {
40+
using namespace hn;
41+
const DFromV<V> d;
42+
const RebindToUnsigned<decltype(d)> du;
43+
const V mask = BitCast(d, Set(du, 0xFFFFFFFFF8000000u));
44+
const V a_hi = And(a, mask);
45+
const V a_lo = Sub(a, a_hi);
46+
const V b_hi = And(b, mask);
47+
const V b_lo = Sub(b, b_hi);
48+
head = Mul(a_hi, b_hi);
49+
rest = MulAdd(a_hi, b_lo, Mul(a_lo, b));
50+
}
51+
3652
} // namespace npsr::HWY_NAMESPACE
3753
HWY_AFTER_NAMESPACE();
3854

npsr/lut-inl.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#define NPSR_LUT_INL_H_
66
#endif
77

8+
#include <limits>
89
#include <tuple>
910

1011
#include "npsr/hwy.h"

npsr/trig/data/approx.h

Lines changed: 1016 additions & 1016 deletions
Large diffs are not rendered by default.

npsr/trig/data/constants.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ template <> inline constexpr double kPiMul2<double>[] = {
3030
template <bool FMA> inline constexpr double kPiDiv16Prec29[] = {
3131
0x1.921fb54442d18p-3, 0x1.1a62633p-57, 0x1.45c06e0e68948p-89, };
3232
template <> inline constexpr double kPiDiv16Prec29<false>[] = {
33-
0x1.921fb54p-3, 0x1.a626331p-61, 0x1.1701b839a252p-91, 0x1.10b461p-33,
33+
0x1.921fb54p-3, 0x1.10b461p-33, 0x1.a626331p-61, 0x1.1701b839a252p-91,
3434
};
3535

3636
template <typename T> inline constexpr char kInvPi = '_';

npsr/trig/extended-inl.h

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ NPSR_INTRIN V Extended(V x) {
212212
// PHASE 7: Convert to Radians
213213
// =============================================================================
214214

215-
// Multiply by π with error compensation (Cody-Waite multiplication)
215+
// Multiply by with error compensation (Cody-Waite multiplication)
216216
constexpr auto kPiMul2 = data::kPiMul2<T>;
217217
const V pi2_hi = Set(d, kPiMul2[0]);
218218
const V pi2_med = Set(d, kPiMul2[1]);
@@ -233,6 +233,14 @@ NPSR_INTRIN V Extended(V x) {
233233
r_lo = Combine(d, DemoteTo(dh, r_lo_w0), DemoteTo(dh, r_lo_w1));
234234
r_w0 = BitCast(d, r0);
235235
r_w1 = BitCast(d, r1);
236+
} else if constexpr (!kNativeFMA) {
237+
// Without FMA, `MulSub(pi2_hi, n, r)` yields zero and drops the 0.5-ulp(r)
238+
// rounding error of r — worth 0.5 ulp of the result near k*pi/2. Recover
239+
// it from the exact head product (head - r is exact by Sterbenz).
240+
V head, rest;
241+
SplitMul(pi2_hi, n, head, rest);
242+
r_lo = Add(Sub(head, r), rest);
243+
r_lo = Add(Mul(pi2_med, n), r_lo);
236244
} else {
237245
r_lo = MulSub(pi2_hi, n, r);
238246
r_lo = MulAdd(pi2_med, n, r_lo);
@@ -272,22 +280,30 @@ NPSR_INTRIN V Extended(V x) {
272280
// =============================================================================
273281
// PHASE 10: Final Assembly
274282
// =============================================================================
275-
V res_lo = NegMulAdd(func_hi, r, deriv);
276-
res_lo = MulAdd(res_lo, r_lo, func_lo);
277283
V res_hi_lo = MulAdd(sigma, r, func_hi);
278-
V res_hi = MulAdd(deriv_hi, r, res_hi_lo);
279-
284+
V res_hi, deriv_hi_r_cor;
285+
if constexpr (!kNativeFMA && !kIsSingle) {
286+
// Same substitution as in Phase 7. sigma is a signed power of two (see
287+
// approx.h) so sigma*r is exact, but deriv_hi*r would round under a
288+
// decayed MulAdd and corrupt the head; SplitMul recovers it.
289+
V dr, dr_rest;
290+
SplitMul(deriv_hi, r, dr, dr_rest);
291+
res_hi = Add(res_hi_lo, dr);
292+
V dr_hi = Sub(res_hi, res_hi_lo);
293+
deriv_hi_r_cor = Add(Sub(dr, dr_hi), dr_rest);
294+
} else {
295+
res_hi = MulAdd(deriv_hi, r, res_hi_lo);
296+
deriv_hi_r_cor = MulAdd(deriv_hi, r, Sub(res_hi_lo, res_hi));
297+
}
280298
V sum_cor = MulAdd(sigma, r, Sub(func_hi, res_hi_lo));
281-
V deriv_hi_r_cor = MulAdd(deriv_hi, r, Sub(res_hi_lo, res_hi));
282-
deriv_hi_r_cor = Add(deriv_hi_r_cor, sum_cor);
283-
res_lo = Add(res_lo, deriv_hi_r_cor);
299+
V res_lo0 = Add(deriv_hi_r_cor, sum_cor);
284300

285301
// Polynomial corrections
286302
V s2 = Set(d, kIsSingle ? 0x1.1110b8p-7f : 0x1.1110fabb3551cp-7);
287303
V s1 = Set(d, kIsSingle ? -0x1.555556p-3f : -0x1.5555555554448p-3);
288304
V sin_poly = MulAdd(s2, r2, s1);
289-
sin_poly = Mul(sin_poly, r);
290305
sin_poly = Mul(sin_poly, r2);
306+
sin_poly = Mul(sin_poly, r);
291307

292308
V c1 = Set(d, kIsSingle ? 0x1.5554f8p-5f : 0x1.5555555554ccfp-5);
293309
const V neg_half = Set(d, static_cast<T>(-0.5));
@@ -301,8 +317,11 @@ NPSR_INTRIN V Extended(V x) {
301317
}
302318
cos_poly = Mul(cos_poly, r2);
303319

304-
res_lo = MulAdd(sin_poly, deriv, res_lo);
305-
res_lo = MulAdd(cos_poly, func_hi, res_lo);
320+
V corr = NegMulAdd(func_hi, r, deriv);
321+
V res_lo_rlo = MulAdd(corr, r_lo, func_lo);
322+
V res_lo_sin = MulAdd(deriv, sin_poly, res_lo0);
323+
V res_lo_cos = MulAdd(func_hi, cos_poly, res_lo_sin);
324+
V res_lo = Add(res_lo_cos, res_lo_rlo);
306325
return Add(res_hi, res_lo);
307326
}
308327
// NOLINTNEXTLINE(google-readability-namespace-comments)

npsr/trig/high-inl.h

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,16 @@ NPSR_INTRIN V High(V x) {
9595
/**
9696
* This function computes sin(x) or cos(x) for |x| < 2^24 using the Cody-Waite
9797
* reduction algorithm combined with table lookup and polynomial approximation,
98-
* achieves < 1 ULP error for |x| < 2^24.
98+
* achieves <= 1 ULP error for |x| < 2^24 (worst case reaches, but does not
99+
* exceed, 1 ULP).
99100
*
100101
* Algorithm Overview:
101102
* 1. Range Reduction: Reduces input x to r where |r| < π/16
102103
* - Computes n = round(x * 16/π) and r = x - n*π/16
103-
* - Uses multi-precision arithmetic (3 parts of π/16) for accuracy
104+
* - Uses multi-word π/16 (3 words with FMA, 4 without) for accuracy
104105
*
105106
* 2. Table Lookup: Retrieves precomputed sin(n*π/16) and cos(n*π/16)
106-
* - Includes high and low precision parts for cos values
107+
* - Stores high and low parts for both sin and cos (lows packed 32:32)
107108
*
108109
* 3. Polynomial Approximation: Computes sin(r) and cos(r)
109110
* - sin(r) ≈ r * (1 + r²*P_sin(r²)) where P_sin is a minimax polynomial
@@ -142,17 +143,16 @@ NPSR_INTRIN V High(V x) {
142143
// Note: cos_lo and sin_lo are packed together (32 bits each) to save memory.
143144
// cos_lo can be used as-is since it's in the upper bits, sin_lo needs
144145
// extraction. The precision loss is negligible for the final result.
145-
// see data/lut-inl.h.sol for the table generation code.
146+
// see data/kpi16-inl.h.sol for the table generation code.
146147
V sin_lo = BitCast(d, ShiftLeft<32>(BitCast(du, cos_lo)));
147148

148149
// Step 3: Multi-precision computation of remainder r
149150
// r = x - n*(π/16)_high
151+
// Without native FMA the non-tail parts carry 27/25/29 bits, so the two
152+
// leading products n*part are exact for |n| < ceil(2^24*16/π) = 85445660.
153+
// part2 and the tail round, but the idioms below capture that rounding.
150154
constexpr auto kPiDiv16Prec29 = data::kPiDiv16Prec29<kNativeFMA>;
151155
V r_hi = NegMulAdd(n, Set(d, kPiDiv16Prec29[0]), x);
152-
if constexpr (!kNativeFMA) {
153-
// For F64, we need to handle the low precision part separately
154-
r_hi = NegMulAdd(n, Set(d, kPiDiv16Prec29[3]), r_hi);
155-
}
156156
const V pi16_med = Set(d, kPiDiv16Prec29[1]);
157157
const V pi16_lo = Set(d, kPiDiv16Prec29[2]);
158158
V r_med = NegMulAdd(n, pi16_med, r_hi);
@@ -162,6 +162,15 @@ NPSR_INTRIN V High(V x) {
162162
V term = NegMulAdd(pi16_med, n, Sub(r_hi, r_med));
163163
V r_lo = MulAdd(pi16_lo, n, Sub(r, r_med));
164164
r_lo = Sub(term, r_lo);
165+
if constexpr (!kNativeFMA) {
166+
// Fourth piece (48 bits at 2^-91). Reusing the one rounded product in both
167+
// the subtraction and its error capture keeps (r, r_lo) an exact
168+
// double-double even when r is tiny near k*π/2.
169+
const V tail_prod = Mul(n, Set(d, kPiDiv16Prec29[3]));
170+
const V r_prev = r;
171+
r = Sub(r_prev, tail_prod);
172+
r_lo = Add(r_lo, Sub(Sub(r_prev, r), tail_prod));
173+
}
165174

166175
// Step 4: Polynomial approximation
167176
V r2 = Mul(r, r);
@@ -210,13 +219,23 @@ NPSR_INTRIN V High(V x) {
210219
if constexpr (OP == Operation::kCos) {
211220
// Cosine reconstruction: cos_table - sin_table*remainder
212221
// Equivalent to: cos(a)*cos(r) - sin(a)*sin(r) but more efficient
213-
V res_hi = NegMulAdd(r, sin_hi, cos_hi); // cos_hi - r*sin_hi
214-
215-
// This captures the precision lost in the main computation
216-
V r_sin_hi = Sub(cos_hi, res_hi); // Extract high part of multiplication
217-
218-
// Handles rounding errors and adds sin_low contribution
219-
V r_sin_low = MulSub(r, sin_hi, r_sin_hi); // Compute multiplication error
222+
V res_hi, r_sin_low;
223+
if constexpr (kNativeFMA) {
224+
res_hi = NegMulAdd(r, sin_hi, cos_hi); // cos_hi - r*sin_hi
225+
226+
// This captures the precision lost in the main computation
227+
V r_sin_hi = Sub(cos_hi, res_hi); // Extract high part of multiplication
228+
229+
// Handles rounding errors and adds the low-part contribution
230+
r_sin_low = MulSub(r, sin_hi, r_sin_hi); // Compute multiplication error
231+
} else {
232+
// Dekker split stands in for the FMA idiom
233+
V r_sin, r_sin_rest;
234+
SplitMul(r, sin_hi, r_sin, r_sin_rest);
235+
res_hi = Sub(cos_hi, r_sin);
236+
V r_sin_hi = Sub(cos_hi, res_hi);
237+
r_sin_low = Add(Sub(r_sin, r_sin_hi), r_sin_rest);
238+
}
220239
V sin_low_corr = MulAdd(r, sin_lo, r_sin_low); // Add sin_low term
221240

222241
// This is used to apply the low-precision remainder correction
@@ -245,13 +264,23 @@ NPSR_INTRIN V High(V x) {
245264
} else {
246265
// Sine reconstruction: sin_table + cos_table*remainder
247266
// Equivalent to: sin(a)*cos(r) + cos(a)*sin(r) but more efficient
248-
V res_hi = MulAdd(r, cos_hi, sin_hi); // sin_hi + r*cos_hi
249-
250-
// This captures the precision lost in the main computation
251-
V r_cos_hi = Sub(res_hi, sin_hi); // Extract high part of multiplication
252-
253-
// Handles rounding errors and adds cos_low contribution
254-
V r_cos_low = MulSub(r, cos_hi, r_cos_hi); // Compute multiplication error
267+
V res_hi, r_cos_low;
268+
if constexpr (kNativeFMA) {
269+
res_hi = MulAdd(r, cos_hi, sin_hi); // sin_hi + r*cos_hi
270+
271+
// This captures the precision lost in the main computation
272+
V r_cos_hi = Sub(res_hi, sin_hi); // Extract high part of multiplication
273+
274+
// Handles rounding errors and adds the low-part contribution
275+
r_cos_low = MulSub(r, cos_hi, r_cos_hi); // Compute multiplication error
276+
} else {
277+
// Dekker split stands in for the FMA idiom
278+
V r_cos, r_cos_rest;
279+
SplitMul(r, cos_hi, r_cos, r_cos_rest);
280+
res_hi = Add(sin_hi, r_cos);
281+
V r_cos_hi = Sub(res_hi, sin_hi);
282+
r_cos_low = Add(Sub(r_cos, r_cos_hi), r_cos_rest);
283+
}
255284
V cos_low_corr = MulAdd(r, cos_lo, r_cos_low); // Add cos_low term
256285

257286
// Intermediate term for r_low correction: cos_table - sin_table*r

npsr/trig/inl.h

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
// configurable precision, special case handling, and algorithm selection
44
//
55
// The implementation automatically selects between three algorithms:
6-
// 1. Low precision: ~1-4 ULP error, fastest
6+
// 1. Low precision: ~2-3 ULP error, fastest
77
// 2. High precision: ~1 ULP error, moderate speed
8-
// 3. Extended precision: Exact for |x| > 2^24 (float) or 2^53 (double)
8+
// 3. Extended precision: Payne-Hanek reduction for huge |x|
9+
// (> 10^4 for float, > 2^24 for double)
910

1011
#if defined(NPSR_TRIG_INL_H_) == defined(HWY_TARGET_TOGGLE) // NOLINT
1112
#ifdef NPSR_TRIG_INL_H_
@@ -46,7 +47,8 @@ namespace npsr::HWY_NAMESPACE::trig {
4647
*
4748
* Thresholds for extended precision:
4849
* - Float: |x| > 10,000 (empirically chosen for accuracy)
49-
* - Double: |x| > 2^24 (16,777,216 - where 53-bit mantissa loses precision)
50+
* - Double: |x| > 2^24 (16,777,216 - beyond this the reduction's n*π products
51+
* stop being exactly representable)
5052
*/
5153
template <Operation OP, typename Prec, typename V>
5254
NPSR_INTRIN V Trig(Prec &prec, V x) {
@@ -56,8 +58,8 @@ NPSR_INTRIN V Trig(Prec &prec, V x) {
5658
V ret;
5759
// Step 1: Select base algorithm based on accuracy requirements
5860
if constexpr (Prec::kLowAccuracy) {
59-
// Low precision: Cody-Waite reduction with degree-9 polynomial
60-
// Error: ~2 ULP and 3~ for non-fma
61+
// Low precision: Cody-Waite reduction, degree-9 (f32) / degree-15 (f64)
62+
// Error: ~2-3 ULP
6163
ret = Low<OP>(x);
6264
} else {
6365
// High precision: π/16 reduction with table lookup + polynomial
@@ -78,11 +80,16 @@ NPSR_INTRIN V Trig(Prec &prec, V x) {
7880
// For |x| > threshold, standard algorithms lose precision due to
7981
// catastrophic cancellation in x - n*π reduction
8082
if constexpr (Prec::kLargeArgument) {
81-
// Thresholds chosen based on when standard reduction loses accuracy:
82-
// - Float: 10,000 is conservative but ensures < 1 ULP error
83-
// - Double: 2^24 is where mantissa can't represent x and x-2π distinctly
83+
// Thresholds mark where each path's n*π products stop being exact:
84+
// - Float: 10,000, conservative but keeps the widened path < 1 ULP.
85+
// - Double: 2^24, past which |round(x*16/π)| overflows the π/16 split.
86+
// - Double low-accuracy cos: holds until trunc(|x|/π) > 8388606; the
87+
// constant is the largest double below that boundary (~8388607π).
88+
constexpr bool kIsLowCos =
89+
Prec::kLowAccuracy && OP == trig::Operation::kCos;
90+
constexpr double kLargeF64 = kIsLowCos ? 0x1.921fb2200366fp+24 : 16777216.0;
8491
auto has_large_arg =
85-
And(Gt(Abs(x), Set(d, kIsSingle ? 10000.0f : 16777216.0)), is_finite);
92+
And(Gt(Abs(x), Set(d, kIsSingle ? 10000.0f : kLargeF64)), is_finite);
8693

8794
// Extended precision is expensive, only use when necessary
8895
if (HWY_UNLIKELY(!AllFalse(d, has_large_arg))) {

npsr/trig/low-inl.h

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,17 @@ NPSR_INTRIN V PolyLow(V r, V r2) {
5656
using namespace hn;
5757

5858
const DFromV<V> d;
59-
const V c15 = Set(d, -0x1.9f1517e9f65fp-41);
60-
const V c13 = Set(d, 0x1.60e6bee01d83ep-33);
61-
const V c11 = Set(d, -0x1.ae6355aaa4a53p-26);
62-
const V c9 = Set(d, 0x1.71de3806add1ap-19);
63-
const V c7 = Set(d, -0x1.a01a019a659ddp-13);
64-
const V c5 = Set(d, 0x1.111111110a573p-7);
65-
const V c3 = Set(d, -0x1.55555555554a8p-3);
59+
// Both ops fit sin(r) on the same interval, so the two coefficient sets
60+
// differ only in trailing bits; each matches its SVML counterpart
61+
// (__svml_sin_d_la / __svml_cos_d_la) bit-for-bit.
62+
constexpr bool kCos = OP == Operation::kCos;
63+
const V c15 = Set(d, kCos ? -0x1.9f0d60811aac8p-41 : -0x1.9f1517e9f65fp-41);
64+
const V c13 = Set(d, kCos ? 0x1.60e6857a2f220p-33 : 0x1.60e6bee01d83ep-33);
65+
const V c11 = Set(d, kCos ? -0x1.ae63546002231p-26 : -0x1.ae6355aaa4a53p-26);
66+
const V c9 = Set(d, kCos ? 0x1.71de38030fea0p-19 : 0x1.71de3806add1ap-19);
67+
const V c7 = Set(d, kCos ? -0x1.a01a019a5b87bp-13 : -0x1.a01a019a659ddp-13);
68+
const V c5 = Set(d, kCos ? 0x1.111111110a4a8p-7 : 0x1.111111110a573p-7);
69+
const V c3 = Set(d, kCos ? -0x1.55555555554a7p-3 : -0x1.55555555554a8p-3);
6670
V poly = MulAdd(c15, r2, c13);
6771
poly = MulAdd(r2, poly, c11);
6872
poly = MulAdd(r2, poly, c9);
@@ -110,26 +114,28 @@ NPSR_INTRIN V Low(V x) {
110114
// N' = N - 0.5
111115
n = Sub(n, Set(d, static_cast<T>(0.5)));
112116
}
113-
// Use Cody-Waite method with triple-precision PI
117+
// Cody-Waite reduction with multi-word π (3 words with FMA, 4 without)
114118
constexpr auto kPi = data::kPi<T, kNativeFMA>;
115-
116119
V r = NegMulAdd(n, Set(d, kPi[0]), x_abs);
117120
r = NegMulAdd(n, Set(d, kPi[1]), r);
118121
V r_lo = NegMulAdd(n, Set(d, kPi[2]), r);
122+
119123
if constexpr (!kNativeFMA) {
120-
if (!kIsSingle) {
121-
r = r_lo;
122-
}
123124
r_lo = NegMulAdd(n, Set(d, kPi[3]), r_lo);
124125
}
125-
126-
if constexpr (kIsSingle) {
126+
if constexpr (kIsSingle || !kNativeFMA) {
127+
// The polynomial needs the fully reduced value; r still owes the last
128+
// π word(s).
127129
r = r_lo;
128130
}
131+
129132
V r2 = Mul(r, r);
130133
V poly = PolyLow<OP>(r, r2);
131134

132135
if constexpr (!kIsSingle) {
136+
// Non-FMA has r == r_lo, giving the plain r + r³·poly. With FMA this is
137+
// r_lo·(1 + r²·poly) ≈ sin(r_lo): r and r_lo differ by n·π[2] (~2^-83)
138+
// and sin(r)/r varies slowly, so the error stays sub-ULP.
133139
V r2_corr = Mul(r2, r_lo);
134140
poly = MulAdd(r2_corr, poly, r_lo);
135141
}

0 commit comments

Comments
 (0)