Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,20 +127,22 @@ is wrong on subnormals
(full tables in **[docs/perf.md](docs/perf.md)**):

- **Faster:** whole-array reductions (`Sum`/`Mean`/`Max` at 4 Mi: 2.3–4.5×),
row reductions (`SumAxis(1)` 2.2×, `MaxAxis(1)` 1.7×), `Exp` from 256 Ki
elements (2.4–5.6×), the `*Into` forms of `Add`/`Mul`/`Sqrt` (faster or at
row reductions (`SumAxis(1)` 2.2×, `MaxAxis(1)` 1.7×), float64 `Exp` at
every size (on one core 1.2–1.3×; in a `Workspace` on 16 cores 1.3–1.5×
up to 16 Ki and 7–17× from 256 Ki), the `*Into` forms of `Add`/`Mul`/`Sqrt` (faster or at
parity at every size), and `MatMul` against single-threaded OpenBLAS
(3.7–5.9× from 512²); in a `Workspace`, `x + y` and `sqrt(x*y + x)` match or
beat NumPy at every size measured; float64 `Log` on one core with AVX2
(1.55× at 16 Ki, 1.68× at 1 Mi).
- **At parity:** `SumAxis(0)`, `Inner` against 16-thread OpenBLAS.
- **Slower:** `MatMul` against 16-thread OpenBLAS at 512² and up (0.42× at
512², 0.51× at 1024², measured in one run with OpenBLAS; at 256² ours is
1.44× faster); `Dot` and mat·vec against NumPy's 16-thread BLAS (0.69× and 0.52× since v0.6.0's helper pool, measured in one run; 0.16× before); `Exp`
below 256 Ki elements (0.82×); the *allocating* forms of elementwise ops up
to 256 Ki elements, `Concatenate`/`Stack` and slice copies outside a
`Workspace` (0.2–0.5×: Go's allocator and garbage collector, which
`Workspace` largely removes).
1.44× faster); `Dot` and mat·vec against NumPy's 16-thread BLAS (0.69× and 0.52× since v0.6.0's helper pool, measured in one run; 0.16× before); the
*allocating* forms of elementwise ops up to 256 Ki elements (allocating
float64 `Exp` with 16 Ps is at parity there, 0.9–1.0×, although its kernel
is faster), `Concatenate`/`Stack` and slice copies outside a `Workspace`
(0.2–0.5×: Go's allocator and garbage collector, whose cost grows with
GOMAXPROCS and which `Workspace` largely removes).

On Apple silicon the GEMM reached parity with tuned BLAS at 1024² (OpenBLAS in
an arm64 VM, single-threaded vecLib on an M4 Max), and every product beats the
Expand Down
17 changes: 17 additions & 0 deletions benchmarks/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@ func BenchmarkExp(b *testing.B) {
}
}

// BenchmarkExpWS is BenchmarkExp in a Workspace reset every iteration: the
// kernel without the allocation of each result, whose cost grows with
// GOMAXPROCS (each garbage-collector cycle involves every P).
func BenchmarkExpWS(b *testing.B) {
for _, n := range elemSizes {
ws := nd.NewWorkspace()
x := ws.Use(vec(n))
b.Run(fmt.Sprintf("n=%d", n), func(b *testing.B) {
b.SetBytes(int64(n * 8))
for i := 0; i < b.N; i++ {
_ = x.Exp()
ws.Reset()
}
})
}
}

func BenchmarkSqrt(b *testing.B) {
for _, n := range elemSizes {
x := vec(n)
Expand Down
34 changes: 32 additions & 2 deletions docs/perf.md
Original file line number Diff line number Diff line change
Expand Up @@ -1634,8 +1634,38 @@ the constant block's layout is tested.
−47% and −49% against v0.35.0 (`abbench`, 8 interleaved rounds, p = 0.000),
both allocating, on `BenchmarkDTypeExpLog`'s data.

The float32 kernels for the other architectures are still to write; there
`Exp` and `Log` run the Go transcriptions.
Since v0.37.0 these kernels live in go-simd/floats (`floats.Exp`, `Log`,
`Float32Exp`, `Float32Log`, go-simd/floats#29), with NEON versions of the
float32 pair for arm64; ndarray calls them (`internal/kernels/explog.go`).
The NEON kernels are proven correctly rounded over all 2³² inputs on
darwin/arm64 but were not benchmarked: every arm64 host available was at
load 48–100. ppc64le, s390x, riscv64 and loong64 still run the Go
transcriptions.

#### float64 `Exp` on 16 cores: the allocation, not the kernel (2026-11-08)

The README listed float64 `Exp` below 256 Ki as slower than NumPy (0.82×),
measured with 16 cores. On one core it is faster at every size. With
GOMAXPROCS = 16 the same serial code (below `ParThreshold`, 32 Ki, nothing
is split) got slower than with 1: 89 µs against 73 at 16 Ki. The same holds
for `Add`, which goes from 14 to 23–26 µs, while `AddInto`, which allocates
nothing, costs 4.3 µs at both. Each result's allocation costs more with more
Ps: every garbage-collector cycle starts and stops work on all of them. In a
`Workspace` (`BenchmarkExpWS`) the difference disappears:

| float64 `Exp`, Zen 3, load ≤ 5 | 1 Ki | 16 Ki | 256 Ki | 4 Mi |
|---|---:|---:|---:|---:|
| NumPy 2.5.4, `np.exp(a)` (one thread) | 6.0 µs | 85.5 µs | 1.32 ms | 25.0 ms |
| ours, 1 P, allocating | 4.9 µs | 73 µs | 1.11 ms | 19.3 ms |
| ours, 16 P, allocating | 5.8–6.6 µs | 83–95 µs | 0.63–0.78 ms | 4.8–6.7 ms |
| ours, 16 P, `Workspace` | **4.1 µs** | **64 µs** | **0.18 ms** | **1.47 ms** |

`BenchmarkExp` and `BenchmarkExpWS` (`(i%97)·0.5`, the README's data), two
rounds each at `-cpu 1` and `-cpu 16` on cores 0–15; NumPy medians of nine
on one pinned core. In a `Workspace` on 16 cores, float64 `Exp` is 1.3–1.5×
NumPy up to 16 Ki and 7–17× from 256 Ki. Allocating, it is at parity up to
16 Ki, and at 4 Mi the allocation of each 32 MB result takes about 3.3 ms of
the 4.8.

## SIMD coverage

Expand Down
Loading