Transforms modulo one prime
#include <flint/fft_small.h>void sd_fft_trunc(sd_fft_ctx_t Q, double * d, ulong L, ulong itrunc, ulong otrunc)sourceTruncated forward transform of length 2^L in place: with f the polynomial with coefficients d[0], ..., d[itrunc - 1], sets d[sd_fft_ctx_trunc_index(L, i)] to a value congruent to f(w_i) for 0 \le i < \mathtt{otrunc}. The inputs must lie in (-3p, 3p), and so do the outputs.
d must be aligned to 32 bytes and have room for 2^L doubles. For L \le 8 the entries from itrunc on are set to zero and the whole transform is computed; for larger L, itrunc and otrunc are rounded up to multiples of 256, the entries from itrunc to the next multiple of 256 are set to zero, and only the blocks below the rounded otrunc are written. Any 1 \le \mathtt{itrunc}, \mathtt{otrunc} \le 2^L is allowed. The evaluation points do not depend on L, so if both truncations are at most 2^{L-1} the result is that of depth L - 1 at about the same cost; recovering the input with sd_ifft_trunc requires \mathtt{otrunc} \ge \mathtt{itrunc}. See Transforms modulo one prime.
void sd_ifft_trunc(sd_fft_ctx_t Q, double * d, ulong L, ulong trunc)sourceTruncated inverse transform of length 2^L in place: if d[i] holds the value at position i of the forward transform of a polynomial g of length at most trunc, for i < \mathtt{trunc}, sets d[i] to a value congruent to 2^L g_i for i < \mathtt{trunc}. The inputs must lie in (-2p, 2p); the outputs lie in (-3p, 3p).
For L \le 8 the whole inverse transform is computed and trunc is ignored, so all 2^L values must be present. For larger L, trunc is rounded up to a multiple of 256 and g may have that length. The outputs are not normalised; the multiplication drivers fold 2^{-L} into the pointwise products.
void sd_fft_trunc_tr(sd_fft_ctx_t Q, double * d, ulong L, ulong itrunc, ulong otrunc)sourcevoid sd_ifft_trunc_tr(sd_fft_ctx_t Q, double * d, ulong L, ulong trunc)sourceThe transposes of sd_fft_trunc and sd_ifft_trunc with the same parameters, in place: for all vectors a and v, \langle \mathtt{sd\_fft\_trunc}(a), v \rangle = \langle a, \mathtt{sd\_fft\_trunc\_tr}(v) \rangle, and likewise for the inverse transform. sd_fft_trunc_tr reads the first otrunc entries, indexed like the output of sd_fft_trunc, and sets d_t to a value congruent to \sum_{i < \mathtt{otrunc}} d[\mathtt{sd\_fft\_ctx\_trunc\_index}(L, i)]\, w_i^t for 0 \le t < \mathtt{itrunc}. Each costs the same as the transform it transposes.
sd_fft_trunc_tr accepts inputs in (-2p, 2p) and produces outputs in (-2p, 2p), so it can follow sd_fft_ctx_point_mul; sd_ifft_trunc_tr accepts and produces values in (-3p, 3p), like sd_fft_trunc. The truncations are rounded as for the untransposed transforms: for L > 8 to multiples of 256 (sd_fft_trunc_tr first zeroes the entries from otrunc to the next multiple of 256), and for L \le 8 the full transform is computed (sd_fft_trunc_tr zeroes the entries from otrunc on, and sd_ifft_trunc_tr ignores trunc). The transposed inverse transform followed by a pointwise product and the transposed forward transform computes a transposed convolution; see Transposed transforms and Transposed transforms.
void sd_fft_ctx_point_mul(const sd_fft_ctx_t Q, double * a, const double * b, ulong m_, ulong depth)sourcevoid sd_fft_ctx_point_sqr(const sd_fft_ctx_t Q, double * a, ulong m_, ulong depth)sourceSet a_i \gets m a_i b_i, respectively m a_i^2, modulo p for i < 2^{\mathtt{depth}}, with 0 \le m \le p and depth at least 4. Inputs in (-3p, 3p) give outputs in (-3p/2, 3p/2), valid inputs for sd_ifft_trunc.
sd_fft_trunc and sd_ifft_trunc are the number-theoretic transforms that all of fft_small is built on. They work in place on an array of doubles holding integers, modulo a prime p < 2^{50} with 2^L \mid p - 1, and are truncated: only part of the input is nonzero and only part of the output is needed. Using them directly makes sense for convolutions modulo one of the context primes, or modulo another suitable prime with a context of its own.
What they compute#
Let w_0, w_1, w_2, \ldots be the roots of unity of sd_fft_ctx_w: the first 2^L of them are the 2^L-th roots of unity in bit-reversed order, w_i = \omega^{\operatorname{rev}_L(i)} for a primitive 2^L-th root \omega. For the polynomial f = \sum_{j < \mathtt{itrunc}} d_j x^j formed by the first itrunc entries, sd_fft_trunc stores a value congruent to f(w_i) at position sd_fft_ctx_trunc_index(L, i), for i < \mathtt{otrunc}. That position is i for L \le 3; for L \ge 4 each group of 16 values is stored as a transposed 4 \times 4 matrix, because the innermost kernel saves its final transposition. Pointwise operations do not care about the order, and the inverse transform expects the same one.
sd_ifft_trunc inverts the transform up to the factor 2^L: if the first trunc positions hold the values of the forward transform of a polynomial g of length at most trunc, it replaces them by values congruent to 2^L g_0, \ldots, 2^L g_{\mathtt{trunc}-1}. The truncated inverse needs no other values, because the missing evaluations are determined by the known zero coefficients [vdH2004].
sd_fft_ctx_point_mul multiplies the transforms pointwise and by a constant m in one pass. Taking m = 2^{-L} \bmod p makes the inverse transform return the product itself.
sd_fft_trunc_tr and sd_ifft_trunc_tr are the transposes of the two transforms, with the same parameters: sd_fft_trunc_tr reads otrunc values on the evaluation side, in the order above, and writes itrunc coefficients \sum_{i < \mathtt{otrunc}} v_i w_i^t, t < \mathtt{itrunc}, where v_i is the entry at position sd_fft_ctx_trunc_index(L, i). See Transposed transforms.
Requirements#
dmust be aligned to 32 bytes and hold 2^L doubles.- Entries are integers. The forward transform accepts and returns entries in (-3p, 3p). The inverse transform accepts entries in (-2p, 2p), which
sd_fft_ctx_point_mulproduces from forward outputs, and returns entries in (-3p, 3p). Reduce withnmod_set_sior the vector reductions ofmachine_vectors.hto obtain residues. - Truncations are rounded: for L \le 8 the forward transform zeroes the entries from
itruncon and computes all 2^L values, and the inverse transform ignorestruncand needs all 2^L values. For L > 8 both work on blocks of 256 entries:itrunc,otruncandtruncare rounded up to multiples of 256, the forward transform zeroes the entries fromitruncto the next multiple of 256, and entries beyond the rounded output truncation are neither read nor written. sd_fft_ctx_point_multouches all 2^L entries of both arrays, so entries beyond the truncation must hold some finite values; it requires L \ge 4 and 0 \le m \le p.- The transposed transforms have the rounding of the transforms they transpose.
sd_ifft_trunc_traccepts and returns entries in (-3p, 3p), likesd_fft_trunc;sd_fft_trunc_traccepts entries in (-2p, 2p), likesd_ifft_trunc, and returns entries in (-2p, 2p). - The context must cover depth L, which all four transforms ensure by calling
sd_fft_ctx_fit_depth, and 2^L must divide p - 1.
Algorithm#
The transform is a fast Fourier transform [CT1965] over \mathbb{Z}/p\mathbb{Z} [Pol1971], computed as a tree of reductions of f modulo x^{2^k} - w_j [Ber2008]: the butterfly (a, b) \mapsto (a + sb, a - sb) splits a residue modulo x^{2h} - s^2 into its residues modulo x^h - s and x^h + s. Two levels are merged into radix-4 butterflies. Above 256 points it is organised as Bailey's matrix algorithm [Bai1990]: the length 2^k blocks are viewed as a matrix of 2^{k_1} rows of 2^{k_2} blocks, k_1 = \lfloor k/2 \rfloor, and column transforms on strided blocks are followed by row transforms with twisted roots, each recursively. Within a block, fixed-size kernels keep the data in registers. The inverse transform runs the same structure backwards with Gentleman–Sande butterflies [GS1966]. Each butterfly uses the floating-point modular arithmetic of Modular arithmetic in floating point, with lazy reductions whose ranges are proved on the algorithm page.
AlgorithmFFTs modulo word-size primes in floating pointThe number-theoretic transforms offft_small modulo primes p < 2^{50}, on integers stored in doubles: the order of the roots, radix-4 butterflies with lazy reduction, blocking and truncation, and proofs that every value stays in range.
Transposed transforms#
A transform computed by a linear program of butterflies has a transpose computed by the same butterflies in reverse order, each replaced by its transpose, at exactly the same cost (the transposition principle, The transposition principle). The transpose of the butterfly (a, b) \mapsto (a + wb, a - wb) of the forward transform is (a, b) \mapsto (a + b, w(a - b)), the shape of the inverse butterflies with the forward twiddles, and conversely; truncation transposes into truncation, with the roles of itrunc and otrunc exchanged.
Their use is the transposed convolution. For a fixed a of length a_n, multiplication by a of polynomials of length z_n is, for a transform truncation T \ge a_n + z_n - 1, the composition b \mapsto I(m\,\hat a \cdot F(b)) of the forward transform F of b (itrunc = z_n, otrunc = T), the pointwise product with m\,\hat a, and the inverse transform I with truncation T, followed by taking the first a_n + z_n - 1 coefficients. Its transpose is v \mapsto F^T(m\,\hat a \cdot I^T(v)) for v of length a_n + z_n - 1 padded with zeros to T, and by Lemma 2 of Middle products and the transposition principle it computes z_j = \sum_i a_i v_{i+j} for j < z_n: a middle product, at the cost of a product of lengths a_n and z_n. _nmod_poly_mul_tr_mpn_ctx is this computation modulo several primes; see Transposed transforms.
Examples#
The positions of the first values of a transform of length 16, and the inverse transform:
#include <flint/flint.h>
#include <flint/nmod.h>
#include <flint/fft_small.h>
int main(void)
{
mpn_ctx_struct * R = get_default_mpn_ctx();
sd_fft_ctx_struct * Q = R->ffts + 0; /* p = 63*2^44 + 1 */
nmod_t mod = Q->mod;
ulong L = 4, N = 16, i, j, k, fx, x, inv16;
ulong f[3] = { 1, 2, 3 }; /* f = 1 + 2x + 3x^2 */
double * d = flint_aligned_alloc(32, N * sizeof(double));
for (i = 0; i < 3; i++)
d[i] = f[i];
/* forward transform: f(w_i) lands at position trunc_index(L, i) */
sd_fft_trunc(Q, d, L, 3, N);
for (i = 0; i < N; i++)
{
x = nmod_set_si((slong) sd_fft_ctx_w(Q, i), mod);
fx = 0;
for (j = 3; j-- > 0; )
fx = nmod_add(nmod_mul(fx, x, mod), f[j], mod);
k = sd_fft_ctx_trunc_index(L, i);
if (nmod_set_si((slong) d[k], mod) != fx)
flint_printf("mismatch at i = %wu\n", i);
if (i < 6)
flint_printf("w_%wu: position %2wu\n", i, k);
}
/* inverse transform: returns 16 f, to be divided by 2^L */
sd_ifft_trunc(Q, d, L, N);
inv16 = nmod_inv(16, mod);
for (i = 0; i < 4; i++)
flint_printf("%wu ", nmod_mul(nmod_set_si((slong) d[i], mod), inv16, mod));
flint_printf("\n");
flint_aligned_free(d);
flint_cleanup_master();
return 0;
}w_0: position 0 w_1: position 4 w_2: position 8 w_3: position 12 w_4: position 1 w_5: position 5 1 2 3 0
A product of polynomials of lengths 300 and 200 modulo the prime with truncated transforms of length 2^{10}, of which only the first 512 values are computed:
#include <flint/flint.h>
#include <flint/nmod.h>
#include <flint/nmod_vec.h>
#include <flint/nmod_poly.h>
#include <flint/fft_small.h>
int main(void)
{
sd_fft_ctx_struct * Q = get_default_mpn_ctx()->ffts + 0;
nmod_t mod = Q->mod;
slong an = 300, bn = 200, zn = an + bn - 1, i;
ulong L = 10, m;
nn_ptr a = _nmod_vec_init(an), b = _nmod_vec_init(bn);
nn_ptr c = _nmod_vec_init(zn), z = _nmod_vec_init(zn);
double * x = flint_aligned_alloc(32, 1024 * sizeof(double));
double * y = flint_aligned_alloc(32, 1024 * sizeof(double));
for (i = 0; i < an; i++)
a[i] = nmod_pow_ui(i + 1, 7, mod);
for (i = 0; i < bn; i++)
b[i] = nmod_pow_ui(i + 2, 9, mod);
/* residues in [0, p) are valid inputs; the unused tail is zeroed
because sd_fft_ctx_point_mul reads all 2^L entries */
for (i = 0; i < 1024; i++)
x[i] = y[i] = 0.0;
for (i = 0; i < an; i++)
x[i] = (double) a[i];
for (i = 0; i < bn; i++)
y[i] = (double) b[i];
/* only 512 of the 1024 values are needed, since zn <= 512 */
sd_fft_trunc(Q, x, L, an, 512);
sd_fft_trunc(Q, y, L, bn, 512);
/* x = x * y / 2^L pointwise, then the inverse transform */
m = nmod_inv(nmod_pow_ui(2, L, mod), mod);
sd_fft_ctx_point_mul(Q, x, y, m, L);
sd_ifft_trunc(Q, x, L, 512);
for (i = 0; i < zn; i++)
z[i] = nmod_set_si((slong) x[i], mod);
_nmod_poly_mul(c, a, an, b, bn, mod);
flint_printf("product modulo p: %s\n",
_nmod_vec_equal(z, c, zn) ? "agrees" : "WRONG");
_nmod_vec_clear(a);
_nmod_vec_clear(b);
_nmod_vec_clear(c);
_nmod_vec_clear(z);
flint_aligned_free(x);
flint_aligned_free(y);
flint_cleanup_master();
return 0;
}product modulo p: agrees
The transposed product z_j = \sum_i a_i v_{i+j}, j < 400, of a of length 300 and v of length 699 modulo the prime, with transforms of length 2^{10} truncated to 768 points:
#include <flint/flint.h>
#include <flint/nmod.h>
#include <flint/nmod_vec.h>
#include <flint/fft_small.h>
int main(void)
{
sd_fft_ctx_struct * Q = get_default_mpn_ctx()->ffts + 0;
nmod_t mod = Q->mod;
slong an = 300, zn = 400, vn = an + zn - 1, i, j;
ulong L = 10, T = 768, m, s;
nn_ptr a = _nmod_vec_init(an), v = _nmod_vec_init(vn);
nn_ptr z = _nmod_vec_init(zn);
double * x = flint_aligned_alloc(32, 1024 * sizeof(double));
double * y = flint_aligned_alloc(32, 1024 * sizeof(double));
int ok = 1;
for (i = 0; i < an; i++)
a[i] = nmod_pow_ui(i + 1, 7, mod);
for (i = 0; i < vn; i++)
v[i] = nmod_pow_ui(i + 2, 9, mod);
for (i = 0; i < 1024; i++)
x[i] = y[i] = 0.0;
for (i = 0; i < an; i++)
x[i] = (double) a[i];
for (i = 0; i < vn; i++)
y[i] = (double) v[i];
/* a is transformed as for a product of length T */
sd_fft_trunc(Q, x, L, an, T);
/* the transposed pipeline: ifft_tr, pointwise product, fft_tr */
m = nmod_inv(nmod_pow_ui(2, L, mod), mod);
sd_ifft_trunc_tr(Q, y, L, T);
sd_fft_ctx_point_mul(Q, y, x, m, L);
sd_fft_trunc_tr(Q, y, L, zn, T);
for (j = 0; j < zn; j++)
{
s = 0;
for (i = 0; i < an; i++)
s = nmod_addmul(s, a[i], v[i + j], mod);
z[j] = nmod_set_si((slong) y[j], mod);
ok = ok && (z[j] == s);
}
flint_printf("transposed product: %s\n", ok ? "agrees" : "WRONG");
_nmod_vec_clear(a);
_nmod_vec_clear(v);
_nmod_vec_clear(z);
flint_aligned_free(x);
flint_aligned_free(y);
flint_cleanup_master();
return 0;
}transposed product: agrees
Guides#
References#
- [vdH2004] J. van der Hoeven, "Truncated FFTs", ISSAC 2004 notes, https://www.texmacs.org/joris/issac04/issac04.html
- [CT1965] J. W. Cooley and J. W. Tukey. An algorithm for the machine calculation of complex Fourier series. Mathematics of Computation 19 (1965), 297–301.
- [Pol1971] J. M. Pollard. The fast Fourier transform in a finite field. Mathematics of Computation 25 (1971), 365–374.
- [Ber2008] D. J. Bernstein, "Fast multiplication and its applications", in Algorithmic Number Theory, MSRI Publications 44 (2008), 325–384
- [Bai1990] D. H. Bailey. FFTs in external or hierarchical memory. The Journal of Supercomputing 4 (1990), 23–35.
- [GS1966] W. M. Gentleman and G. Sande. Fast Fourier transforms – for fun and profit. AFIPS Fall Joint Computer Conference 29 (1966), 563–578.
The FLINT team, “Transforms modulo one prime (fft_small.h),” FLINT: Fast Library for Number Theory, documentation, version 3.7.0-dev, 2026. https://flintlib.org/doc/fft_small/sd_fft.html (generated 2026-10-20).
@misc{flint-doc-fft-small-sd-fft,
author = {{The FLINT team}},
title = {{Transforms modulo one prime (fft\_small.h)}},
howpublished = {FLINT: Fast Library for Number Theory, documentation, version 3.7.0-dev},
year = {2026},
url = {https://flintlib.org/doc/fft_small/sd_fft.html},
note = {Generated 2026-10-20}
}