|
"""Benchmark comparing the performance of numpy vs numba (just-in-time) execution. |
|
|
|
This script measures the wall-clock time of equivalent numeric operations |
|
implemented with plain numpy and with @njit-compiled numba functions, on a |
|
large 1-D float array. |
|
|
|
The goal is to illustrate where numba provides a speedup over numpy: |
|
* Operations numpy can vectorize natively (e.g. ``sum``) - moderate speedup. |
|
* Element-by-element Python-style loops that numpy cannot vectorize - large |
|
speedup, because the loop body is compiled to native code. |
|
""" |
|
|
|
import time |
|
|
|
import numpy as np |
|
from numba import njit, prange |
|
|
|
|
|
def numpy_sum(n): |
|
"""Sum a random array of ``n`` values using numpy's native ``sum``. |
|
|
|
Args: |
|
n (int): Number of elements in the array. |
|
|
|
Returns: |
|
float: Sum of the elements. |
|
""" |
|
a = np.random.rand(n) |
|
return a.sum() |
|
|
|
|
|
@njit |
|
def numba_sum(a): |
|
"""Sum an array with a compiled numba loop. |
|
|
|
Args: |
|
a (np.ndarray): Input 1-D float array. |
|
|
|
Returns: |
|
float: Sum of the elements. |
|
""" |
|
s = 0.0 |
|
for i in range(a.shape[0]): |
|
s += a[i] |
|
return s |
|
|
|
|
|
@njit(parallel=True) |
|
def numba_sum_parallel(a): |
|
"""Sum an array using numba's parallel ``prange`` loop. |
|
|
|
Args: |
|
a (np.ndarray): Input 1-D float array. |
|
|
|
Returns: |
|
float: Sum of the elements. |
|
""" |
|
s = 0.0 |
|
for i in prange(a.shape[0]): |
|
s += a[i] |
|
return s |
|
|
|
|
|
def numpy_loop(a): |
|
"""Apply ``x*2 + 1`` element-wise using a pure Python loop over numpy. |
|
|
|
This is deliberately NOT vectorized, so it is very slow. It represents |
|
code numpy cannot optimize and is the case where numba shines. |
|
|
|
Args: |
|
a (np.ndarray): Input 1-D float array. |
|
|
|
Returns: |
|
np.ndarray: Result of ``a[i] * 2 + 1`` for each element. |
|
""" |
|
out = np.empty_like(a) |
|
for i in range(a.shape[0]): |
|
out[i] = a[i] * 2.0 + 1.0 |
|
return out |
|
|
|
|
|
@njit |
|
def numba_loop(a): |
|
"""Apply ``x*2 + 1`` element-wise with a compiled numba loop. |
|
|
|
Benchmark comparison to :func:`numpy_loop`. |
|
|
|
Args: |
|
a (np.ndarray): Input 1-D float array. |
|
|
|
Returns: |
|
np.ndarray: Result of ``a[i] * 2 + 1`` for each element. |
|
""" |
|
out = np.empty_like(a) |
|
for i in range(a.shape[0]): |
|
out[i] = a[i] * 2.0 + 1.0 |
|
return out |
|
|
|
|
|
def bench(name, fn, *args, reps=5): |
|
"""Benchmark ``fn`` and print its best wall-clock time over ``reps`` runs. |
|
|
|
A single warm-up call runs first to skip lazy initialization (e.g. numba |
|
compilation or JIT for the first invocation). The best (minimum) time is |
|
reported to reduce noise from background system activity. |
|
|
|
Args: |
|
name (str): Label printed in the results table. |
|
fn (callable): Function to benchmark. |
|
*args: Positional arguments passed to ``fn``. |
|
reps (int, optional): Number of timed repetitions. Defaults to 5. |
|
|
|
Returns: |
|
float: Best measured wall-clock time in seconds. |
|
""" |
|
fn(*args) |
|
times = [] |
|
for _ in range(reps): |
|
t0 = time.perf_counter() |
|
fn(*args) |
|
times.append(time.perf_counter() - t0) |
|
best = min(times) |
|
print(f"{name:<20} {best * 1e3:>10.3f} ms") |
|
return best |
|
|
|
|
|
def main(): |
|
"""Run the full benchmark and print a speedup summary.""" |
|
n = 10_000_000 |
|
rng = np.random.default_rng(42) |
|
a = rng.random(n) |
|
|
|
print(f"Array size: {n}") |
|
print("-" * 40) |
|
|
|
# Warm up numba compilation so timing excludes JIT overhead. |
|
numba_sum(a) |
|
numba_sum_parallel(a) |
|
numba_loop(a) |
|
|
|
t_np = bench("numpy sum", numpy_sum, n) |
|
t_nb = bench("numba sum", numba_sum, a) |
|
t_nbp = bench("numba sum par", numba_sum_parallel, a) |
|
t_np_loop = bench("numpy loop", numpy_loop, a) |
|
t_nb_loop = bench("numba loop", numba_loop, a) |
|
|
|
print("-" * 40) |
|
print(f"numba sum speedup vs numpy: {t_np / t_nb:.2f}x") |
|
print(f"numba par sum speedup: {t_np / t_nbp:.2f}x") |
|
print(f"numba loop vs numpy loop: {t_np_loop / t_nb_loop:.2f}x") |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |