@@ -92,6 +92,30 @@ NPSR_INTRIN V High(V x) {
9292 }
9393 return poly;
9494}
95+ /* *
96+ * Substitute for the FMA product-error idiom on targets without native FMA.
97+ *
98+ * With FMA, `e = MulSub(a, b, Round(a*b))` recovers the rounding error of the
99+ * product exactly. Without it, that expression only sees the already-rounded
100+ * product and returns nothing useful. Instead, split both operands with a bit
101+ * mask (26 significant bits in the head) so `head = a_hi*b_hi` is exact by
102+ * construction, and return the dropped cross terms in `rest`:
103+ * a*b == head + rest, up to a negligible ~2^-105-scale rounding of `rest`.
104+ */
105+ template <typename V, HWY_IF_F64 (TFromV<V>)>
106+ NPSR_INTRIN void SplitMul (V a, V b, V& head, V& rest) {
107+ using namespace hn ;
108+ const DFromV<V> d;
109+ const RebindToUnsigned<decltype (d)> du;
110+ const V mask = BitCast (d, Set (du, 0xFFFFFFFFF8000000u ));
111+ const V a_hi = And (a, mask);
112+ const V a_lo = Sub (a, a_hi);
113+ const V b_hi = And (b, mask);
114+ const V b_lo = Sub (b, b_hi);
115+ head = Mul (a_hi, b_hi);
116+ rest = MulAdd (a_hi, b_lo, Mul (a_lo, b));
117+ }
118+
95119/* *
96120 * This function computes sin(x) or cos(x) for |x| < 2^24 using the Cody-Waite
97121 * reduction algorithm combined with table lookup and polynomial approximation,
@@ -147,12 +171,13 @@ NPSR_INTRIN V High(V x) {
147171
148172 // Step 3: Multi-precision computation of remainder r
149173 // r = x - n*(π/16)_high
174+ // Both splits apply their parts in descending order through the same DAG.
175+ // Without native FMA the non-tail parts carry 27/27/29 bits so every
176+ // n*part product is exact for |n| <= 85445660 = round(2^24 * 16/π); each
177+ // subtraction's rounding error is then captured exactly below, and the
178+ // decayed mul+sub sequence loses nothing against the fused path.
150179 constexpr auto kPiDiv16Prec29 = data::kPiDiv16Prec29 <kNativeFMA >;
151180 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- }
156181 const V pi16_med = Set (d, kPiDiv16Prec29 [1 ]);
157182 const V pi16_lo = Set (d, kPiDiv16Prec29 [2 ]);
158183 V r_med = NegMulAdd (n, pi16_med, r_hi);
@@ -162,6 +187,15 @@ NPSR_INTRIN V High(V x) {
162187 V term = NegMulAdd (pi16_med, n, Sub (r_hi, r_med));
163188 V r_lo = MulAdd (pi16_lo, n, Sub (r, r_med));
164189 r_lo = Sub (term, r_lo);
190+ if constexpr (!kNativeFMA ) {
191+ // Fourth piece (53 bits at 2^-91). Reuse the one rounded product in both
192+ // the subtraction and its error capture so (r, r_lo) stays an exact
193+ // double-double of the reduction even when r is tiny near k*π/2.
194+ const V tail_prod = Mul (n, Set (d, kPiDiv16Prec29 [3 ]));
195+ const V r_prev = r;
196+ r = Sub (r_prev, tail_prod);
197+ r_lo = Add (r_lo, Sub (Sub (r_prev, r), tail_prod));
198+ }
165199
166200 // Step 4: Polynomial approximation
167201 V r2 = Mul (r, r);
@@ -210,13 +244,23 @@ NPSR_INTRIN V High(V x) {
210244 if constexpr (OP == Operation::kCos ) {
211245 // Cosine reconstruction: cos_table - sin_table*remainder
212246 // 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
247+ V res_hi, r_sin_low;
248+ if constexpr (kNativeFMA ) {
249+ res_hi = NegMulAdd (r, sin_hi, cos_hi); // cos_hi - r*sin_hi
250+
251+ // This captures the precision lost in the main computation
252+ V r_sin_hi = Sub (cos_hi, res_hi); // Extract high part of multiplication
253+
254+ // Handles rounding errors via the FMA product-error idiom
255+ r_sin_low = MulSub (r, sin_hi, r_sin_hi); // Compute multiplication error
256+ } else {
257+ // Exact-by-construction head product replaces the FMA idiom
258+ V r_sin, r_sin_rest;
259+ SplitMul (r, sin_hi, r_sin, r_sin_rest);
260+ res_hi = Sub (cos_hi, r_sin);
261+ V r_sin_hi = Sub (cos_hi, res_hi);
262+ r_sin_low = Add (Sub (r_sin, r_sin_hi), r_sin_rest);
263+ }
220264 V sin_low_corr = MulAdd (r, sin_lo, r_sin_low); // Add sin_low term
221265
222266 // This is used to apply the low-precision remainder correction
@@ -245,13 +289,23 @@ NPSR_INTRIN V High(V x) {
245289 } else {
246290 // Sine reconstruction: sin_table + cos_table*remainder
247291 // 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
292+ V res_hi, r_cos_low;
293+ if constexpr (kNativeFMA ) {
294+ res_hi = MulAdd (r, cos_hi, sin_hi); // sin_hi + r*cos_hi
295+
296+ // This captures the precision lost in the main computation
297+ V r_cos_hi = Sub (res_hi, sin_hi); // Extract high part of multiplication
298+
299+ // Handles rounding errors via the FMA product-error idiom
300+ r_cos_low = MulSub (r, cos_hi, r_cos_hi); // Compute multiplication error
301+ } else {
302+ // Exact-by-construction head product replaces the FMA idiom
303+ V r_cos, r_cos_rest;
304+ SplitMul (r, cos_hi, r_cos, r_cos_rest);
305+ res_hi = Add (sin_hi, r_cos);
306+ V r_cos_hi = Sub (res_hi, sin_hi);
307+ r_cos_low = Add (Sub (r_cos, r_cos_hi), r_cos_rest);
308+ }
255309 V cos_low_corr = MulAdd (r, cos_lo, r_cos_low); // Add cos_low term
256310
257311 // Intermediate term for r_low correction: cos_table - sin_table*r
@@ -282,6 +336,9 @@ NPSR_INTRIN V High(V x) {
282336 // This unified approach works because:
283337 // - sin(x + π) = -sin(x)
284338 // - cos(x + π) = -cos(x)
339+ // TODO(seiko2plus): x_sign_int and the Xor are dead code (always zero) and
340+ // should be removed. The sign is bit 4 of n_biased alone; sin(-0.0) is
341+ // handled in trig/inl.h under kSpecialCases.
285342 VU x_sign_int = ShiftLeft<63 >(BitCast (du, x));
286343 // XOR with quadrant info in n_biased
287344 VU combined = Xor (BitCast (du, n_biased), ShiftLeft<4 >(x_sign_int));
0 commit comments