Performance: Loops vs Vectorization
Measure the speed difference between Python loops and NumPy vectorized operations with time and timeit, and understand why NumPy wins.
"NumPy is fast" is easy to say. Let's measure it. We will double one million ball-by-ball run values, first with a Python list comprehension and then with a single NumPy expression, and time both.
Definition
Vectorization is the technique of expressing an operation on an entire array as a single expression (like
arr * 2) so that the element-by-element loop runs inside NumPy's pre-compiled C code instead of in the Python interpreter. Benchmarking is measuring how long code takes to run, using tools liketime.perf_counter()or thetimeitmodule.
- Pre-compiled C code — machine code that was compiled once when NumPy was built; it runs without Python's per-step overhead (type checks, object creation).
time.perf_counter()— returns a high-resolution clock reading in seconds; the difference between two readings is the elapsed time.timeit— runs a piece of code many times and reports the total time, which gives a more reliable measurement than a single run.
Syntax
python
import time
start = time.perf_counter()
# ... code to measure ...
elapsed = time.perf_counter() - start # seconds, as a float
import timeit
total_seconds = timeit.timeit(stmt, number=100) # stmt: a function or a code string| Function / parameter | Meaning | Default |
|---|---|---|
time.perf_counter() | High-resolution timer reading (seconds) | — |
timeit.timeit(stmt, ...) | Total time to run stmt number times | — |
stmt | The code to time — a zero-argument function (e.g. a lambda) or a string | "pass" |
number | How many times to run stmt | 1000000 |
%timeit expr | Jupyter "magic": auto-picks the number of runs and prints mean ± std | — |
Example — list comprehension vs vectorized, with time
python
import time
import numpy as np
n = 1_000_000
runs_list = list(range(n))
runs_arr = np.arange(n)
start = time.perf_counter()
doubled_list = [x * 2 for x in runs_list]
list_time = time.perf_counter() - start
start = time.perf_counter()
doubled_arr = runs_arr * 2
numpy_time = time.perf_counter() - start
print(f"List comprehension: {list_time:.4f} s")
print(f"NumPy vectorized: {numpy_time:.4f} s")
print(f"NumPy was about {list_time / numpy_time:.0f}x faster")
print("Same answer?", doubled_list == doubled_arr.tolist())Sample output from one run — your numbers will differ:
text
List comprehension: 0.0583 s
NumPy vectorized: 0.0033 s
NumPy was about 18x faster
Same answer? TrueHow it works: both versions produce exactly the same numbers (Same answer? True). The list version runs a million interpreted Python steps; the NumPy version makes one call, and C does the million steps. Exact timings depend on your CPU, Python version, what else is running, and even on the browser (the playground runs Python inside WebAssembly, which is slower overall) — but the vectorized version is consistently many times faster, and the gap grows with the size of the data.
Example — sum() on a list vs .sum() on an array, with timeit
python
import timeit
import numpy as np
runs_list = list(range(100_000))
runs_arr = np.arange(100_000)
t_list = timeit.timeit(lambda: sum(runs_list), number=100)
t_np = timeit.timeit(lambda: runs_arr.sum(), number=100)
print(f"sum(list) x100: {t_list:.4f} s")
print(f"arr.sum() x100: {t_np:.4f} s")Sample output — your numbers will differ:
text
sum(list) x100: 0.0354 s
arr.sum() x100: 0.0022 sHow it works: timeit repeats each statement 100 times, which smooths out random noise. Even Python's built-in sum() (already written in C) is slower, because it still has to unpack 100,000 separate Python integer objects; arr.sum() adds raw machine integers straight from contiguous memory.
Why vectorized code is faster
| Step for each element | Python loop | NumPy vectorized |
|---|---|---|
| Interpret the loop body | Yes, every iteration | No — compiled C loop |
| Check the element's type | Yes, every time | No — dtype is known once |
| Follow a pointer to the object | Yes | No — values are contiguous |
| Create a new Python object for the result | Yes | No — writes raw values |
| Use CPU cache / SIMD efficiently | Poorly | Very well |
The anti-pattern: looping over a NumPy array
Putting data in an array does not make your loop faster. This is still a slow Python loop — and it can even be slower than looping over a list, because each element is first wrapped into a NumPy scalar object:
python
import numpy as np
arr = np.arange(5)
total = 0
for x in arr:
total += x
print(total, type(total))
print(arr.sum())text
10 <class 'numpy.int64'>
10Same answer, but the loop gives up all of NumPy's speed. Rule of thumb: if you are writing a for loop over array elements, look for the NumPy function that does it for you.
Tip: For tiny arrays (a handful of elements), NumPy's per-call overhead can make it no faster than a list. NumPy shines on hundreds, thousands or millions of values — exactly the size of real data.
Common mistakes
- ❌ Timing a single run and trusting the number. ✅ Use
timeit(or%timeitin Jupyter) to repeat and average. - ❌ Including array creation inside the timed block and blaming NumPy. ✅ Create data first, then time only the operation.
- ❌ Writing
for x in arr:loops "because it's NumPy now". ✅ Use vectorized expressions and functions (arr * 2,arr.sum(),arr[arr > 50]). - ❌ Quoting exact speedups like "NumPy is always 50× faster". ✅ The speedup depends on the operation, the data size and the machine; say "typically 10–100× faster for large arrays".
Interview tip
Q: Is NumPy always faster than a Python list? A: No. For very small collections, NumPy's function-call overhead can dominate, and looping element-by-element over an array is slow. NumPy is dramatically faster when you use vectorized operations on large arrays, because the work runs in compiled C over contiguous, typed memory.
Key points to remember
- Vectorization moves the per-element loop from the Python interpreter into NumPy's compiled C code.
- Use time.perf_counter() for a quick measurement and timeit (or %timeit) for reliable, repeated measurements.
- Vectorized NumPy is typically many times (often 10–100×) faster on large arrays; exact timings vary by machine.
- Speed comes from contiguous typed memory, no per-element type checks, no Python object creation, and CPU cache/SIMD use.
- Looping over a NumPy array with a Python for loop throws the speed advantage away.
- For tiny arrays NumPy may not be faster — its advantage grows with data size.
Try it yourself
Benchmark: list comprehension vs vectorized
timeit: sum() vs .sum()
Practice: time a 10% bonus on a season of balls
Revision Flashcards
Tap a card to flip it and see the answer.
