[glibc] AArch64: Vectorise AdvSIMD fp32 sin/cos special case

Wilco Dijkstra via Glibc-cvs <[email protected]> Thu, 18 Jun 2026 16:05:29 +0000 (GMT)
Newsgroups gmane.comp.lib.glibc.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=faa9ea921abdb5affae09b430f1d28c86c7405a0

commit faa9ea921abdb5affae09b430f1d28c86c7405a0
Author: Dylan Fleming <[email protected]>
Date:   Thu Jun 18 14:54:57 2026 +0000

    AArch64: Vectorise AdvSIMD fp32 sin/cos special case
    
    Implemented single precision vector fallback for AdvSIMD sinf/cosf.
    
    Includes cleanup of the fast pass to remove old styling and conventions.
    
    Reviewed-by: Wilco Dijkstra  <[email protected]>

Diff:
---
 sysdeps/aarch64/fpu/cosf_advsimd.c     |  94 +++++++----
 sysdeps/aarch64/fpu/sinf_advsimd.c     |  91 +++++++----
 sysdeps/aarch64/fpu/v_trigf_fallback.h | 276 +++++++++++++++++++++++++++++++++
 3 files changed, 407 insertions(+), 54 deletions(-)

diff --git a/sysdeps/aarch64/fpu/cosf_advsimd.c b/sysdeps/aarch64/fpu/cosf_advsimd.c
index 8ba5677cc6..60fd58d318 100644
--- a/sysdeps/aarch64/fpu/cosf_advsimd.c
+++ b/sysdeps/aarch64/fpu/cosf_advsimd.c
@@ -18,59 +18,97 @@
    <https://www.gnu.org/licenses/>.  */
 
 #include "v_math.h"
+#include "v_trigf_fallback.h"
 
 static const struct data
 {
-  float32x4_t poly[4];
-  float32x4_t range_val, inv_pi, pi_1, pi_2, pi_3;
+  float inv_pi, pi_1, pi_2, pi_3;
+  float32x4_t c0, c1, c2, c3;
+  float32x4_t range_val;
 } data = {
-  /* 1.886 ulp error.  */
-  .poly = { V4 (-0x1.555548p-3f), V4 (0x1.110df4p-7f), V4 (-0x1.9f42eap-13f),
-	    V4 (0x1.5b2e76p-19f) },
+  .c0 = V4 (-0x1.55554ep-3f),
+  .c1 = V4 (0x1.110f22p-7f),
+  .c2 = V4 (-0x1.9f7cd6p-13f),
+  .c3 = V4 (0x1.5e6364p-19f),
 
-  .pi_1 = V4 (0x1.921fb6p+1f),
-  .pi_2 = V4 (-0x1.777a5cp-24f),
-  .pi_3 = V4 (-0x1.ee59dap-49f),
+  .inv_pi = 0x1.45f306p-2f,
+  .pi_1 = 0x1.921fb6p+1f,
+  .pi_2 = -0x1.777a5cp-24f,
+  .pi_3 = -0x1.ee59dap-49f,
 
-  .inv_pi = V4 (0x1.45f306p-2f),
-  .range_val = V4 (0x1p20f)
+  .range_val = V4 (0x1p20f),
 };
 
-#define C(i) d->poly[i]
+static inline float32x4_t VPCS_ATTR
+cos_fallback (float32x4_t x)
+{
+  struct reduction_result_t r = large_range_reduction (x);
+  float32x4x2_t lookup = sin_cos_lookup (r.octant);
+  float32x4x2_t eval_fast = sincos_eval (r.remainder);
+
+  /* Construct cos(x) from k and r, using angle addition formula, with
+     approximations of sin(r) and cos(r) - 1 to reduce rounding errors.
+     cos(x) = cos(k + r)
+	     = cos(k)*cos(r) - sin(k)*sin(r)
+	     = cos(k)*cosm1(r) - sin(k)*sin(r) + cos(k).  */
+
+  /* lookup = { sin(k), cos(k) }, eval_fast = { sin(r), cos(r) - 1 }.  */
+  float32x4_t sin_k = lookup.val[0];
+  float32x4_t cos_k = lookup.val[1];
+  float32x4_t sin_r = eval_fast.val[0];
+  float32x4_t cosm1_r = eval_fast.val[1];
+
+  float32x4_t cos_k_cosm1_r = vmulq_f32 (cos_k, cosm1_r);
+  float32x4_t result = vfmsq_f32 (cos_k_cosm1_r, sin_k, sin_r);
+  return vaddq_f32 (result, cos_k);
+}
 
 static float32x4_t VPCS_ATTR NOINLINE
 special_case (float32x4_t x, float32x4_t y, uint32x4_t odd, uint32x4_t cmp)
 {
-  /* Fall back to scalar code.  */
+  uint32x4_t is_inf = vcageq_f32 (x, v_f32 (INFINITY));
+  float32x4_t large = cos_fallback (x);
+  large = vbslq_f32 (is_inf, v_f32 (NAN), large);
   y = vreinterpretq_f32_u32 (veorq_u32 (vreinterpretq_u32_f32 (y), odd));
-  return v_call_f32 (cosf, x, y, cmp);
+  return vbslq_f32 (cmp, large, y);
 }
 
+/* Vector version of cosf.
+   The maximum observed error is 1.31 + 0.5 ULP if |x| < 0x1p20.
+   _ZGVnN4v_cosf (0x1.35fb0cp-2)
+    got 0x1.e8b83cp-1
+   want 0x1.e8b838p-1.
+   The special domain has a slightly higher maximum error than the fast path:
+   Maximum observed error is 1.43 + 0.5ULP
+   _ZGVnN4v_cosf (0x1.a2785ap+108)
+    got -0x1.dc17fp-2
+   want -0x1.dc17f4p-2.  */
 float32x4_t VPCS_ATTR NOINLINE V_NAME_F1 (cos) (float32x4_t x)
 {
   const struct data *d = ptr_barrier (&data);
-  float32x4_t n, r, r2, r3, y;
-  uint32x4_t odd, cmp;
-
-  cmp = vcageq_f32 (x, d->range_val);
+  float32x4_t pi_vals = vld1q_f32 (&d->inv_pi);
+  uint32x4_t cmp = vcageq_f32 (x, d->range_val);
 
   /* n = rint((|x|+pi/2)/pi) - 0.5.  */
-  n = vrndaq_f32 (vfmaq_f32 (v_f32 (0.5), x, d->inv_pi));
-  odd = vshlq_n_u32 (vreinterpretq_u32_s32 (vcvtq_s32_f32 (n)), 31);
+  float32x4_t n = vrndaq_f32 (vfmaq_laneq_f32 (v_f32 (0.5f), x, pi_vals, 0));
+  uint32x4_t odd = vshlq_n_u32 (vreinterpretq_u32_s32 (vcvtq_s32_f32 (n)), 31);
   n = vsubq_f32 (n, v_f32 (0.5f));
 
   /* r = |x| - n*pi  (range reduction into -pi/2 .. pi/2).  */
-  r = vfmsq_f32 (x, d->pi_1, n);
-  r = vfmsq_f32 (r, d->pi_2, n);
-  r = vfmsq_f32 (r, d->pi_3, n);
+  float32x4_t r = x;
+  r = vfmsq_laneq_f32 (r, n, pi_vals, 1);
+  r = vfmsq_laneq_f32 (r, n, pi_vals, 2);
+  r = vfmsq_laneq_f32 (r, n, pi_vals, 3);
 
   /* y = sin(r).  */
-  r2 = vmulq_f32 (r, r);
-  r3 = vmulq_f32 (r2, r);
-  y = vfmaq_f32 (C (2), C (3), r2);
-  y = vfmaq_f32 (C (1), y, r2);
-  y = vfmaq_f32 (C (0), y, r2);
-  y = vfmaq_f32 (r, y, r3);
+  float32x4_t r2 = vmulq_f32 (r, r);
+  float32x4_t r3 = vmulq_f32 (r2, r);
+
+  float32x4_t y;
+  y = vfmaq_f32 (d->c2, r2, d->c3);
+  y = vfmaq_f32 (d->c1, r2, y);
+  y = vfmaq_f32 (d->c0, r2, y);
+  y = vfmaq_f32 (r, r3, y);
 
   if (__glibc_unlikely (v_any_u32 (cmp)))
     return special_case (x, y, odd, cmp);
diff --git a/sysdeps/aarch64/fpu/sinf_advsimd.c b/sysdeps/aarch64/fpu/sinf_advsimd.c
index d80fcff0ee..5243a91991 100644
--- a/sysdeps/aarch64/fpu/sinf_advsimd.c
+++ b/sysdeps/aarch64/fpu/sinf_advsimd.c
@@ -18,56 +18,95 @@
    <https://www.gnu.org/licenses/>.  */
 
 #include "v_math.h"
+#include "v_trigf_fallback.h"
 
 static const struct data
 {
-  float32x4_t poly[4];
-  float32x4_t range_val, inv_pi, pi_1, pi_2, pi_3;
+  float inv_pi, pi_1, pi_2, pi_3;
+  float32x4_t c0, c1, c2, c3;
+  float32x4_t range_val;
 } data = {
-  /* 1.886 ulp error.  */
-  .poly = { V4 (-0x1.555548p-3f), V4 (0x1.110df4p-7f), V4 (-0x1.9f42eap-13f),
-	    V4 (0x1.5b2e76p-19f) },
+  .c0 = V4 (-0x1.55554ep-3f),
+  .c1 = V4 (0x1.110f22p-7f),
+  .c2 = V4 (-0x1.9f7cd6p-13f),
+  .c3 = V4 (0x1.5e6364p-19f),
 
-  .pi_1 = V4 (0x1.921fb6p+1f),
-  .pi_2 = V4 (-0x1.777a5cp-24f),
-  .pi_3 = V4 (-0x1.ee59dap-49f),
+  .inv_pi = 0x1.45f306p-2f,
+  .pi_1 = 0x1.921fb6p+1f,
+  .pi_2 = -0x1.777a5cp-24f,
+  .pi_3 = -0x1.ee59dap-49f,
 
-  .inv_pi = V4 (0x1.45f306p-2f),
-  .range_val = V4 (0x1p20f)
+  .range_val = V4 (0x1p20f),
 };
 
-#define C(i) d->poly[i]
+static inline VPCS_ATTR float32x4_t
+sin_fallback (float32x4_t x)
+{
+  struct reduction_result_t r = large_range_reduction (x);
+  float32x4x2_t lookup = sin_cos_lookup (r.octant);
+  float32x4x2_t eval_fast = sincos_eval (r.remainder);
+
+  /* Construct sin(x) from k and r, using angle addition formula, with
+     approximations of sin(r) and cos(r) - 1 to reduce rounding errors.
+     sin(x) = sin(k + r)
+	    = cos(k)*sin(r) + sin(k)*cos(r)
+	    = cos(k)*sin(r) + sin(k)*cosm1(r) + sin(k).  */
+
+  float32x4_t sin_k = lookup.val[0];
+  float32x4_t cos_k = lookup.val[1];
+  float32x4_t sin_r = eval_fast.val[0];
+  float32x4_t cosm1_r = eval_fast.val[1];
+
+  float32x4_t sin_k_cosm1_r = vmulq_f32 (sin_k, cosm1_r);
+  float32x4_t result = vfmaq_f32 (sin_k_cosm1_r, cos_k, sin_r);
+  return vaddq_f32 (result, sin_k);
+}
 
 static float32x4_t VPCS_ATTR NOINLINE
 special_case (float32x4_t x, float32x4_t y, uint32x4_t odd, uint32x4_t cmp)
 {
-  /* Fall back to scalar code.  */
+  uint32x4_t is_inf = vcageq_f32 (x, v_f32 (INFINITY));
+  float32x4_t large = sin_fallback (x);
+  large = vbslq_f32 (is_inf, v_f32 (NAN), large);
   y = vreinterpretq_f32_u32 (veorq_u32 (vreinterpretq_u32_f32 (y), odd));
-  return v_call_f32 (sinf, x, y, cmp);
+  return vbslq_f32 (cmp, large, y);
 }
 
+/* Vector version of sinf.
+   Maximum observed error is 1.25 + 0.5 ULP if |x| < 0x1p20.
+   _ZGVnN4v_sinf (0x1.f0ea62p-2)
+    got 0x1.fdf15p-1
+   want 0x1.fdf154p-1.
+   The special domain has a slightly higher maximum error than the fast path:
+   Maximum observed error is 1.42 + 0.5ULP
+   _ZGVnN4v_sinf (0x1.dbe63cp+36)
+    got -0x1.d76b4p-2
+   want -0x1.d76b44p-2.  */
 float32x4_t VPCS_ATTR NOINLINE V_NAME_F1 (sin) (float32x4_t x)
 {
   const struct data *d = ptr_barrier (&data);
-  float32x4_t n, r, r2, y;
-  uint32x4_t odd, cmp;
-  cmp = vcageq_f32 (x, d->range_val);
+  uint32x4_t cmp = vcageq_f32 (x, d->range_val);
 
+  float32x4_t pi_vals = vld1q_f32 (&d->inv_pi);
   /* n = rint(|x|/pi).  */
-  n = vrndaq_f32 (vmulq_f32 (x, d->inv_pi));
-  odd = vshlq_n_u32 (vreinterpretq_u32_s32 (vcvtq_s32_f32 (n)), 31);
+  float32x4_t n = vrndaq_f32 (vmulq_laneq_f32 (x, pi_vals, 0));
+  uint32x4_t odd = vshlq_n_u32 (vreinterpretq_u32_s32 (vcvtq_s32_f32 (n)), 31);
 
   /* r = |x| - n*pi  (range reduction into -pi/2 .. pi/2).  */
-  r = vfmsq_f32 (x, d->pi_1, n);
-  r = vfmsq_f32 (r, d->pi_2, n);
-  r = vfmsq_f32 (r, d->pi_3, n);
+  float32x4_t r = x;
+  r = vfmsq_laneq_f32 (r, n, pi_vals, 1);
+  r = vfmsq_laneq_f32 (r, n, pi_vals, 2);
+  r = vfmsq_laneq_f32 (r, n, pi_vals, 3);
 
   /* y = sin(r).  */
-  r2 = vmulq_f32 (r, r);
-  y = vfmaq_f32 (C (2), C (3), r2);
-  y = vfmaq_f32 (C (1), y, r2);
-  y = vfmaq_f32 (C (0), y, r2);
-  y = vfmaq_f32 (r, vmulq_f32 (y, r2), r);
+  float32x4_t r2 = vmulq_f32 (r, r);
+  float32x4_t r3 = vmulq_f32 (r2, r);
+
+  float32x4_t y;
+  y = vfmaq_f32 (d->c2, r2, d->c3);
+  y = vfmaq_f32 (d->c1, r2, y);
+  y = vfmaq_f32 (d->c0, r2, y);
+  y = vfmaq_f32 (r, r3, y);
 
   if (__glibc_unlikely (v_any_u32 (cmp)))
     return special_case (x, y, odd, cmp);
diff --git a/sysdeps/aarch64/fpu/v_trigf_fallback.h b/sysdeps/aarch64/fpu/v_trigf_fallback.h
new file mode 100644
index 0000000000..875cfdbe3f
--- /dev/null
+++ b/sysdeps/aarch64/fpu/v_trigf_fallback.h
@@ -0,0 +1,276 @@
+/* Vectorised fallback for single-precision AdvSIMD trig functions.
+
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+/* Row i uses q = i - 3 and stores a 4-term binary32 expansion of
+   frac((4/pi) * 2^(8*q)), biased into [0.5, 1.5). With x_reduced exponent in
+   [32, 39], ph.hi then lands on a multiple of 2^8 and contributes no bits to
+   k mod 8.  */
+static const float FOUR_OVER_PI[16][4] = {
+  { 0x1.000002p0f, -0x1.7419f2p-25f, -0x1.1b1bbep-51f, -0x1.5ac07cp-76f },
+  { 0x1.000146p0f, -0x1.9f246cp-29f, -0x1.bbead6p-55f, -0x1.ec5418p-86f },
+  { 0x1.0145f4p0f, -0x1.f246c6p-25f, -0x1.df56bp-50f, -0x1.ec5418p-78f },
+  { 0x1.45f306p0f, 0x1.b9391p-25f, 0x1.529fc2p-51f, 0x1.d5f47ep-77f },
+  { 0x1.e60dbap-1f, -0x1.8ddf56p-26f, -0x1.603d8ap-51f, -0x1.05c15ap-76f },
+  { 0x1.06dc9cp0f, 0x1.1054a8p-25f, -0x1.ec5418p-54f, 0x1.f534dep-79f },
+  { 0x1.b9391p-1f, 0x1.529fc2p-27f, 0x1.d5f47ep-53f, -0x1.65912p-78f },
+  { 0x1.391054p-1f, 0x1.4fe13ap-26f, 0x1.7d1f54p-51f, -0x1.6447e4p-76f },
+  { 0x1.1054a8p-1f, -0x1.ec5418p-30f, 0x1.f534dep-55f, -0x1.f924ecp-82f },
+  { 0x1.2a53f8p0f, 0x1.3abe9p-26f, -0x1.596448p-52f, 0x1.b6c52cp-80f },
+  { 0x1.53f84ep0f, 0x1.5f47d4p-25f, 0x1.a6ee06p-50f, 0x1.b6295ap-75f },
+  { 0x1.f09d6p-1f, -0x1.70565ap-26f, 0x1.dc0db6p-51f, 0x1.4acc9ep-78f },
+  { 0x1.4eafa4p0f, -0x1.596448p-28f, 0x1.b6c52cp-56f, -0x1.9b0ef2p-81f },
+  { 0x1.5f47d4p-1f, 0x1.a6ee06p-26f, 0x1.b6295ap-51f, -0x1.b0ef1cp-77f },
+  { 0x1.47d4d4p-1f, -0x1.11f924p-26f, -0x1.d6a66cp-51f, -0x1.de37ep-78f },
+  { 0x1.d4d378p-1f, -0x1.f924ecp-26f, 0x1.5993c4p-51f, 0x1.c821p-78f },
+};
+
+/* Table of { sin (K * pi/4), cos (K * pi/4) }.
+   For K < 8.  */
+static const float SINCOS_K_PI_OVER_4[8][2] = {
+  { 0, 0x1p0f },  { 0x1.6a09e6p-1f, 0x1.6a09e6p-1f },
+  { 0x1p0f, 0 },  { 0x1.6a09e6p-1f, -0x1.6a09e6p-1f },
+  { 0, -0x1p0f }, { -0x1.6a09e6p-1f, -0x1.6a09e6p-1f },
+  { -0x1p0f, 0 }, { -0x1.6a09e6p-1f, 0x1.6a09e6p-1f },
+};
+
+struct reduction_result_t
+{
+  uint32x4_t octant;
+  float32x4x2_t remainder;
+};
+
+/* Error-free multiplication using double-float computation
+   using the TwoProd algorithm.
+   hi is the rounded product, lo is the exact FMA residual.  */
+static inline float32x4x2_t VPCS_ATTR
+two_prod (float32x4_t a, float32x4_t b)
+{
+  float32x4x2_t result;
+  result.val[0] = vmulq_f32 (a, b);
+  result.val[1] = vfmaq_f32 (vnegq_f32 (result.val[0]), a, b);
+  return result;
+}
+
+/* Error-free sum using double-float computation.
+   using the FastTwoSum algorithm, which requires |a| >= |b|.
+   hi is the rounded sum and lo recovers the low-order
+   bits lost by that rounding.  */
+static inline float32x4x2_t VPCS_ATTR
+fast_two_sum (float32x4_t a, float32x4_t b)
+{
+  float32x4x2_t result;
+  result.val[0] = vaddq_f32 (a, b);
+  float32x4_t t = vsubq_f32 (result.val[0], a);
+  result.val[1] = vsubq_f32 (b, t);
+  return result;
+}
+
+/* Loads four rows of FOUR_OVER_PI, and then transposes it to align correctly
+   for vector multiplies.  */
+static inline float32x4x4_t VPCS_ATTR
+load_datablock (uint32x4_t idx)
+{
+  float32x4x4_t data;
+  uint32_t idx0 = vgetq_lane_u32 (idx, 0) & 15;
+  uint32_t idx1 = vgetq_lane_u32 (idx, 1) & 15;
+  uint32_t idx2 = vgetq_lane_u32 (idx, 2) & 15;
+  uint32_t idx3 = vgetq_lane_u32 (idx, 3) & 15;
+
+  float32x4_t temp0 = vld1q_f32 (&FOUR_OVER_PI[idx0][0]);
+  float32x4_t temp1 = vld1q_f32 (&FOUR_OVER_PI[idx1][0]);
+  float32x4_t temp2 = vld1q_f32 (&FOUR_OVER_PI[idx2][0]);
+  float32x4_t temp3 = vld1q_f32 (&FOUR_OVER_PI[idx3][0]);
+
+  /* Transpose the table loaded data.  */
+  float32x4_t row01_lo = vtrn1q_f32 (temp0, temp1);
+  float32x4_t row01_hi = vtrn2q_f32 (temp0, temp1);
+  float32x4_t row23_lo = vtrn1q_f32 (temp2, temp3);
+  float32x4_t row23_hi = vtrn2q_f32 (temp2, temp3);
+
+  data.val[0] = vreinterpretq_f32_f64 (vzip1q_f64 (
+      vreinterpretq_f64_f32 (row01_lo), vreinterpretq_f64_f32 (row23_lo)));
+  data.val[1] = vreinterpretq_f32_f64 (vzip1q_f64 (
+      vreinterpretq_f64_f32 (row01_hi), vreinterpretq_f64_f32 (row23_hi)));
+  data.val[2] = vreinterpretq_f32_f64 (vzip2q_f64 (
+      vreinterpretq_f64_f32 (row01_lo), vreinterpretq_f64_f32 (row23_lo)));
+  data.val[3] = vreinterpretq_f32_f64 (vzip2q_f64 (
+      vreinterpretq_f64_f32 (row01_hi), vreinterpretq_f64_f32 (row23_hi)));
+
+  return data;
+}
+
+/* Reduce x for |x| > 0x1p8 inputs, such that:
+    x = (k + y) * (pi / 4)
+
+   Returns a struct containing:
+    The integer k,
+    A double-single remainder y.
+
+   Note that y is returned in terms of 4/pi, and so has to be converted
+   back to radians before can be used for polynomial evaluation; this is done
+   inside of sincos_eval, as it allows for more efficient constant loading.  */
+static inline struct reduction_result_t VPCS_ATTR
+large_range_reduction (float32x4_t x)
+{
+  /* First, x is reduced into the range of [2^32, 2^40), by directly adjusting
+     the exponent. This ensures the leading product contribute only multiples
+     of 2^8, so the useful bits of k mod 8 are entirely contained within the
+     lower product terms.  */
+  uint32x4_t ix = vreinterpretq_u32_f32 (x);
+  int32x4_t x_e_m32
+      = vreinterpretq_s32_u32 (vsraq_n_u32 (v_u32 ((-(127 + 32))), ix, 23));
+
+  /* We can then use the new exponent as an index
+     for the FOUR_OVER_PI table.  */
+  uint32x4_t idx = vreinterpretq_u32_s32 (vsraq_n_s32 (v_s32 (3), x_e_m32, 3));
+  float32x4x4_t data = load_datablock (idx);
+
+  /* x_e_m32 has already been split into:
+       x_e_m32 = 8 * ROW + offset
+     where ROW selected the FOUR_OVER_PI row above.
+
+     We want to keep the offset (x_e_m32 mod 8), and use it to produce a new
+     exponent (32 + offset) so that x_reduced is within our intended [32, 39]
+     window.  */
+  int32x4_t masked = vandq_s32 (x_e_m32, v_s32 (7));
+  int32x4_t added = vaddq_s32 (masked, v_s32 ((127 + 32)));
+  int32x4_t shifted = vshlq_n_s32 (added, 23);
+  uint32x4_t new_exponent = vreinterpretq_u32_s32 (shifted);
+
+  /* Finally, we get our reduced x value, by reinserting the new exponent into
+     the original input mantissa.  */
+  uint32x4_t x_mantissa = vandq_u32 (ix, v_u32 (0x7fffff));
+  uint32x4_t new_ix = vorrq_u32 (new_exponent, x_mantissa);
+  float32x4_t unsigned_x_reduced = vreinterpretq_f32_u32 (new_ix);
+  float32x4_t x_reduced
+      = vbslq_f32 (v_u32 (0x80000000), x, unsigned_x_reduced);
+
+  /* We now use the reduced x to calculate x ~= (k + r) * (pi / 4).
+     First, we multiply x_reduced by the first three chunks of the FOUR_OVER_PI
+     table, using double-single arithmetic to maintain a high precision
+     intermediate.  */
+  float32x4x2_t ph = two_prod (x_reduced, data.val[0]);
+  float32x4x2_t pm = two_prod (x_reduced, data.val[1]);
+  float32x4x2_t pl = two_prod (x_reduced, data.val[2]);
+
+  /* Next, we need to accumulate the results together to get an integer k for
+     our octant. However, ph.val[0] will always be a multiple of 2^8, so it
+     cannot affect k mod 8. pm.val[1] and pl will always be sufficiently small
+     that they cannot affect the integer portion of the result, Therefore we
+     only need to sum ph.val[1] and pm.val[0] when computing k mod 8.  */
+  float32x4_t sum_hi = vaddq_f32 (ph.val[1], pm.val[0]);
+  float32x4_t kd = vrndaq_f32 (sum_hi);
+
+  /* To compute the remainder, we need to remove the rounded integer k and
+     accumulate the remaining terms as a double-single remainder.  */
+  float32x4_t y_hi = vaddq_f32 (vsubq_f32 (ph.val[1], kd), pm.val[0]);
+  float32x4x2_t y_mid = fast_two_sum (pm.val[1], pl.val[0]);
+  float32x4_t y_lo = pl.val[1];
+
+  /* The low portion of x_reduced * D3 has no meaningful contribution to the
+     result, so a simple FMA is sufficient.  */
+  float32x4_t y_l = vfmaq_f32 (y_lo, x_reduced, data.val[3]);
+
+  /* We then accumulate the final remainder.  */
+  float32x4x2_t y = fast_two_sum (y_hi, y_mid.val[0]);
+  y.val[1] = vaddq_f32 (y.val[1], vaddq_f32 (y_mid.val[1], y_l));
+
+  struct reduction_result_t result;
+  result.remainder = y;
+  result.octant = vreinterpretq_u32_s32 (vcvtnq_s32_f32 (kd));
+  return result;
+}
+
+/* Lookup sin(k * pi/4) and cos(k * pi/4).  */
+static inline float32x4x2_t VPCS_ATTR
+sin_cos_lookup (uint32x4_t k)
+{
+  float32x4x2_t result;
+
+  unsigned idx0_s = vgetq_lane_u32 (k, 0) & 7;
+  unsigned idx1_s = vgetq_lane_u32 (k, 1) & 7;
+  unsigned idx2_s = vgetq_lane_u32 (k, 2) & 7;
+  unsigned idx3_s = vgetq_lane_u32 (k, 3) & 7;
+
+  float32x2_t row0 = vld1_f32 (&SINCOS_K_PI_OVER_4[idx0_s][0]);
+  float32x2_t row1 = vld1_f32 (&SINCOS_K_PI_OVER_4[idx1_s][0]);
+  float32x2_t row2 = vld1_f32 (&SINCOS_K_PI_OVER_4[idx2_s][0]);
+  float32x2_t row3 = vld1_f32 (&SINCOS_K_PI_OVER_4[idx3_s][0]);
+
+  /* Transpose four { sin, cos } rows into separate sin/cos vectors.  */
+  float32x2_t row01_sin = vtrn1_f32 (row0, row1);
+  float32x2_t row01_cos = vtrn2_f32 (row0, row1);
+  float32x2_t row23_sin = vtrn1_f32 (row2, row3);
+  float32x2_t row23_cos = vtrn2_f32 (row2, row3);
+
+  result.val[0] = vcombine_f32 (row01_sin, row23_sin);
+  result.val[1] = vcombine_f32 (row01_cos, row23_cos);
+  return result;
+}
+
+static const struct eval_data
+{
+  float pi_over_4_hi, pi_over_4_lo, s2, c3;
+  float32x4_t s0, s1;
+  float32x4_t c0, c1, c2;
+} eval_data = { .s0 = V4 (-0x1.555546p-3f),
+		.s1 = V4 (0x1.11076p-7f),
+		.s2 = -0x1.994eb4p-13f,
+		.c0 = V4 (-0x1p-1f),
+		.c1 = V4 (0x1.55554ap-5f),
+		.c2 = V4 (-0x1.6c0c1ap-10f),
+		.c3 = 0x1.99e0eep-16f,
+		.pi_over_4_hi = 0x1.921fb6p-1f,
+		.pi_over_4_lo = -0x1.777a5cp-26f };
+
+/* Evaluates sin(r) and cos(r) - 1 for the reduced argument.
+
+   Note that the input y is still terms of 4/pi, and so has to be
+   multiplied by pi/4 before evaluation.  */
+static inline float32x4x2_t VPCS_ATTR
+sincos_eval (float32x4x2_t y)
+{
+  const struct eval_data *d = ptr_barrier (&eval_data);
+  float32x4_t coeffs = vld1q_f32 (&d->pi_over_4_hi);
+
+  /* Reconstruct r = y * (pi/4).  */
+  float32x4_t corr = vfmaq_laneq_f32 (vmulq_laneq_f32 (y.val[1], coeffs, 0),
+				      y.val[0], coeffs, 1);
+
+  float32x4_t r = vfmaq_laneq_f32 (corr, y.val[0], coeffs, 0);
+  float32x4_t r2 = vmulq_f32 (r, r);
+  float32x4_t r3 = vmulq_f32 (r2, r);
+
+  /* sin(r) = r + r^3 * P(r^2).  */
+  float32x4_t sin = vfmaq_laneq_f32 (d->s1, r2, coeffs, 2);
+  sin = vfmaq_f32 (d->s0, r2, sin);
+  sin = vfmaq_f32 (r, r3, sin);
+
+  /* Return cos(r) - 1 rather than cos(r) to avoid cancellation during the
+     later angle-add reconstruction around exact k * pi/4 values.  */
+  float32x4_t cosm1 = vfmaq_laneq_f32 (d->c2, r2, coeffs, 3);
+  cosm1 = vfmaq_f32 (d->c1, r2, cosm1);
+  cosm1 = vfmaq_f32 (d->c0, r2, cosm1);
+  cosm1 = vmulq_f32 (cosm1, r2);
+
+  float32x4x2_t result;
+  result.val[0] = sin;
+  result.val[1] = cosm1;
+  return result;
+}