// Package parallel provides the chunked worker-pool primitives the engine's // kernels build on -- the Go stand-in for rayon's fold/reduce shape. Public // engine signatures never expose this package; parallelism stays an // implementation detail behind pure-std func values.
package parallel import ( ) // Workers is the parallelism ceiling: GOMAXPROCS. func () int { return runtime.GOMAXPROCS(0) } // chunkPlan splits [0, n) into `count` contiguous chunks of `size` (the last // short), count at most workers*4. Bounding the chunk count keeps per-chunk // accumulator allocations small (a map-typed accumulator otherwise pays one // allocation per chunk -- the pathology the Rust NodeSet::par_fold comment // documents), while the x4 headroom keeps load balancing when chunks are // uneven. Chunk k spans [k*size, min((k+1)*size, n)); returning the plan as // two ints rather than a materialised []​[2]int keeps For/ForWorker/Fold from // allocating a slice on every call. count is 0 for n <= 0. func chunkPlan( int) (, int) { if <= 0 { return 0, 0 } := max(Workers(), 1) * 4 = max(/, 1) return ( + - 1) / , } // Chunks reports how many contiguous chunks For/ForWorker split [0, n) // into (the load-balancing granularity). NOTE: ForWorker worker indexes // are POOL worker ids bounded by Workers(), not by this -- size // per-worker scratch with Workers(). func ( int) int { , := chunkPlan() return } // For runs body over [0, n) split into contiguous chunks, pulled from a // shared counter by a pool of at most Workers() goroutines. The 4x chunk // oversplit keeps dynamic load balancing (chunks are independent, so // pull order is free), while the goroutine count stays at the worker // count -- spawning one goroutine per chunk cost iterative algorithms // two allocations per chunk on every pass. func ( int, func(, int)) { , := chunkPlan() if <= 1 { if > 0 { (0, ) } return } := min(, max(Workers(), 1)) var atomic.Int64 var sync.WaitGroup .Add() for range { go func() { defer .Done() for { := int(.Add(1)) - 1 if >= { return } (*, min((+1)*, )) } }() } .Wait() } // ForWorker is For with a worker identity: body additionally receives the // POOL WORKER's stable index in [0, min(Workers(), Chunks(n))) so kernels // can keep per-worker scratch (the Go stand-in for thread-locals) in a // pre-sized slice -- size it by Workers(). Within a call, one index is // owned by exactly one goroutine, which invokes body sequentially for // each chunk it pulls, so scratch[worker] is safe to reuse both across a // call's chunks and across successive calls. func ( int, func(, , int)) { , := chunkPlan() if <= 1 { if > 0 { (0, 0, ) } return } := min(, max(Workers(), 1)) var atomic.Int64 var sync.WaitGroup .Add() for := range { go func() { defer .Done() for { := int(.Add(1)) - 1 if >= { return } (, *, min((+1)*, )) } }() } .Wait() } // Fold is the rayon fold/reduce shape over [0, n): one accumulator per // WORKER (never per chunk or element), each worker folding one contiguous // index range in ascending order, then merged with reduce in ascending // worker order -- so an order-sensitive associative reduce (a concat) // still sees the fully in-order sequence, exactly like the former // one-accumulator-per-chunk form. A heavy accumulator (a count slab, a // map) is built once per worker instead of once per 4x-oversplit chunk; // the oversplit's load-balancing headroom is deliberately traded away // here because a fold's per-index cost is near-uniform in the kernels // (For/ForWorker keep it). identity is called once per worker plus once // for an empty input. func [ any]( int, func() , func( , int) , func(, ) ) { if <= 0 { return () } := min(, max(Workers(), 1)) if <= 1 { := () for := 0; < ; ++ { = (, ) } return } := ( + - 1) / := make([], ) var sync.WaitGroup .Add() for := range { , := *, min((+1)*, ) go func() { defer .Done() := () for := ; < ; ++ { = (, ) } [] = }() } .Wait() := [0] for , := range [1:] { = (, ) } return } // FoldInto is Fold with caller-owned worker accumulators: accs supplies // one pre-seeded accumulator per worker (len(accs) caps the parallelism, // further capped by n), each worker folds its contiguous in-order index // range into its slot, and reduce merges in ascending worker order -- // the same in-order contract as Fold. The returned value aliases accs[0] // for reference-typed T, and workers write accs[w] = fold(...), so the // caller must reset EVERY accumulator before the next call. Reuse across // calls is the point: a map keeps its buckets through clear(), a slab // keeps its backing, so a warm call allocates nothing for accumulator // state. func [ any]( [], int, func( , int) , func(, ) ) { if <= 0 || len() == 0 { var if len() > 0 { return [0] } return } := min(, len()) if <= 1 { := [0] for := 0; < ; ++ { = (, ) } [0] = return } := ( + - 1) / var sync.WaitGroup .Add() for := range { , := *, min((+1)*, ) go func() { defer .Done() := [] for := ; < ; ++ { = (, ) } [] = }() } .Wait() := [0] for , := range [1:] { = (, ) } [0] = return } // Join runs the given closures concurrently and blocks until all complete -- // the finalize-style N-way join. func ( ...func()) { if len() == 1 { [0]() return } var sync.WaitGroup for , := range { .Add(1) go func() { defer .Done() () }() } .Wait() }