FLINT3.7.0-dev
Algorithm

Parallel loops and binary splitting

Two generic helpers distribute work over the threads of the pool (The thread pool): flint_parallel_do runs independent calls f(0),…,f(n−1)f(0), \ldots, f(n-1), and flint_parallel_binary_splitting runs the two halves of a binary splitting recursion in parallel.

Static scheduling of a loop#

flint_parallel_do evaluates f(0),…,f(n−1)f(0), \ldots, f(n-1) with TT threads, where TT is the number of workers obtained plus the calling thread. The indices are divided once, before the threads start; there is no work stealing. Two schedules are available.

Blocks (FLINT_PARALLEL_UNIFORM): with s=⌈n/T⌉s = \lceil n/T \rceil, thread tt gets the indices ts≤i<min⁡((t+1)s,n)ts \le i < \min((t+1)s, n). When all calls cost the same, the loads differ by at most ss calls, and each thread works on a contiguous range, which is good for memory locality.

Strides (FLINT_PARALLEL_STRIDED): thread tt gets the indices i≡t(modT)i \equiv t \pmod T. This is the better choice when the cost cic_i of call ii varies monotonically, as it does when ii indexes coefficients or subproblems of increasing size.

Theorem 1 (Balance of strided scheduling). Let c0≤c1≤⋯≤cn−1c_0 \le c_1 \le \cdots \le c_{n-1} and let St=∑i≡t mod TciS_t = \sum_{i \equiv t \bmod T} c_i be the load of thread tt under strided scheduling. Then ∣St−Su∣≤cn−1\lvert S_t - S_u \rvert \le c_{n-1} for all 0≤t,u<T0 \le t, u < T.

Proof. Let t<ut < u and let mtm_t and mum_u be the numbers of indices of the two threads. The index sets are {t+kT:0≤k<mt}\{t + kT : 0 \le k < m_t\} and {u+kT:0≤k<mu}\{u + kT : 0 \le k < m_u\}, and mt−1≤mu≤mtm_t - 1 \le m_u \le m_t because t<u<t+Tt < u < t + T.

Since t+kT<u+kTt + kT < u + kT, monotonicity gives ct+kT≤cu+kTc_{t+kT} \le c_{u+kT} for k<muk < m_u. Hence St≤Su+(mt−mu)cn−1≤Su+cn−1S_t \le S_u + (m_t - m_u) c_{n-1} \le S_u + c_{n-1}.

Since u+kT<t+(k+1)Tu + kT < t + (k+1)T, also cu+kT≤ct+(k+1)Tc_{u+kT} \le c_{t+(k+1)T} whenever k+1<mtk + 1 < m_t. If mu=mt−1m_u = m_t - 1, summing over k<muk < m_u gives Su≤St−ct≤StS_u \le S_t - c_t \le S_t. If mu=mtm_u = m_t, the same sum covers all but the last index of thread uu, so Su≤St−ct+cu+(mu−1)T≤St+cn−1S_u \le S_t - c_t + c_{u + (m_u - 1)T} \le S_t + c_{n-1}. The case of nonincreasing costs is symmetric.

With blocks, monotone costs can make the imbalance as large as ⌈n/T⌉(cn−1−c0)\lceil n/T \rceil (c_{n-1} - c_0), roughly the difference between the first and last block.

The flag FLINT_PARALLEL_DYNAMIC is reserved for dynamic scheduling, in which threads take new indices from a shared counter as they finish. It is not implemented and currently gives blocks.

Parallel binary splitting#

Binary splitting evaluates P(a,b)=f(a)∘⋯∘f(b−1)P(a, b) = f(a) \circ \cdots \circ f(b-1) as P(a,m)∘P(m,b)P(a, m) \circ P(m, b) with m=a+⌊(b−a)/2⌋m = a + \lfloor (b - a)/2 \rfloor, recursing until b−ab - a is at most a cutoff [HP1998; Ber2008]. The two recursive calls are independent, so the recursion tree has natural parallelism near its root.

Algorithm 1: flint_parallel_binary_splitting

Input: a range [a,b)[a, b), a cutoff c≥1c \ge 1 and a thread budget tt (thread_limit, or flint_get_num_threads() if not positive).

  1. If b−a≤cb - a \le c, compute P(a,b)P(a, b) with the base case and return.
  2. Request one worker, subject to the caller's own budget.
  3. If one was obtained: wake it on P(m,b)P(m, b) with budget ⌊t/2⌋−1\lfloor t/2 \rfloor - 1, lower the caller's budget to t−⌊t/2⌋−1t - \lfloor t/2 \rfloor - 1 and compute P(a,m)P(a, m), then restore the budget and wait for the worker.
  4. Otherwise compute P(a,m)P(a, m) and P(m,b)P(m, b) one after the other.
  5. Return the merge of the two results.

Each level halves the budget, so with T=2kT = 2^k threads the top kk levels of the tree run in parallel and every thread then works on one subtree of size about (b−a)/T(b - a)/T. The two recursive calls receive their shares as thread_limit, so no more than tt threads run base cases at the same time. For products whose operands grow along the recursion, most of the time is spent in the last merges near the root, so the speedup of binary splitting by itself is limited; it is most effective when the merge operation, typically a large multiplication, is itself parallel.

void flint_parallel_do(do_func_t f, void * args, slong n, int thread_limit, int flags)
void flint_parallel_binary_splitting(void * res, bsplit_basecase_func_t basecase, bsplit_merge_func_t merge, size_t sizeof_res, bsplit_init_func_t init, bsplit_clear_func_t clear, void * args, slong a, slong b, slong basecase_cutoff, int thread_limit, int flags)

Referenced from#

References#

  • [HP1998] B. Haible and T. Papanikolaou, "Fast multiprecision evaluation of series of rational numbers", Algorithmic Number Theory (ANTS-III), LNCS 1423 (1998), 338–350
  • [Ber2008] D. J. Bernstein, "Fast multiplication and its applications", in Algorithmic Number Theory, MSRI Publications 44 (2008), 325–384
Cite this page

The FLINT team, “Parallel loops and binary splitting,” FLINT: Fast Library for Number Theory, documentation, version 3.7.0-dev, 2026. https://flintlib.org/doc/algorithms/parallel/parallel-loops.html (generated 2026-11-02).