Expand description
Minimum task sizes for parallel loops.
Splitting a loop across workers buys throughput and costs latency. Both sides of that trade collapse into one number, the fewest items a task may hold:
floor >= total items -> one task
floor < total items -> total / floor tasksA floor lets a call site write its loop body once. Branching on a length writes the body twice, and can only ask for one task or for all.
§Entering a loop
One task is not yet a serial loop. rayon reaches a body through its own bridge and consumer, even with nothing to split.
That costs about 1 ns per item, which is a tenth of a fold of a few field operations.
Two terminals take the leaf iterator directly once the floor forbids a split:
A loop written with either runs as a plain serial loop, with no bridge to pay for.
TaskSizeExt::with_min_task_bytes floors the split and does nothing else.
Use it where one item is a whole chunk, whose own work dwarfs the per-item cost.
§Cost model
One item is charged by the bytes it moves, at a fixed rate per byte.
Moved bytes are counted as every element the body reads plus every element it writes. A read-modify-write of one element is therefore two, not one.
- Bytes track the element type, so the floor follows it without being restated.
- A packed extension element costs 32 times a 4-byte prime field element.
- A task therefore needs 32 times fewer of the wide items than of the narrow ones.
The rate assumes a body with instruction-level parallelism, as a streaming loop has. A body that waits on its own previous step charges a multiple of its width.
Charging too little is safe for how a split loop is cut up. It is not safe for whether the loop splits at all, which is why a slow body says so.
§Pool size
The time a loop must be worth scales with the number of workers. Waking a wider pool costs more, so a wider pool demands a longer loop.
A pool of one worker never splits at all, which is also what a build without rayon gets.
§Overrides
P3_MIN_PARALLEL_NSfixes the serial time a loop must be worth, ignoring the pool size.P3_MAX_TASK_NSsets the time budget one task holds once a loop does split.- Setting both to
0drops every floor to a single item. - The default gate is calibrated separately for Linux and macOS dispatch costs.
A floor of one is rayon’s own unbounded splitting, which is what an A/B run of a loop wants.
A site that uses the floor as its own chunk length reads that same 0 as a chunk of one item.
That shape is slower than either arm, and it is not one the model ever picks.
The packed fill behind a table of ascending powers is the site to watch.
It pays one exponentiation per chunk, so a chunk of one item pays one per item.
On 32 Linux workers that setting costs the fill 60x at 2^12 powers and about 4x at 2^22.
Twiddle tables and query power tables both come from it, so a whole-prover A/B mostly times it.
A lower bound on the chunk does not rescue the setting.
One fill round as the bound wins back 2 to 3x at 2^22 and nothing at 2^12.
Such a site is measured against its own body rather than through these knobs.
A caller that would rather not depend on the ambient environment sets both from code.
Either way the budgets are fixed once and then hold for the rest of the process. The first parallel loop fixes them, so a change after that point is refused.
§Examples
use p3_maybe_rayon::prelude::*;
let data = vec![1u64; 1 << 10];
// An adapter changes only how the loop is divided, never what it covers.
let sum: u64 = data.par_iter().with_min_task::<u64>().sum();
assert_eq!(sum, 1 << 10);
// A call site that picks its own chunk length asks for the floor directly.
// The answer moves with the pool and with any override, so only its range is fixed.
let chunk = min_task_len(data.len(), size_of::<u64>());
assert!((1..=data.len()).contains(&chunk));Traits§
- Task
Size Ext - Task-size adapters for parallel iterators.
Functions§
- min_
task_ len - Minimum items one task must hold, for a loop of the given shape.
- should_
split - Whether a loop of the given shape is worth splitting at all.