[PATCH 07/10] sntrup761: defer reduction in polynomial multiplication

Jussi Kivilinna <[email protected]> Sun, 2 Aug 2026 12:55:12 +0300
Newsgroups gmane.comp.encryption.gpg.libgcrypt.devel
Message-ID <[email protected]>
* cipher/sntrup761.c (Rq_mult_small, R3_mult): Accumulate inner product
in int32_t and reduce once per output coefficient.
* tests/bench-slope.c (pq_algos): Use full measurement repetitions for
sntrup761.
--

Both multiplications called 'Fq_freeze'/'F3_freeze' on every multiply-
accumulate of the O(p^2) inner loop. Terms are bounded by q12 for Rq and
by one for R3, and there are at most p of them, so the accumulator stays
far from int32_t range and reduction is needed only for the result.

Benchmark on AMD Ryzen 9 9950X3D, SNTRUP761 usec/operation, quick
random disabled ('base' being state before this patch series):

             |     base   before    after   speedup    total
 keygen      |  37971.9   2900.4   1971.5     1.47x   19.26x
 encap       |   7355.8   1184.7    243.0     4.88x   30.27x
 decap       |  12207.1   3322.4    592.5     5.61x   20.60x

Remaining keygen time is in R3_recip and Rq_recip3, where reduction is
part of an element-wise running update and cannot be deferred.

Signed-off-by: Jussi Kivilinna <[email protected]>
---
 cipher/sntrup761.c  | 24 ++++++++++++++----------
 tests/bench-slope.c |  2 +-
 2 files changed, 15 insertions(+), 11 deletions(-)

diff --git a/cipher/sntrup761.c b/cipher/sntrup761.c
index 94229d79..bcb6e651 100644
--- a/cipher/sntrup761.c
+++ b/cipher/sntrup761.c
@@ -451,22 +451,24 @@ static void
 R3_mult (small * h, const small * f, const small * g)
 {
   small fg[p + p - 1];
-  small result;
+  int32_t result;
   int i, j;
 
+  /* Terms are in {-1,0,1} and there are at most p of them, so the
+     accumulator cannot overflow and needs reduction only once. */
   for (i = 0; i < p; ++i)
     {
       result = 0;
       for (j = 0; j <= i; ++j)
-	result = F3_freeze (result + f[j] * g[i - j]);
-      fg[i] = result;
+	result += f[j] * g[i - j];
+      fg[i] = F3_freeze (result);
     }
   for (i = p; i < p + p - 1; ++i)
     {
       result = 0;
       for (j = i - p + 1; j < p; ++j)
-	result = F3_freeze (result + f[j] * g[i - j]);
-      fg[i] = result;
+	result += f[j] * g[i - j];
+      fg[i] = F3_freeze (result);
     }
 
   for (i = p + p - 2; i >= p; --i)
@@ -547,22 +549,24 @@ static void
 Rq_mult_small (Fq * h, const Fq * f, const small * g)
 {
   Fq fg[p + p - 1];
-  Fq result;
+  int32_t result;
   int i, j;
 
+  /* Products are bounded by q12 and there are at most p terms, so the
+     accumulator cannot overflow and needs reduction only once. */
   for (i = 0; i < p; ++i)
     {
       result = 0;
       for (j = 0; j <= i; ++j)
-	result = Fq_freeze (result + f[j] * (int32_t) g[i - j]);
-      fg[i] = result;
+	result += f[j] * (int32_t) g[i - j];
+      fg[i] = Fq_freeze (result);
     }
   for (i = p; i < p + p - 1; ++i)
     {
       result = 0;
       for (j = i - p + 1; j < p; ++j)
-	result = Fq_freeze (result + f[j] * (int32_t) g[i - j]);
-      fg[i] = result;
+	result += f[j] * (int32_t) g[i - j];
+      fg[i] = Fq_freeze (result);
     }
 
   for (i = p + p - 2; i >= p; --i)
diff --git a/tests/bench-slope.c b/tests/bench-slope.c
index 4b296f0b..911b2b15 100644
--- a/tests/bench-slope.c
+++ b/tests/bench-slope.c
@@ -3691,7 +3691,7 @@ static const struct
 #endif
   { "sntrup761", GCRY_KEM_SNTRUP761, NULL,
     GCRY_KEM_SNTRUP761_PUBKEY_LEN, GCRY_KEM_SNTRUP761_SECKEY_LEN,
-    GCRY_KEM_SNTRUP761_ENCAPS_LEN, GCRY_KEM_SNTRUP761_SHARED_LEN, 16, 4, 0 },
+    GCRY_KEM_SNTRUP761_ENCAPS_LEN, GCRY_KEM_SNTRUP761_SHARED_LEN, 1, 1, 0 },
   { "cm6688128f", GCRY_KEM_CM6688128F, NULL,
     GCRY_KEM_CM6688128F_PUBKEY_LEN, GCRY_KEM_CM6688128F_SECKEY_LEN,
     GCRY_KEM_CM6688128F_ENCAPS_LEN, GCRY_KEM_CM6688128F_SHARED_LEN, 16, 1, 1 },
-- 
2.53.0