|
"""Benchmark comparing numpy and numba performance. |
|
|
|
This script times a sum-of-squares computation over a large array using two |
|
approaches: |
|
|
|
- numpy: vectorized expression ``(x * x).sum()``, which operates on the whole |
|
array using optimized C routines. |
|
- numba: a JIT-compiled parallel loop that iterates over the array with |
|
``prange``, compiling the Python function down to native machine code. |
|
|
|
The reference result from numpy is validated against numba's output to ensure |
|
the implementations agree, then the measured wall-clock times are reported |
|
along with the resulting speedup factor. |
|
|
|
Run with: |
|
|
|
uv run python benchmark.py |
|
""" |
|
|
|
import time |
|
|
|
import numpy as np |
|
from numba import njit, prange |
|
|
|
# Number of elements in the generated input array. Large enough for a |
|
# meaningful comparison, small enough to keep the benchmark well under 30 s. |
|
N = 20_000_000 |
|
|
|
|
|
@njit(parallel=True) |
|
def numba_sum_sq(x): |
|
"""Return the sum of squares of a 1-D array using numba. |
|
|
|
Decorated with ``@njit(parallel=True)`` so numba compiles this to native |
|
code and distributes the ``prange`` loop over multiple CPU threads. A |
|
parallel reduction over ``prange`` is used to accumulate the result. |
|
|
|
Parameters |
|
---------- |
|
x : numpy.ndarray |
|
A contiguous 1-D floating-point array. |
|
|
|
Returns |
|
------- |
|
float |
|
The sum of ``x[i] * x[i]`` for all elements of ``x``. |
|
""" |
|
total = 0.0 |
|
for i in prange(x.size): |
|
total += x[i] * x[i] |
|
return total |
|
|
|
|
|
def main(): |
|
"""Generate data, time both implementations, and print the results.""" |
|
# Reproducible random input. |
|
rng = np.random.default_rng(42) |
|
x = rng.standard_normal(N) |
|
|
|
# Warm up numba's JIT compiler on a tiny slice so the actual timing below |
|
# measures already-compiled code (excluding compilation overhead). |
|
numba_sum_sq(x[:1000]) |
|
|
|
# Warm up numpy caches / page in the small slice. |
|
x[:1000].sum() |
|
|
|
# Time the numpy vectorized implementation. |
|
t0 = time.perf_counter() |
|
np_result = (x * x).sum() |
|
t_np = time.perf_counter() - t0 |
|
|
|
# Time the numba implementation (already JIT-compiled from the warm-up). |
|
t0 = time.perf_counter() |
|
nb_result = numba_sum_sq(x) |
|
t_nb = time.perf_counter() - t0 |
|
|
|
# Sanity check: the two results must agree to within a tight relative |
|
# tolerance (they may differ only by floating-point summation order). |
|
assert abs(np_result - nb_result) < 1e-6 * abs(np_result), "results differ" |
|
|
|
# Report measured timings and the speedup factor. |
|
print(f"numpy: {t_np*1000:.1f} ms") |
|
print(f"numba: {t_nb*1000:.1f} ms") |
|
print(f"speedup: {t_np/t_nb:.1f}x") |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |