Skip to content

Commit eb24f8b

Browse files
committed
Fix small argument handling and add cosine support for double precision
- Implement efficient SVML-style linear approximation for sin/cos - Fix precision issues in Cody-Waite range reduction - Add proper sign handling for both sine and cosine - Improve polynomial evaluation with correction terms - Document mathematical equivalence between traditional and optimized approaches
1 parent a672683 commit eb24f8b

1 file changed

Lines changed: 146 additions & 80 deletions

File tree

npsr/trig/small-inl.h

Lines changed: 146 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#include "npsr/common.h"
2-
#include "npsr/trig/lut-inl.h"
2+
#include "npsr/trig/data/small.h"
33
#include "npsr/utils-inl.h"
44

5-
#if defined(NPSR_TRIG_SMALL_INL_H_) == defined(HWY_TARGET_TOGGLE) // NOLINT
5+
#if defined(NPSR_TRIG_SMALL_INL_H_) == defined(HWY_TARGET_TOGGLE) // NOLINT
66
#ifdef NPSR_TRIG_SMALL_INL_H_
77
#undef NPSR_TRIG_SMALL_INL_H_
88
#else
@@ -67,25 +67,22 @@ HWY_API V SmallPolyLow(V r, V r2) {
6767
poly = MulAdd(r2, poly, c7);
6868
poly = MulAdd(r2, poly, c5);
6969
poly = MulAdd(r2, poly, c3);
70-
V r3 = Mul(r2, r);
71-
poly = MulAdd(r3, poly, r);
7270
return poly;
7371
}
7472

75-
template <bool IS_COS, typename V> HWY_API V SmallArgLow(V x) {
73+
template <bool IS_COS, typename V>
74+
HWY_API V SmallArgLow(V x) {
7675
const DFromV<V> d;
7776
const RebindToUnsigned<decltype(d)> du;
78-
using U = VFromD<decltype(du)>;
7977
using T = TFromV<V>;
8078
// Load frequently used constants as vector registers
8179
const V abs_mask = BitCast(d, Set(du, SignMask<T>() - 1));
8280
const V x_abs = And(abs_mask, x);
83-
const V x_sign = AndNot(abs_mask, x);
84-
85-
constexpr bool IsSingle = std::is_same_v<T, float>;
81+
const V x_sign = AndNot(x_abs, x);
8682

83+
constexpr bool kIsSingle = std::is_same_v<T, float>;
8784
// Transform cosine to sine using identity: cos(x) = sin(x + π/2)
88-
const V half_pi = Set(d, 0x1.921fb6p0f);
85+
const V half_pi = Set(d, kIsSingle ? 0x1.921fb6p0f : 0x1.921fb54442d18p0);
8986
V x_trans = x_abs;
9087
if constexpr (IS_COS) {
9188
x_trans = Add(x_abs, half_pi);
@@ -95,11 +92,10 @@ template <bool IS_COS, typename V> HWY_API V SmallArgLow(V x) {
9592

9693
// Compute N = round(x/π) using "magic number" technique
9794
// and stores integer part in mantissa
98-
const V inv_pi = Set(d, IsSingle ? 0x1.45f306p-2f : 0x1.45f306dc9c883p-2);
99-
const V magic_round = Set(d, IsSingle ? 0x1.8p23f : 0x1.8p52);
95+
const V inv_pi = Set(d, kIsSingle ? 0x1.45f306p-2f : 0x1.45f306dc9c883p-2);
96+
const V magic_round = Set(d, kIsSingle ? 0x1.8p23f : 0x1.8p52);
10097
V n_biased = MulAdd(x_trans, inv_pi, magic_round);
10198
V n = Sub(n_biased, magic_round);
102-
10399
// Adjust quotient for cosine (accounts for π/2 phase shift)
104100
if constexpr (IS_COS) {
105101
// For cosine, we computed N = round((x + π/2)/π) but need N' for x:
@@ -109,20 +105,25 @@ template <bool IS_COS, typename V> HWY_API V SmallArgLow(V x) {
109105
n = Sub(n, Set(d, static_cast<T>(0.5)));
110106
}
111107
// Use Cody-Waite method with triple-precision PI
112-
const V pi_hi = Set(d, IsSingle ? 0x1.921fb6p1f : 0x1.921fb54442d18p+1);
113-
const V pi_med = Set(d, -0x1.777a5cp-24f);
114-
const V pi_lo = Set(d, IsSingle ? -0x1.ee59dap-49f : 0x1.1a62633145c06p-53);
115-
V r = NegMulAdd(n, pi_hi, x_abs); // x - N*π_hi
116-
if constexpr (IsSingle) {
117-
r = NegMulAdd(n, pi_med, r); // - N*π_medium
108+
const V pi_hi = Set(d, kIsSingle ? 0x1.921fb6p1f : 0x1.921fb54442d18p+1);
109+
const V pi_med =
110+
Set(d, kIsSingle ? -0x1.777a5cp-24f : 0x1.c1cd129024e09p-106);
111+
const V pi_lo = Set(d, kIsSingle ? -0x1.ee59dap-49f : 0x1.1a62633145c06p-53);
112+
V r = NegMulAdd(n, pi_hi, x_abs);
113+
if constexpr (kIsSingle) {
114+
r = NegMulAdd(n, pi_med, r);
118115
}
119-
r = NegMulAdd(n, pi_lo, r); // - N*π_low
116+
r = NegMulAdd(n, pi_lo, r);
120117
V r2 = Mul(r, r);
121-
// Extract octant sign information from quotient
122-
// to determines sign flip in final result
123-
r = Xor(r, BitCast(d, ShiftLeft<sizeof(T) * 8 - 1>(BitCast(du, n_biased))));
124-
125118
V poly = SmallPolyLow<IS_COS>(r, r2);
119+
if constexpr (!kIsSingle) {
120+
V r_mid = NegMulAdd(n, pi_med, r);
121+
V r2_corr = Mul(r2, r_mid);
122+
poly = MulAdd(r2_corr, poly, r_mid);
123+
}
124+
// Extract octant sign information from quotient and flip the sign bit
125+
poly = Xor(poly,
126+
BitCast(d, ShiftLeft<sizeof(T) * 8 - 1>(BitCast(du, n_biased))));
126127
if constexpr (IS_COS) {
127128
poly = IfThenElse(is_cos_near_zero, Set(d, static_cast<T>(1.0)), poly);
128129
} else {
@@ -139,18 +140,16 @@ HWY_INLINE V SmallArg(V x) {
139140
using DU = RebindToUnsigned<D>;
140141
using DH = Half<D>;
141142
using DW = RepartitionToWide<D>;
142-
using VU = Vec<DU>;
143143
using VW = Vec<DW>;
144144

145145
const D d;
146146
const DU du;
147147
const DH dh;
148148
const DW dw;
149-
150149
// Load frequently used constants as vector registers
151150
const V abs_mask = BitCast(d, Set(du, 0x7FFFFFFF));
152151
const V x_abs = And(abs_mask, x);
153-
const V x_sign = AndNot(abs_mask, x);
152+
const V x_sign = AndNot(x_abs, x);
154153

155154
// Transform cosine to sine using identity: cos(x) = sin(x + π/2)
156155
const V half_pi = Set(d, 0x1.921fb6p0f);
@@ -236,6 +235,10 @@ HWY_INLINE V SmallArg(V x) {
236235
*/
237236
template <bool IS_COS, typename V, HWY_IF_F64(TFromV<V>)>
238237
HWY_INLINE V SmallArg(V x) {
238+
using trig::data::kHiCosKPi16Table;
239+
using trig::data::kHiSinKPi16Table;
240+
using trig::data::kPackedLowSinCosKPi16Table;
241+
239242
using T = TFromV<V>;
240243
using D = DFromV<V>;
241244
using DU = RebindToUnsigned<D>;
@@ -245,11 +248,10 @@ HWY_INLINE V SmallArg(V x) {
245248
const DU du;
246249

247250
// Constants for range reduction
248-
constexpr T kInvPi = 0x1.45f306dc9c883p2; // 16/π for range reduction
249-
constexpr T kPi16High = 0x1.921fb54442d18p-3; // π/16 high precision part
250-
constexpr T kPi16Low = 0x1.1a62633p-57; // π/16 low precision part
251-
constexpr T kPi16Tiny = 0x1.45c06e0e68948p-89; // π/16 tiny precision part
252-
251+
constexpr T kInvPi = 0x1.45f306dc9c883p2; // 16/π for range reduction
252+
constexpr T kPi16High = 0x1.921fb54442d18p-3; // π/16 high precision part
253+
constexpr T kPi16Low = 0x1.1a62633p-57; // π/16 low precision part
254+
constexpr T kPi16Tiny = 0x1.45c06e0e68948p-89; // π/16 tiny precision part
253255
// Step 1: Range reduction - find n such that x = n*(π/16) + r, where |r| <
254256
// π/16
255257
V magic = Set(d, 0x1.8p52);
@@ -258,21 +260,26 @@ HWY_INLINE V SmallArg(V x) {
258260

259261
// Extract integer index for table lookup (n mod 16)
260262
VU n_int = BitCast(du, n_biased);
263+
VU table_idx = And(n_int, Set(du, 0xF)); // Mask to get n mod 16
261264

262265
// Step 2: Load precomputed sine/cosine values for n mod 16
263-
V sin_hi = LutX2(kHiSinKPi16Table<double>, n_int);
264-
V cos_hi = LutX2(kHiCosKPi16Table<double>, n_int);
265-
V cos_lo = LutX2(kPackedLowSinCosKPi16Table<double>, n_int);
266+
V sin_hi = LutX2(kHiSinKPi16Table, table_idx);
267+
V cos_hi = LutX2(kHiCosKPi16Table, table_idx);
268+
// Note: cos_lo and sin_lo are packed together (32 bits each) to save memory.
269+
// cos_lo can be used as-is since it's in the upper bits, sin_lo needs
270+
// extraction. The precision loss is negligible for the final result.
271+
// see lut-inl.h.py for the table generation code.
272+
V cos_lo = LutX2(kPackedLowSinCosKPi16Table, table_idx);
273+
// Extract sin_low from packed format (upper 32 bits)
274+
V sin_lo = BitCast(d, ShiftLeft<32>(BitCast(du, cos_lo)));
266275

267276
// Step 3: Multi-precision computation of remainder r
268-
V r_hi = NegMulAdd(n, Set(d, kPi16High), x); // r = x - n*(π/16)_high
269-
V r_mid = NegMulAdd(n, Set(d, kPi16Low), r_hi); // Subtract low part
270-
V r = NegMulAdd(n, Set(d, kPi16Tiny), r_mid); // Subtract tiny part
271-
277+
V r_hi = NegMulAdd(n, Set(d, kPi16High), x); // r = x - n*(π/16)_high
278+
V r_mid = NegMulAdd(n, Set(d, kPi16Low), r_hi); // Subtract low part
279+
V r = NegMulAdd(n, Set(d, kPi16Tiny), r_mid); // Subtract tiny part
272280
// Compute low precision part of r for extra accuracy
273-
V delta = Sub(r, r_mid);
274-
V term = NegMulAdd(Set(d, kPi16Low), n, Sub(r_hi, delta));
275-
V r_lo = MulAdd(Set(d, kPi16Tiny), n, delta);
281+
V term = NegMulAdd(Set(d, kPi16Low), n, Sub(r_hi, r_mid));
282+
V r_lo = MulAdd(Set(d, kPi16Tiny), n, Sub(r, r_mid));
276283
r_lo = Sub(term, r_lo);
277284

278285
// Step 4: Polynomial approximation
@@ -295,58 +302,117 @@ HWY_INLINE V SmallArg(V x) {
295302
cos_poly = MulAdd(cos_poly, r2, Set(d, -0x1.ffffffffffffcp-2));
296303

297304
// Step 5: Reconstruction using angle addition formulas
298-
// sin(n*π/16 + r) = sin(n*π/16)*cos(r) + cos(n*π/16)*sin(r)
299-
// cos(n*π/16 + r) = cos(n*π/16)*cos(r) - sin(n*π/16)*sin(r)
300305
//
301-
// Where:
302-
// sin(r) = r * (1 + sin_poly)
303-
// cos(r) = 1 + r² * cos_poly
306+
// Mathematical equivalence between traditional and SVML approaches:
307+
//
308+
// Traditional angle addition:
309+
// sin(a+r) = sin(a)*cos(r) + cos(a)*sin(r)
310+
// cos(a+r) = cos(a)*cos(r) - sin(a)*sin(r)
311+
//
312+
// Where for small r (|r| < π/16):
313+
// cos(r) ≈ 1 + r²*cos_poly
314+
// sin(r) ≈ r*(1 + sin_poly) ≈ r + r*sin_poly
315+
//
316+
// SVML's efficient linear approximation:
317+
// sin(a+r) ≈ sin(a) + cos(a)*r + polynomial_corrections
318+
// cos(a+r) ≈ cos(a) - sin(a)*r + polynomial_corrections
319+
//
320+
// This is mathematically equivalent but computationally more efficient:
321+
// - Uses first-order linear terms directly: Sh + Ch*R, Ch - R*Sh
322+
// - Applies higher-order polynomial corrections separately
323+
// - Fewer multiplications and better numerical stability
324+
//
325+
// Implementation follows SVML structure:
326+
// sin(n*π/16 + r) = sin_table + cos_table*remainder (+ corrections)
327+
// cos(n*π/16 + r) = cos_table - sin_table*remainder (+ corrections)
328+
V result;
329+
if constexpr (IS_COS) {
330+
// Cosine reconstruction: cos_table - sin_table*remainder
331+
// Equivalent to: cos(a)*cos(r) - sin(a)*sin(r) but more efficient
332+
V res_hi = NegMulAdd(r, sin_hi, cos_hi); // cos_hi - r*sin_hi
304333

305-
// Apply angle addition with multi-precision arithmetic
306-
// Main term: sin(n*π/16) + r*cos(n*π/16)
307-
V res_hi = MulAdd(r, cos_hi, sin_hi);
334+
// This captures the precision lost in the main computation
335+
V r_sin_hi = Sub(cos_hi, res_hi); // Extract high part of multiplication
308336

309-
// Compute error from r*cos_hi multiplication
310-
V r_cos = Sub(res_hi, sin_hi);
311-
V mul_err = MulSub(r, cos_hi, r_cos);
337+
// Handles rounding errors and adds sin_low contribution
338+
V r_sin_low = MulSub(r, sin_hi, r_sin_hi); // Compute multiplication error
339+
V sin_low_corr = MulAdd(r, sin_lo, r_sin_low); // Add sin_low term
312340

313-
// Compute cos(n*π/16) - r*sin(n*π/16) for intermediate calculations
314-
V cos_r_sin = NegMulAdd(r, sin_hi, cos_hi);
341+
// This is used to apply the low-precision remainder correction
342+
V sin_cos_r = MulAdd(r, cos_hi, sin_hi);
315343

316-
// Extract sin_low from packed format (upper 32 bits)
317-
V sin_lo = BitCast(d, ShiftLeft<32>(BitCast(du, cos_lo)));
318-
V lo_corr = MulAdd(r_lo, cos_r_sin, sin_lo);
344+
// Main low precision correction: cos_low - r_low*(sin_table + cos_table*r)
345+
// Applies the effect of the low-precision remainder on the final result
346+
V low_corr = NegMulAdd(r_lo, sin_cos_r, cos_lo);
319347

320-
// Apply polynomial corrections
321-
V r_cos_hi = Mul(cos_hi, r);
322-
V sin_corr = Mul(sin_hi, cos_poly); // sin(n*π/16) * (cos(r)-1)/r²
348+
// Polynomial corrections using the remainder
349+
V r_sin = Mul(r, sin_hi); // For polynomial application
323350

324-
// Extract cos_low from packed format (lower 32 bits used directly)
325-
V cos_corr = MulAdd(r, cos_lo, mul_err);
326-
V total_corr = Add(cos_corr, lo_corr);
351+
// Apply polynomial corrections: cos_table*cos_poly - r*sin_table*sin_poly
352+
// This handles the higher-order terms from cos(r) and sin(r) expansions
353+
V poly_corr = Mul(cos_hi, cos_poly); // cos(a) * (cos(r)-1)/r²
354+
// - sin(a)*r * (sin(r)/r-1)
355+
poly_corr = NegMulAdd(r_sin, sin_poly, poly_corr);
327356

328-
// Combine all terms: sin(n*π/16 + r) ≈ sin(n*π/16) + r*cos(n*π/16) +
329-
// corrections
330-
V result = MulAdd(r_cos_hi, sin_poly, sin_corr);
331-
result = MulAdd(r2, result, total_corr);
332-
result = Add(res_hi, result);
357+
// Combine all low precision corrections
358+
V total_low = Sub(low_corr, sin_low_corr);
333359

334-
// Handle sign for negative zero edge case
335-
if constexpr (IS_COS) {
336-
// For cosine, we need to adjust the phase by π/2
337-
// This would require additional logic not shown in the original
338-
// TODO: Implement cosine-specific adjustments
360+
// Final assembly: main_term + r²*polynomial_corrections + low_corrections
361+
result = MulAdd(r2, poly_corr, total_low);
362+
result = Add(res_hi, result);
363+
364+
} else {
365+
// Sine reconstruction: sin_table + cos_table*remainder
366+
// Equivalent to: sin(a)*cos(r) + cos(a)*sin(r) but more efficient
367+
V res_hi = MulAdd(r, cos_hi, sin_hi); // sin_hi + r*cos_hi
368+
369+
// This captures the precision lost in the main computation
370+
V r_cos_hi = Sub(res_hi, sin_hi); // Extract high part of multiplication
371+
372+
// Handles rounding errors and adds cos_low contribution
373+
V r_cos_low = MulSub(r, cos_hi, r_cos_hi); // Compute multiplication error
374+
V cos_low_corr = MulAdd(r, cos_lo, r_cos_low); // Add cos_low term
375+
376+
// Intermediate term for r_low correction: cos_table - sin_table*r
377+
// This is used to apply the low-precision remainder correction
378+
V cos_r_sin = NegMulAdd(r, sin_hi, cos_hi);
379+
380+
// Main low precision correction: sin_low - r_low*(cos_table - sin_table*r)
381+
// Applies the effect of the low-precision remainder on the final result
382+
V low_corr = MulAdd(r_lo, cos_r_sin, sin_lo);
383+
// Polynomial corrections using the remainder
384+
V r_cos = Mul(r, cos_hi); // For polynomial application
385+
386+
// Apply polynomial corrections: sin_table*cos_poly + r*cos_table*sin_poly
387+
// This handles the higher-order terms from cos(r) and sin(r) expansions
388+
V poly_corr = Mul(sin_hi, cos_poly); // sin(a) * (cos(r)-1)/r²
389+
poly_corr =
390+
MulAdd(r_cos, sin_poly, poly_corr); // + cos(a)*r * (sin(r)/r-1)
391+
392+
// Combine all low precision corrections
393+
V total_low = Add(low_corr, cos_low_corr);
394+
// Final assembly: main_term + r²*polynomial_corrections + low_corrections
395+
result = MulAdd(r2, poly_corr, total_low);
396+
result = Add(res_hi, result);
339397
}
340398

341-
// Apply final sign correction based on quadrant
342-
VU sign_bits = ShiftRight<4>(n_int);
343-
sign_bits = ShiftLeft<63>(sign_bits);
344-
result = Xor(result, BitCast(d, sign_bits));
399+
// Apply final sign correction same for both sine and cosine
400+
// Both functions change sign every π radians, corresponding to bit 4 of n_int
401+
// This unified approach works because:
402+
// - sin(x + π) = -sin(x)
403+
// - cos(x + π) = -cos(x)
404+
VU x_sign_int = ShiftLeft<63>(BitCast(du, x));
405+
// XOR with quadrant info in n_biased
406+
VU combined = Xor(BitCast(du, n_biased), ShiftLeft<4>(x_sign_int));
407+
// Extract final sign
408+
VU sign = ShiftRight<4>(combined);
409+
sign = ShiftLeft<63>(sign);
410+
result = Xor(result, BitCast(d, sign)); // Apply sign flip
345411
return result;
346412
}
347413
// NOLINTNEXTLINE(google-readability-namespace-comments)
348-
} // namespace npsr::HWY_NAMESPACE::sincos
414+
} // namespace npsr::HWY_NAMESPACE::sincos
349415

350416
HWY_AFTER_NAMESPACE();
351417

352-
#endif // NPSR_TRIG_SMALL_INL_H_
418+
#endif // NPSR_TRIG_SMALL_INL_H_

0 commit comments

Comments
 (0)