Exercises — Vectorization — avoiding Python loops, speed comparison
The single tool we lean on throughout is the time model from the parent Vectorization note:
The figure above is the mental picture for the whole page. They cross at a break-even . Left of the crossing, looping wins; right of it, vectorization pulls away. Keep this crossing in your head — half the exercises are just "which side of the crossing am I on?"
Level 1 — Recognition
Exercise 1.1 (L1)
Which of these two snippets is "vectorized" in the NumPy sense, and which one loops in Python? data is a np.ndarray.
# A
out = []
for x in data:
out.append(x * 2)
# B
out = data * 2Recall Solution 1.1
B is vectorized. In B, data * 2 hands the entire array to a single compiled C loop — the multiplication happens inside NumPy, not in the Python interpreter.
A loops in Python: the for runs one interpreter step per element, and each step unboxes a np.float64 object, checks its type, and appends to a Python list. That is exactly the "reading the manual once per brick" pattern the parent note warns about.
Exercise 1.2 (L1)
Match each operation to whether it is a good vectorization target: (a) summing numbers, (b) reading a file line by line and parsing, (c) applying x**2 + 1 to floats.
Recall Solution 1.2
- (a) Yes →
data.sum(); a pure reduction over a numeric array. - (b) No → file I/O and string parsing are not uniform numeric array ops; there is no contiguous
float64block to sweep. - (c) Yes →
data**2 + 1, two element-wise C loops. Rule of thumb: vectorization pays off when you have the same numeric operation applied to every element of a big packed array.
Level 2 — Application
Exercise 2.1 (L2)
Rewrite this ReLU (clip negatives to zero) without a Python loop, two different ways.
for i in range(len(a)):
if a[i] < 0:
a[i] = 0Recall Solution 2.1
Way 1 — boolean mask (in place):
a[a < 0] = 0a < 0 builds a boolean mask array in C (one True/False per element); indexing with it writes 0 only where the mask is True.
Trade-off to notice: the mask a < 0 is itself a full-length temporary array — one boolean per element, so it costs extra elements of memory and one extra sweep of memory traffic to build and read. It is cheap (a byte, sometimes a bit, per element rather than 8 bytes) but not free, and on bandwidth-bound work that extra pass matters. That is why fusing the comparison into the write (as a[a < 0] = 0 does) is preferred over materialising the mask, storing it in a variable, and reusing it repeatedly.
Way 2 — np.where (new array):
a = np.where(a < 0, 0, a)Reads "where the condition is true pick 0, else keep a", selecting per element inside C. Note this also builds the mask internally and allocates a new output array, so it moves more memory than the in-place mask version.
Both remove the Python-level if, which was the expensive part.
Exercise 2.2 (L2)
You have row (length 4) and col (length 3). Produce the matrix with no Python loops.
Recall Solution 2.2
M = row[:, None] + col[None, :] # shape (4, 3)row[:, None] reshapes row to a column of shape ; col[None, :] reshapes col to a row of shape . NumPy broadcasts them: the column is copied across 3 columns, the row across 4 rows, and they add element-wise in C — a full nested loop's worth of work with zero Python iteration.
Exercise 2.3 (L2)
Using the time model with ns, ns, ns, compute , , and the speedup for .
Recall Solution 2.3
- The setup cost is negligible here, so we sit essentially at the ceiling.
Level 3 — Analysis
Exercise 3.1 (L3)
Find the break-even where the loop and the vectorized version take equal time. Use ns, ns, ns.
Recall Solution 3.1
Set : WHAT we did: equated the two lines of the figure. WHY: the crossing point is exactly where neither method wins. Solve for : So for arrays smaller than about 10 elements the loop is actually faster — precisely the "don't vectorize tiny arrays" warning, now derived rather than asserted.
Exercise 3.2 (L3)
Building an array of elements with np.append in a loop copies, on average, elements per call. Compare the total copy work against a list-then-convert approach.
Recall Solution 3.2
np.append loop: total copies . For that is
This is the hallmark behaviour (Big-O Complexity).
List then np.array: the list append is amortized and the final conversion copies each element once → copies, i.e. .
Ratio less copying. The "obvious" array-growing loop is thousands of times more work.
Exercise 3.3 (L3)
For a large vectorized a + b, the parent note says you observe ~10–30 ms even though the arithmetic model predicts far less. Which physical limit is biting, and how does it fit the time model?
Recall Solution 3.3
Memory bandwidth (Memory Layout and Cache). For float64 arrays, a + b must read two arrays and write one → bytes moved. At, say, ~15 GB/s effective bandwidth that is ms — matching the observation.
In the time model this means is no longer set by the add instruction but by the time to fetch/store one element from memory. The CPU waits on data, not on math — so making the arithmetic cheaper wouldn't help. This is the ceiling the parent note calls "memory-bandwidth bound".
Level 4 — Synthesis
Exercise 4.1 (L4)
A colleague wraps a scalar Python function with np.vectorize and expects a 100× speedup. Predict the actual speedup and justify with the time model.
Recall Solution 4.1
Prediction: essentially no speedup (~1×). np.vectorize is a convenience wrapper that still calls your Python function once per element — the loop remains in the interpreter. So stays at the slow Python value ; only changes slightly.
Fix: rewrite the function using ufuncs (so the per-element cost drops from to ), or compile it with Numba and Cython.
Exercise 4.2 (L4)
Design a benchmark to measure the break-even on your own machine, and describe what curve you expect to see.
Recall Solution 4.2
import numpy as np, timeit
for N in [1, 3, 10, 30, 100, 1000, 10000]:
a = np.random.rand(N)
t_loop = timeit.timeit(lambda: [x*2 for x in a], number=1000)
t_vec = timeit.timeit(lambda: a*2, number=1000)
print(N, t_loop/1000, t_vec/1000, t_loop/t_vec)Expected curve: the speedup column starts below 1 for tiny (loop wins because dominates the vectorized denominator), crosses 1 near the break-even from Exercise 3.1, then rises toward the ratio and finally flattens at large when memory bandwidth caps . Plotting speedup vs N on a log- axis reproduces the crossing in the page figure.
Level 5 — Mastery
Exercise 5.1 (L5)
Two designs process elements. Design A: one vectorized call, ns, ns. Design B: split the work into 1000 tiny vectorized calls of 1000 elements each (still ns, ns per call). Which is faster and by how much?
Recall Solution 5.1
Design A: Design B: each call handles elements: . With 1000 calls: A is faster, by Lesson: fragmenting a vectorized job re-pays on every chunk. Keep arrays whole and calls few — chunk only when memory forces you to.
Exercise 5.2 (L5)
You must compute, for arrays x, y of length , the value . Give a fully vectorized one-liner, count how many full-length temporaries it creates, then give a lower-memory version and estimate the bytes saved (float64).
Recall Solution 5.2
Straightforward one-liner:
total = (x**2 + 3*y).sum()Temporaries of length : x**2 (1), 3*y (2), and their sum x**2 + 3*y (3) → 3 temporaries before the reduction collapses it to a scalar.
Each temporary is bytes MB. Three of them MB of transient allocation.
Lower-memory version using fused reductions:
total = np.dot(x, x) + 3.0 * y.sum() # (sum of x_i^2) + 3*(sum y_i)Here and — algebra lets each reduction stream through without building a full-length temporary. Temporaries: 0 full-length arrays. Bytes saved: MB of temporary array memory (3 × 80 MB) eliminated, and far less traffic across the memory bus — directly attacking the bandwidth bottleneck from Exercise 3.3.
Connections
- Vectorization — avoiding Python loops, speed comparison (index 5.4.5) — the parent this drills.
- NumPy Arrays vs Python Lists — why the packed memory makes small.
- Big-O Complexity — the of
np.append. - Broadcasting Rules — the loop-free matrix build in 2.2.
- Universal Functions (ufuncs) — the real fix for
np.vectorize. - Memory Layout and Cache / Numba and Cython — the bandwidth ceiling and how to beat it.