@@ -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
0 commit comments