gh-38476: Include modular composition for polynomial rings over finite fields
Modular composition was very slow for polynomial rings over finite
fields, so I have exposed and used the functions from NTL / Flint.
Compared to the naive method we see very nice speed ups.
```py
sage: R.<x> = GF(2**127 - 1)[]
sage: f = R.random_element(degree=100)
sage: g = R.random_element(degree=100)
sage: h = R.random_element(degree=50)
sage: %timeit f(g) % h
960 ms ± 10.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
sage: %timeit f.modular_composition(g, h)
721 µs ± 10.4 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
sage: f(g) % h == f.modular_composition(g, h)
True
sage:
sage: R.<x> = GF((2**127 - 1, 2))[]
sage: f = R.random_element(degree=100)
sage: g = R.random_element(degree=100)
sage: h = R.random_element(degree=50)
sage: %timeit f(g) % h
1.03 s ± 6.74 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
sage: %timeit f.modular_composition(g, h)
29.1 ms ± 346 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
sage: f(g) % h == f.modular_composition(g, h)
True
sage:
sage: R.<x> = GF(163)[]
sage: f = R.random_element(degree=100)
sage: g = R.random_element(degree=100)
sage: h = R.random_element(degree=50)
sage: %timeit f(g) % h
1.68 ms ± 39.9 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops
each)
sage: %timeit f.modular_composition(g, h)
283 µs ± 5.27 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
sage: f(g) % h == f.modular_composition(g, h)
True
```
Both `./sage -t` and `./sage -tox ...` are erroring on my new build
after 10.4 so I'm going to let the CI catch my typos.
URL: https://github.com/sagemath/sage/pull/38476
Reported by: Giacomo Pope
Reviewer(s): Giacomo Pope, Lorenz Panny