From f8aec40982d4b71ccf8fe10ee576642e56d50183 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Tue, 20 Oct 2026 16:00:14 +0200 Subject: [PATCH] Add ASV benchmarks for add and subtract, including complex in-place The ASV suite had no benchmark for add or subtract and no micro-benchmark for a complex dtype, so changes to those loops, such as the contiguous complex fast path in gh-278, did not show up in it. Add BenchAdd and BenchSubtract to micro/bench_micro.py, written like BenchArctan2 and BenchPower, for float32, float64, complex64 and complex128 at 10k, 100k and 1M elements. Add micro/bench_inplace.py with BenchAddInplace and BenchSubtractInplace for complex in-place calls (out=a, out=b and z += z) at 10k and 100k, where mkl_umath runs its own loops instead of oneMKL VM. --- benchmarks/README.md | 3 +- benchmarks/benchmarks/micro/bench_inplace.py | 54 ++++++++++++++++++++ benchmarks/benchmarks/micro/bench_micro.py | 47 +++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 benchmarks/benchmarks/micro/bench_inplace.py diff --git a/benchmarks/README.md b/benchmarks/README.md index 70c8e234..39a7d56a 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -8,7 +8,8 @@ The `npbench/` suite uses kernels from [npbench](https://github.com/spcl/npbench | File | Ufuncs | Dtypes | Sizes/Presets | |------|--------|--------|---------------| -| `micro/bench_micro.py` | 25 unary (`exp`, `log`, `sin`, `cos`, `sqrt`, `cbrt`, etc.) + `arctan2`, `power` | float32, float64 | 10k, 100k, 1M | +| `micro/bench_micro.py` | 25 unary (`exp`, `log`, `sin`, `cos`, `sqrt`, `cbrt`, etc.) + `arctan2`, `power`, `add`, `subtract` | float32, float64 (`add`, `subtract` also complex64, complex128) | 10k, 100k, 1M | +| `micro/bench_inplace.py` | `add`, `subtract` in place: `out=a`, `out=b`, `z += z` | complex64, complex128 | 10k, 100k | | `npbench/bench_softmax.py` | `exp`, `sum` (`max` runs on stock NumPy) | float32 | M (32x8x256x256), L (64x16x448x448) | | `npbench/bench_arc_distance.py` | `sin`, `cos`, `arctan2`, `sqrt` | float64 | M (1M), L (10M) | | `npbench/bench_go_fast.py` | `tanh` | float64 | M (6k x 6k), L (20k x 20k) | diff --git a/benchmarks/benchmarks/micro/bench_inplace.py b/benchmarks/benchmarks/micro/bench_inplace.py new file mode 100644 index 00000000..afe8d509 --- /dev/null +++ b/benchmarks/benchmarks/micro/bench_inplace.py @@ -0,0 +1,54 @@ +"""Micro-benchmarks for in-place complex add and subtract. + +Up to VML_ASM_THRESHOLD (100000 elements) mkl_umath runs its own C loops +instead of oneMKL VM, with separate loops for an output that is one of the +inputs: out=a, out=b and z += z (both inputs and the output are one array). + +Arrays are pre-allocated in setup() and reused across timing calls. +Patching is applied once at package import via benchmarks._patch_setup. +""" + +import numpy as np + +from .bench_micro import _uniform + + +class _Inplace: + params = ( + ["complex64", "complex128"], + [10_000, 100_000], + ["out=a", "out=b", "z += z"], + ) + param_names = ["dtype", "size", "mode"] + ufunc = "" + + def setup(self, dtype, size, mode): + self._func = getattr(np, self.ufunc) + rng = np.random.default_rng(42) + a = _uniform(rng, size, dtype) + b = _uniform(rng, size, dtype) + if mode == "out=a": + self.args, self.out = (a, b), a + elif mode == "out=b": + self.args, self.out = (a, b), b + else: + # repeated z += z doubles z and overflows to inf; zeros keep it + # finite, and add/subtract run equally fast on any finite value + z = np.zeros(size, dtype=dtype) + self.args, self.out = (z, z), z + self._func(*self.args, out=self.out) + + def time_inplace(self, dtype, size, mode): + self._func(*self.args, out=self.out) + + +class BenchAddInplace(_Inplace): + """In-place add""" + + ufunc = "add" + + +class BenchSubtractInplace(_Inplace): + """In-place subtract""" + + ufunc = "subtract" diff --git a/benchmarks/benchmarks/micro/bench_micro.py b/benchmarks/benchmarks/micro/bench_micro.py index 5b4b4ec5..130befd8 100644 --- a/benchmarks/benchmarks/micro/bench_micro.py +++ b/benchmarks/benchmarks/micro/bench_micro.py @@ -98,3 +98,50 @@ def setup(self, dtype, size): def time_power(self, dtype, size): np.power(self.base, self.exp) + + +def _uniform(rng, size, dtype): + """Uniform [-1, 1) values; complex parts drawn independently""" + dtype = np.dtype(dtype) + if dtype.kind == "c": + parts = rng.uniform(-1.0, 1.0, 2 * size) + return parts.astype(np.finfo(dtype).dtype).view(dtype) + return rng.uniform(-1.0, 1.0, size).astype(dtype) + + +class BenchAdd: + """Binary ufunc add (MKL v?Add above 100k elements)""" + + params = ( + ["float32", "float64", "complex64", "complex128"], + [10_000, 100_000, 1_000_000], + ) + param_names = ["dtype", "size"] + + def setup(self, dtype, size): + rng = np.random.default_rng(42) + self.a = _uniform(rng, size, dtype) + self.b = _uniform(rng, size, dtype) + np.add(self.a, self.b) + + def time_add(self, dtype, size): + np.add(self.a, self.b) + + +class BenchSubtract: + """Binary ufunc subtract (MKL v?Sub above 100k elements)""" + + params = ( + ["float32", "float64", "complex64", "complex128"], + [10_000, 100_000, 1_000_000], + ) + param_names = ["dtype", "size"] + + def setup(self, dtype, size): + rng = np.random.default_rng(42) + self.a = _uniform(rng, size, dtype) + self.b = _uniform(rng, size, dtype) + np.subtract(self.a, self.b) + + def time_subtract(self, dtype, size): + np.subtract(self.a, self.b)