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

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

commit e585d88982e79fbc117c1b1e7bec541c70981182
Author: Dylan Fleming <[email protected]>
Date:   Thu Jun 18 14:55:15 2026 +0000

    AArch64: Vectorise SVE fp32 sin/cos special case
    
    Implemented single precision vector fallback for SVE sinf/cosf.
    
    Includes a rewrite of SVE sinf to use the trig instructions.
    
    Reviewed-by: Wilco Dijkstra  <[email protected]>

Diff:
---
 sysdeps/aarch64/fpu/cosf_sve.c          |  75 ++++++++-----
 sysdeps/aarch64/fpu/sinf_sve.c          | 137 +++++++++++++-----------
 sysdeps/aarch64/fpu/sv_trigf_fallback.h | 184 ++++++++++++++++++++++++++++++++
 3 files changed, 309 insertions(+), 87 deletions(-)

diff --git a/sysdeps/aarch64/fpu/cosf_sve.c b/sysdeps/aarch64/fpu/cosf_sve.c
index 680d89a8fa..9aedcb14fa 100644
--- a/sysdeps/aarch64/fpu/cosf_sve.c
+++ b/sysdeps/aarch64/fpu/cosf_sve.c
@@ -18,10 +18,11 @@
    <https://www.gnu.org/licenses/>.  */
 
 #include "sv_math.h"
+#include "sv_trigf_fallback.h"
 
 static const struct data
 {
-  float neg_pio2_1, neg_pio2_2, neg_pio2_3, inv_pio2, shift;
+  float neg_pio2_1, neg_pio2_2, neg_pio2_3, inv_pio2, shift, range_val;
 } data = {
   /* Polynomial coefficients are hard-wired in FTMAD instructions.  */
   .neg_pio2_1 = -0x1.921fb6p+0f,
@@ -31,46 +32,71 @@ static const struct data
   /* Original shift used in AdvSIMD cosf,
      plus a contribution to set the bit #0 of q
      as expected by trigonometric instructions.  */
-  .shift = 0x1.800002p+23f
+  .shift = 0x1.800002p+23f,
+  .range_val = 0x1p20f,
 };
 
-#define RangeVal 0x49800000 /* asuint32(0x1p20f).  */
-
 static svfloat32_t NOINLINE
-special_case (svfloat32_t x, svfloat32_t y, svbool_t oob)
+special_case (svfloat32_t x, svfloat32_t y, svbool_t special)
 {
-  return sv_call_f32 (cosf, x, y, oob);
+  special = svaclt (special, x, sv_f32 (INFINITY));
+
+  svfloat32x2_t reduction = large_range_reduction (svptrue_b32 (), x);
+
+  /* Unpack the quadrant from the return struct.  */
+  svuint32_t quadrant = svreinterpret_u32 (svget2 (reduction, 1));
+  svfloat32_t r = svget2 (reduction, 0);
+
+  /* Adjust quadrant to select cosine polynomial.  */
+  quadrant = svadd_x (svptrue_b32 (), quadrant, 1);
+
+  svfloat32_t f = svtssel (r, quadrant);
+  svfloat32_t r2 = svtsmul (r, quadrant);
+  svfloat32_t cos = sv_f32 (0.0f);
+  cos = svtmad (cos, r2, 4);
+  cos = svtmad (cos, r2, 3);
+  cos = svtmad (cos, r2, 2);
+  cos = svtmad (cos, r2, 1);
+  cos = svtmad (cos, r2, 0);
+  cos = svmul_x (svptrue_b32 (), f, cos);
+
+  return svsel (special, cos, y);
 }
 
-/* A fast SVE implementation of cosf based on trigonometric
-   instructions (FTMAD, FTSSEL, FTSMUL).
-   Maximum measured error: 2.06 ULPs.
-   SV_NAME_F1 (cos)(0x1.dea2f2p+19) got 0x1.fffe7ap-6
-				   want 0x1.fffe76p-6.  */
+/* Vector version of cosf.
+   The maximum observed error is 1.56 + 0.5 ULP if |x| < 0x1p20.
+   _ZGVsMxv_cosf (0x1.dea2f2p+19)
+    got 0x1.fffe7ap-6
+   want 0x1.fffe76p-6
+   The special domain has a higher maximum error than the fast path:
+   Maximum observed error is 2.65 + 0.5ULP
+   _ZGVsMxv_cosf (0x1.ff3afcp+53)
+    got -0x1.ffe74p-3
+   want -0x1.ffe73ap-3.  */
 svfloat32_t SV_NAME_F1 (cos) (svfloat32_t x, const svbool_t pg)
 {
   const struct data *d = ptr_barrier (&data);
-
-  svfloat32_t r = svabs_x (pg, x);
-  svbool_t oob = svcmpge (pg, svreinterpret_u32 (r), RangeVal);
+  svbool_t ptrue = svptrue_b32 ();
 
   /* Load some constants in quad-word chunks to minimise memory access.  */
-  svfloat32_t negpio2_and_invpio2 = svld1rq (svptrue_b32 (), &d->neg_pio2_1);
+  svfloat32_t negpio2_and_invpio2 = svld1rq (ptrue, &d->neg_pio2_1);
 
-  /* n = rint(|x|/(pi/2)).  */
-  svfloat32_t q = svmla_lane (sv_f32 (d->shift), r, negpio2_and_invpio2, 3);
-  svfloat32_t n = svsub_x (pg, q, d->shift);
+  /* n = rint(x/(pi/2)).  */
+  svfloat32_t q = svmla_lane (sv_f32 (d->shift), x, negpio2_and_invpio2, 3);
+  svfloat32_t n = svsub_x (ptrue, q, d->shift);
 
-  /* r = |x| - n*(pi/2)  (range reduction into -pi/4 .. pi/4).  */
+  /* r = x - n*(pi/2)  (range reduction into -pi/4 .. pi/4).  */
+  svfloat32_t r = x;
   r = svmla_lane (r, n, negpio2_and_invpio2, 0);
   r = svmla_lane (r, n, negpio2_and_invpio2, 1);
   r = svmla_lane (r, n, negpio2_and_invpio2, 2);
 
   /* Final multiplicative factor: 1.0 or x depending on bit #0 of q.  */
-  svfloat32_t f = svtssel (r, svreinterpret_u32 (q));
+  svuint32_t q_u = svreinterpret_u32 (q);
+  svfloat32_t f = svtssel (r, q_u);
 
   /* cos(r) poly approx.  */
-  svfloat32_t r2 = svtsmul (r, svreinterpret_u32 (q));
+  svfloat32_t r2 = svtsmul (r, q_u);
   svfloat32_t y = sv_f32 (0.0f);
   y = svtmad (y, r2, 4);
   y = svtmad (y, r2, 3);
@@ -78,8 +104,9 @@ svfloat32_t SV_NAME_F1 (cos) (svfloat32_t x, const svbool_t pg)
   y = svtmad (y, r2, 1);
   y = svtmad (y, r2, 0);
 
-  if (__glibc_unlikely (svptest_any (pg, oob)))
-    return special_case (x, svmul_x (svnot_z (pg, oob), f, y), oob);
+  svbool_t cmp = svacge (pg, x, sv_f32 (d->range_val));
+  if (__glibc_unlikely (svptest_any (pg, cmp)))
+    return special_case (x, svmul_x (ptrue, f, y), cmp);
   /* Apply factor.  */
-  return svmul_x (pg, f, y);
+  return svmul_x (ptrue, f, y);
 }
diff --git a/sysdeps/aarch64/fpu/sinf_sve.c b/sysdeps/aarch64/fpu/sinf_sve.c
index 5f5aa60924..d4668bfafd 100644
--- a/sysdeps/aarch64/fpu/sinf_sve.c
+++ b/sysdeps/aarch64/fpu/sinf_sve.c
@@ -18,81 +18,92 @@
    <https://www.gnu.org/licenses/>.  */
 
 #include "sv_math.h"
+#include "sv_trigf_fallback.h"
 
 static const struct data
 {
-  float poly[4];
-  /* Pi-related values to be loaded as one quad-word and used with
-     svmla_lane.  */
-  float negpi1, negpi2, negpi3, invpi;
-  float shift;
+  float neg_pio2_1, neg_pio2_2, neg_pio2_3, inv_pio2, shift, range_val;
 } data = {
-  .poly = {
-    /* Non-zero coefficients from the degree 9 Taylor series expansion of
-       sin.  */
-    -0x1.555548p-3f, 0x1.110df4p-7f, -0x1.9f42eap-13f, 0x1.5b2e76p-19f
-  },
-  .negpi1 = -0x1.921fb6p+1f,
-  .negpi2 = 0x1.777a5cp-24f,
-  .negpi3 = 0x1.ee59dap-49f,
-  .invpi = 0x1.45f306p-2f,
-  .shift = 0x1.8p+23f
+  /* Polynomial coefficients are hard-wired in FTMAD instructions.  */
+  .neg_pio2_1 = -0x1.921fb6p+0f,
+  .neg_pio2_2 = 0x1.777a5cp-25f,
+  .neg_pio2_3 = 0x1.ee59dap-50f,
+  .inv_pio2 = 0x1.45f306p-1f,
+  /* Original shift used in AdvSIMD cosf,
+     plus a contribution to set the bit #0 of q
+     as expected by trigonometric instructions.  */
+  .shift = 0x1.8p+23f,
+  .range_val = 0x1p20f,
 };
 
-#define RangeVal 0x49800000 /* asuint32 (0x1p20f).  */
-#define C(i) sv_f32 (d->poly[i])
-
 static svfloat32_t NOINLINE
-special_case (svfloat32_t x, svfloat32_t y, svbool_t cmp)
+special_case (svfloat32_t x, svfloat32_t y, svbool_t special)
 {
-  return sv_call_f32 (sinf, x, y, cmp);
+  special = svaclt (special, x, sv_f32 (INFINITY));
+
+  svfloat32x2_t reduction = large_range_reduction (svptrue_b32 (), x);
+
+  /* Unpack the quadrant from the return struct.  */
+  svuint32_t quadrant = svreinterpret_u32 (svget2 (reduction, 1));
+  svfloat32_t r = svget2 (reduction, 0);
+
+  svfloat32_t f = svtssel (r, quadrant);
+  svfloat32_t r2 = svtsmul (r, quadrant);
+  svfloat32_t sin = sv_f32 (0.0f);
+  sin = svtmad (sin, r2, 4);
+  sin = svtmad (sin, r2, 3);
+  sin = svtmad (sin, r2, 2);
+  sin = svtmad (sin, r2, 1);
+  sin = svtmad (sin, r2, 0);
+  sin = svmul_x (svptrue_b32 (), f, sin);
+
+  return svsel (special, sin, y);
 }
 
-/* A fast SVE implementation of sinf.
-   Maximum error: 1.89 ULPs.
-   This maximum error is achieved at multiple values in [-2^18, 2^18]
-   but one example is:
-   SV_NAME_F1 (sin)(0x1.9247a4p+0) got 0x1.fffff6p-1 want 0x1.fffffap-1.  */
+/* Vector version of sinf.
+   The maximum observed error is 1.44 + 0.5 ULP when |x| < 0x1p20.
+   _ZGVsMxv_sinf(0x1.4b0d9cp+13)
+    got 0x1.fc28cep-3
+   want 0x1.fc28d2p-3.
+   The special domain has a higher maximum error than the fast path:
+   The maximum observed error is 2.69 + 0.5 ULP when |x| >= 0x1p20.
+   _ZGVsMxv_sinf (0x1.be07aap+77)
+    got 0x1.ffe05ep-5
+   want 0x1.ffe058p-5.  */
 svfloat32_t SV_NAME_F1 (sin) (svfloat32_t x, const svbool_t pg)
 {
   const struct data *d = ptr_barrier (&data);
-
-  svfloat32_t ax = svabs_x (pg, x);
-  svuint32_t sign
-      = sveor_x (pg, svreinterpret_u32 (x), svreinterpret_u32 (ax));
-  svbool_t cmp = svcmpge (pg, svreinterpret_u32 (ax), RangeVal);
-
-  /* pi_vals are a quad-word of helper values - the first 3 elements contain
-     -pi in extended precision, the last contains 1 / pi.  */
-  svfloat32_t pi_vals = svld1rq (svptrue_b32 (), &d->negpi1);
-
-  /* n = rint(|x|/pi).  */
-  svfloat32_t n = svmla_lane (sv_f32 (d->shift), ax, pi_vals, 3);
-  svuint32_t odd = svlsl_x (pg, svreinterpret_u32 (n), 31);
-  n = svsub_x (pg, n, d->shift);
-
-  /* r = |x| - n*pi  (range reduction into -pi/2 .. pi/2).  */
-  svfloat32_t r;
-  r = svmla_lane (ax, n, pi_vals, 0);
-  r = svmla_lane (r, n, pi_vals, 1);
-  r = svmla_lane (r, n, pi_vals, 2);
-
-  /* sin(r) approx using a degree 9 polynomial from the Taylor series
-     expansion. Note that only the odd terms of this are non-zero.  */
-  svfloat32_t r2 = svmul_x (pg, r, r);
-  svfloat32_t y;
-  y = svmla_x (pg, C (2), r2, C (3));
-  y = svmla_x (pg, C (1), r2, y);
-  y = svmla_x (pg, C (0), r2, y);
-  y = svmla_x (pg, r, r, svmul_x (pg, y, r2));
-
-  /* sign = y^sign^odd.  */
-  sign = sveor_x (pg, sign, odd);
-
+  svbool_t ptrue = svptrue_b32 ();
+
+  /* Load some constants in quad-word chunks to minimise memory access.  */
+  svfloat32_t negpio2_and_invpio2 = svld1rq (ptrue, &d->neg_pio2_1);
+
+  /* n = rint(x/(pi/2)).  */
+  svfloat32_t q = svmla_lane (sv_f32 (d->shift), x, negpio2_and_invpio2, 3);
+  svfloat32_t n = svsub_x (ptrue, q, d->shift);
+
+  /* r = x - n*(pi/2)  (range reduction into -pi/4 .. pi/4).  */
+  svfloat32_t r = x;
+  r = svmla_lane (r, n, negpio2_and_invpio2, 0);
+  r = svmla_lane (r, n, negpio2_and_invpio2, 1);
+  r = svmla_lane (r, n, negpio2_and_invpio2, 2);
+
+  /* Final multiplicative factor: 1.0 or x depending on bit #0 of q.  */
+  svuint32_t q_u = svreinterpret_u32 (q);
+  svfloat32_t f = svtssel (r, q_u);
+
+  /* sin(r) poly approx.  */
+  svfloat32_t r2 = svtsmul (r, q_u);
+  svfloat32_t y = sv_f32 (0.0f);
+  y = svtmad (y, r2, 4);
+  y = svtmad (y, r2, 3);
+  y = svtmad (y, r2, 2);
+  y = svtmad (y, r2, 1);
+  y = svtmad (y, r2, 0);
+
+  svbool_t cmp = svacge (pg, x, sv_f32 (d->range_val));
   if (__glibc_unlikely (svptest_any (pg, cmp)))
-    return special_case (x,
-			 svreinterpret_f32 (sveor_x (
-			     svnot_z (pg, cmp), svreinterpret_u32 (y), sign)),
-			 cmp);
-  return svreinterpret_f32 (sveor_x (pg, svreinterpret_u32 (y), sign));
+    return special_case (x, svmul_x (ptrue, f, y), cmp);
+  /* Apply factor.  */
+  return svmul_x (ptrue, f, y);
 }
diff --git a/sysdeps/aarch64/fpu/sv_trigf_fallback.h b/sysdeps/aarch64/fpu/sv_trigf_fallback.h
new file mode 100644
index 0000000000..b8967eda59
--- /dev/null
+++ b/sysdeps/aarch64/fpu/sv_trigf_fallback.h
@@ -0,0 +1,184 @@
+/* Vectorised fallback for Single-Precision SVE 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((2/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
+   q mod 4. Tables are split into groups to improve gather performance.  */
+static const struct trigf_fallback_data
+{
+  float d0[16];
+  float d1[16];
+  float d2[16];
+  float d3[16];
+  float pio2;
+} trigf_fallback_data = {
+  .d0 = {
+    0x1p0f,          0x1.0000a2p0f, 0x1.00a2fap0f, 0x1.45f306p-1f,
+    0x1.f306dcp-1f, 0x1.06dc9cp-1f, 0x1.6e4e44p0f, 0x1.4e4416p0f,
+    0x1.44152ap0f,  0x1.1529fcp0f,  0x1.29fc28p0f, 0x1.f84ebp-1f,
+    0x1.2757d2p0f,  0x1.57d1f6p0f, 0x1.a3ea6ap-1f, 0x1.ea69bcp-1f,
+  },
+  .d1 = {
+    0x1.45f306p-25f, 0x1.f306dcp-25f,  -0x1.f246c6p-26f, 0x1.b9391p-26f,
+    0x1.391054p-26f, 0x1.1054a8p-26f,  0x1.529fc2p-28f,  -0x1.ac07b2p-25f,
+    -0x1.ec5418p-31f, 0x1.3abe9p-27f,  -0x1.505c16p-25f, -0x1.70565ap-27f,
+    -0x1.596448p-29f, -0x1.96447ep-25f, -0x1.11f924p-27f, -0x1.f924ecp-27f,
+  },
+  .d2 = {
+    0x1.b9391p-50f,  0x1.391054p-50f, -0x1.df56bp-51f,  0x1.529fc2p-52f,
+    0x1.4fe13ap-51f, -0x1.ec5418p-55f, 0x1.d5f47ep-54f, 0x1.5f47d4p-50f,
+    0x1.f534dep-56f, -0x1.596448p-53f, 0x1.a6ee06p-51f, 0x1.dc0db6p-52f,
+    0x1.b6c52cp-57f, -0x1.24eb54p-51f, -0x1.d6a66cp-52f, 0x1.5993c4p-52f,
+  },
+  .d3 = {
+    0x1.529fc2p-76f,  0x1.4fe13ap-75f,  -0x1.ec5418p-79f, 0x1.d5f47ep-78f,
+    0x1.7d1f54p-76f, 0x1.f534dep-80f,  -0x1.65912p-79f,  0x1.a6ee06p-75f,
+    -0x1.f924ecp-83f, 0x1.b6c52cp-81f, 0x1.b6295ap-76f,  0x1.4acc9ep-79f,
+    -0x1.9b0ef2p-82f, 0x1.93c43ap-76f, -0x1.de37ep-79f,  0x1.c821p-79f,
+  },
+  .pio2 = 0x1.921fb6p+0f,
+};
+
+/* Error-free multiplication using double-float computation via TwoProd.
+   hi is the rounded product, lo is the exact FMA residual.  */
+static inline svfloat32x2_t
+two_prod (svbool_t pg, svfloat32_t a, svfloat32_t b)
+{
+  svfloat32_t hi = svmul_x (pg, a, b);
+  svfloat32_t lo = svnmls_x (pg, hi, a, b);
+  return svcreate2 (hi, lo);
+}
+
+/* Error-free sum using double-float computation via FastTwoSum, which
+   requires |a| >= |b|. hi is the rounded sum and lo recovers the low-order
+   bits lost by that rounding.  */
+static inline svfloat32x2_t
+fast_two_sum (svbool_t pg, svfloat32_t a, svfloat32_t b)
+{
+  svfloat32_t hi = svadd_x (pg, a, b);
+  svfloat32_t t = svsub_x (pg, hi, a);
+  svfloat32_t lo = svsub_x (pg, b, t);
+  return svcreate2 (hi, lo);
+}
+
+/* Gather coefficients of 2/pi for the selected rows.  */
+static inline svfloat32x4_t
+load_datablock (svbool_t pg, svuint32_t idx,
+		const struct trigf_fallback_data *d)
+{
+  idx = svand_x (pg, idx, 15);
+
+  svfloat32_t d0 = svld1_gather_index (pg, d->d0, idx);
+  svfloat32_t d1 = svld1_gather_index (pg, d->d1, idx);
+  svfloat32_t d2 = svld1_gather_index (pg, d->d2, idx);
+  svfloat32_t d3 = svld1_gather_index (pg, d->d3, idx);
+
+  return svcreate4 (d0, d1, d2, d3);
+}
+
+/* Reduce x for |x| > 0x1p8 inputs, such that:
+    x = (q + y) * (pi / 2), with y in [-1/2, 1/2]
+
+   Returns a svfloat32x2_t struct containing:
+    remainder: The remainder after reduction
+    quadrant: Quadrant of x as an integer reinterpreted as a float for packing.
+
+   Designed to be used with the SVE trig instructions.  */
+static inline svfloat32x2_t
+large_range_reduction (svbool_t pg, svfloat32_t x)
+{
+  const struct trigf_fallback_data *d = ptr_barrier (&trigf_fallback_data);
+
+  /* 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 q mod 4 are entirely contained within the
+     lower product terms.  */
+  svuint32_t ix = svreinterpret_u32 (x);
+  svint32_t x_e_m32
+      = svsub_x (pg, svreinterpret_s32 (svlsr_x (pg, ix, 23)), 127 + 32);
+
+  /* We can then use the new exponent as an index for the 2/pi table.  */
+  svuint32_t idx
+      = svreinterpret_u32 (svadd_x (pg, svasr_x (pg, x_e_m32, 3), 3));
+  svfloat32x4_t datablock = load_datablock (pg, idx, d);
+
+  /* x_e_m32 has already been split into:
+       x_e_m32 = 8 * ROW + offset
+     where ROW selected the 2/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.  */
+  svint32_t masked
+      = svreinterpret_s32 (svand_x (pg, svreinterpret_u32 (x_e_m32), 7));
+  svuint32_t new_exponent
+      = svreinterpret_u32 (svlsl_x (pg, svadd_x (pg, masked, 127 + 32), 23));
+
+  /* Finally, we get our reduced x value, by reinserting the new exponent into
+     the original input mantissa.  */
+  svuint32_t signed_mantissa = svand_x (pg, ix, 0x807fffff);
+  svfloat32_t x_reduced
+      = svreinterpret_f32 (svorr_x (pg, new_exponent, signed_mantissa));
+
+  /* We now use the reduced x to calculate x ~= (q + y) * (pi / 2).
+     First, we multiply x_reduced by the first three chunks of the 2/pi
+     table, using double-single arithmetic to maintain a high precision
+     intermediate.  */
+  svfloat32x2_t ph = two_prod (pg, x_reduced, svget4 (datablock, 0));
+  svfloat32x2_t pm = two_prod (pg, x_reduced, svget4 (datablock, 1));
+  svfloat32x2_t pl = two_prod (pg, x_reduced, svget4 (datablock, 2));
+
+  svfloat32_t ph_lo = svget2 (ph, 1);
+  svfloat32_t pm_hi = svget2 (pm, 0);
+  svfloat32_t pm_lo = svget2 (pm, 1);
+  svfloat32_t pl_hi = svget2 (pl, 0);
+  svfloat32_t pl_lo = svget2 (pl, 1);
+
+  /* Next, we need to accumulate the results together to get an integer k for
+     our quadrant. However, ph.hi will always be a multiple of 2^8, so it
+     cannot affect k mod 4. pm.lo and pl will always be sufficiently small
+     that they cannot affect the integer portion of the result. Therefore we
+     only need to sum ph.lo and pm.hi when computing k mod 4. Rounding sum_hi
+     chooses the nearest quadrant in pi/2 units, which is the control value
+     expected by the trig instructions.  */
+  svfloat32_t sum_hi = svadd_x (pg, ph_lo, pm_hi);
+  svfloat32_t kd = svrinta_x (pg, sum_hi);
+
+  /* To compute the remainder, we need to remove k from the pi/2-scaled
+     value and accumulate the remaining terms as a double-single remainder.  */
+  svfloat32_t y_hi = svadd_x (pg, svsub_x (pg, ph_lo, kd), pm_hi);
+  svfloat32x2_t y_mid = fast_two_sum (pg, pm_lo, pl_hi);
+
+  /* The low portion of x_reduced * D3 has no meaningful contribution to the
+    result, so a simple FMA is sufficient.  */
+  svfloat32_t y_lo = svmla_x (pg, pl_lo, x_reduced, svget4 (datablock, 3));
+
+  /* We then accumulate the final hi/lo remainders.  */
+  y_hi = svadd_x (pg, y_hi, svget2 (y_mid, 0));
+  y_lo = svadd_x (pg, y_lo, svget2 (y_mid, 1));
+
+  /* Multiply the accumulated remainders by pi/2, and adding gives a single
+     final remainder.  */
+  svfloat32_t remainder = svmla_x (pg, svmul_x (pg, y_lo, sv_f32 (d->pio2)),
+				   y_hi, sv_f32 (d->pio2));
+  svint32_t quadrant = svcvt_s32_x (pg, kd);
+  /* Reinterpret quadrant into a float to pack into struct for return.  */
+  return svcreate2 (remainder, svreinterpret_f32 (quadrant));
+}