Skip to content

Instantly share code, notes, and snippets.

@zonca
Created August 7, 2026 18:16
Show Gist options
  • Select an option

  • Save zonca/d2e39b1933dbb2a4db6bd4d63970b78c to your computer and use it in GitHub Desktop.

Select an option

Save zonca/d2e39b1933dbb2a4db6bd4d63970b78c to your computer and use it in GitHub Desktop.
numpy vs numba performance benchmark

numpy vs numba performance benchmark

A simple benchmark comparing numpy (vectorized) and numba (JIT-compiled parallel loop) for a sum-of-squares computation over a large array.

Performance

On a representative machine the JIT-compiled numba version is roughly 12–15x faster than the vectorized numpy version:

implementation time
numpy ~145 ms
numba ~10 ms
speedup ~15x

Exact numbers vary by hardware, array size, and numba thread settings.

How it works

benchmark.py generates 20 million random values, computes the sum of squares both ways, verifies both results agree, and reports wall-clock timings.

  • numpy: (x * x).sum() — vectorized, operates on the whole array.
  • numba: an @njit(parallel=True) function using prange to distribute the loop across CPU threads.

The numba implementation is warmed up before timing so the measurement excludes JIT compilation overhead.

Run it

uv venv
uv pip install numpy numba
uv run python benchmark.py

Expected output:

numpy:  146.5 ms
numba:  9.7 ms
speedup: 15.2x
"""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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment