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 in place: with the polynomial with coefficients d[0], ..., d[itrunc - 1], sets d[sd_fft_ctx_trunc_index(L, i)] to a value congruent to for . The inputs must lie in , and so do the outputs.
d must be aligned to 32 bytes and have room for doubles. For the entries from itrunc on are set to zero and the whole transform is computed; for larger , 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 is allowed. The evaluation points do not depend on , so if both truncations are at most the result is that of depth at about the same cost; recovering the input with sd_ifft_trunc requires . 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 in place: if d[i] holds the value at position of the forward transform of a polynomial of length at most trunc, for , sets d[i] to a value congruent to for . The inputs must lie in ; the outputs lie in .
For the whole inverse transform is computed and trunc is ignored, so all values must be present. For larger , trunc is rounded up to a multiple of 256 and may have that length. The outputs are not normalised; the multiplication drivers fold 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 and , , 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 to a value congruent to for . Each costs the same as the transform it transposes.
sd_fft_trunc_tr accepts inputs in and produces outputs in , so it can follow sd_fft_ctx_point_mul; sd_ifft_trunc_tr accepts and produces values in , like sd_fft_trunc. The truncations are rounded as for the untransposed transforms: for to multiples of 256 (sd_fft_trunc_tr first zeroes the entries from otrunc to the next multiple of 256), and for 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 , respectively , modulo for , with and depth at least 4. Inputs in give outputs in , 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 with , 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 be the roots of unity of sd_fft_ctx_w: the first of them are the -th roots of unity in bit-reversed order, for a primitive -th root . For the polynomial formed by the first itrunc entries, sd_fft_trunc stores a value congruent to at position sd_fft_ctx_trunc_index(L, i), for . That position is for ; for each group of 16 values is stored as a transposed 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 : if the first trunc positions hold the values of the forward transform of a polynomial of length at most trunc, it replaces them by values congruent to . 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 in one pass. Taking 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 , , where is the entry at position sd_fft_ctx_trunc_index(L, i). See Transposed transforms.
Requirements#
dmust be aligned to 32 bytes and hold doubles.- Entries are integers. The forward transform accepts and returns entries in . The inverse transform accepts entries in , which
sd_fft_ctx_point_mulproduces from forward outputs, and returns entries in . Reduce withnmod_set_sior the vector reductions ofmachine_vectors.hto obtain residues. - Truncations are rounded: for the forward transform zeroes the entries from
itruncon and computes all values, and the inverse transform ignorestruncand needs all values. For 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 entries of both arrays, so entries beyond the truncation must hold some finite values; it requires and .- The transposed transforms have the rounding of the transforms they transpose.
sd_ifft_trunc_traccepts and returns entries in , likesd_fft_trunc;sd_fft_trunc_traccepts entries in , likesd_ifft_trunc, and returns entries in . - The context must cover depth , which all four transforms ensure by calling
sd_fft_ctx_fit_depth, and must divide .
Algorithm#
The transform is a fast Fourier transform [CT1965] over [Pol1971], computed as a tree of reductions of modulo [Ber2008]: the butterfly splits a residue modulo into its residues modulo and . Two levels are merged into radix-4 butterflies. Above 256 points it is organised as Bailey's matrix algorithm [Bai1990]: the length blocks are viewed as a matrix of rows of blocks, , 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 , 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 of the forward transform is , 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 of length , multiplication by of polynomials of length is, for a transform truncation , the composition of the forward transform of (itrunc , otrunc ), the pointwise product with , and the inverse transform with truncation , followed by taking the first coefficients. Its transpose is for of length padded with zeros to , and by Lemma 2 of Middle products and the transposition principle it computes for : a middle product, at the cost of a product of lengths and . _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 , 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 , , of of length 300 and of length 699 modulo the prime, with transforms of length 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-11-02).
@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-11-02}
}