From 136e6229d4fdc411b71d288c53064d193fc18fe5 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Wed, 7 Oct 2026 17:30:30 +0200 Subject: [PATCH] docs: v0.6.0 (helper pool): performance mirror, Goroutines note on the home page Co-Authored-By: Claude Opus 5.5 --- docs/index.md | 9 +++++++++ docs/performance.md | 39 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index e1f6767..2ea3c1e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -34,6 +34,15 @@ m.Transpose().Shape() // [3 2] | Linear algebra | `MatMul`, `Dot`, `Inner`, `Outer` | | Memory reuse | `NewWorkspace`, `Use`, `Reset`, `Detach` | +## Goroutines (v0.6.0) + +Operations on large arrays are spread over `GOMAXPROCS` goroutines. After the +first one, the package keeps up to `GOMAXPROCS-1` helper goroutines alive. They +poll for the next operation for 200 µs, then block, so back-to-back operations +do not pay to wake threads. On 8 POWER9 cores that took a 2^20-element dot from +625 to 58 µs. The helpers are never stopped: a goroutine-leak check that runs +after ndarray operations will list them. See [Performance](performance.md). + ## Untrusted input (v0.3.0) A shape the library cannot represent is an error wrapping `ErrShapeMismatch`, diff --git a/docs/performance.md b/docs/performance.md index 07d1fc9..e4a7cb3 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -4,7 +4,7 @@ Honest, reproducible head-to-head of `go-ndarray/ndarray` against **NumPy 2.2.4** on identical hardware. "On n'a pas le droit de se tromper": every number here is measured, every win is real, and where NumPy still leads it says so. -> **Where it stands at v0.5.2.** This page grew round by round: the sections +> **Where it stands at v0.6.0.** This page grew round by round: the sections > up to *Where go-ndarray still loses* were measured on an arm64 Tart VM > (4 vCPU, NumPy 2.2.4, OpenBLAS 0.3.29); the dated sections after it on an > AMD Zen 3, 16 cores, against **NumPy 2.5.3 + OpenBLAS 0.3.34**, except the @@ -594,8 +594,41 @@ in scheduling instead: with the caller working, 2.1 of 4 CPUs were busy against 3.4 for the original. Its goroutines wait in the run queue of the caller's processor until another one is woken to steal them. With the caller blocking, the 4-worker case recovers, but the 8-worker gains go with it. The -code is on the `experiment/parallel-for-scheduling` branch. Not yet solved: -a scheduler that keeps both. +code is on the `experiment/parallel-for-scheduling` branch. + +**Solved in v0.6.0 with a persistent helper pool.** The cost being paid was +waking threads, so the fix is to stop putting them to sleep between back-to-back +operations, as OpenBLAS and OpenMP do. Up to `GOMAXPROCS-1` helper goroutines +are started once. After an operation they poll for the next one for 200 µs, +then park. The work keeps owned runs with stealing (row two of the table +above). Reductions now use four partials per worker: with one each, a helper +that saw the job late had its only block taken by the caller, who then summed +two in a row (Sum of 4 Mi on 4 cores: 2.8 of 4 CPUs busy). On POWER9 bare metal, +each case in a **fresh process**, 6 interleaved rounds, against v0.5.2 +(median): + +| | 2 workers | 4 workers | 8 workers | 8 workers, before → after | +|---|---|---|---|---| +| Dot 2^20 | 3.29× | 2.41× | **10.8×** | 625 → 58 µs | +| MatVec 1024² | 2.01× | 1.65× | 5.78× | 323 → 56 µs | +| SumAxis(1) | 2.06× | 1.80× | 7.09× | 310 → 44 µs | +| Sum 256 Ki | 2.33× | 3.23× | 5.60× | 61 → 11 µs | +| Max 256 Ki | 1.75× | 1.34× | 6.78× | 757 → 112 µs | +| AddInto 256 Ki | 2.22× | 1.91× | 5.57× | 102 → 18 µs | +| Exp 256 Ki | 1.18× | 1.49× | 1.97× | 1013 → 513 µs | +| Sum 4 Mi | 1.33× | 1.17× | 1.41× | 431 → 305 µs | +| Exp 4 Mi | 1.00× | 1.10× (p25 0.84×) | 1.29× | | +| BroadcastAdd | 0.95× | 1.01× | 1.67× | | +| Chain (Mul, Add, Sqrt) 4 Mi | | 0.91× | 1.04× | | +| MatMul 256², 512² (own driver) | | 1.00× | 0.96–0.99× | | + +A first combined run, with every benchmark in one process, also showed serial +operations speeding up (Sqrt of 1 Ki, 1.5–2.2×). Run alone, those are equal. +That gain belonged to whatever state the earlier benchmarks had left, not to +this change, so only fresh-process figures are reported here. The cost: after +an operation, the helpers spend up to 200 µs of CPU polling. They are never +stopped (see the README, *Goroutines*). Not yet measured on amd64 or on Apple +M, whose hosts were unreachable or loaded at the time. ## Log and Log10 (2026-10-04)