This page is a drill . The parent note built the theory: a Python loop pays a big per-element overhead τ py , vectorized code pays a tiny τ vec , and the model is
T = c setup + N τ per-element , Speedup = c setup + N τ vec N τ py .
Here we hit every case that model can throw at you — big N , tiny N , the exact break-even point, the O ( N 2 ) trap, memory-bound reality, and a word problem.
Definition Symbols used on this page (all in nanoseconds unless said otherwise)
N ::: number of elements in the array (a plain count, like "a million numbers").
τ py ::: time the Python loop spends per element (boxing + type-check + dispatch). We use 100 ns.
τ vec ::: time the vectorized C loop spends per element (one raw CPU op). We use 1 ns.
c setup ::: the fixed cost paid once to enter the C routine (function call, shape checks). We use 1000 ns.
1 ns = 1 0 − 9 s, 1 ms = 1 0 − 3 s. So 1 0 6 ns = 1 ms.
Every problem about vectorization lives in one of these cells. The examples below are labelled with the cell (C1…C8) they cover.
Cell
Case class
What changes
Who wins?
C1
Large N (limit)
N → ∞ , setup negligible
Vectorized, ≈ τ py / τ vec
C2
Tiny N (degenerate)
N so small c setup dominates
Loop can win
C3
Break-even N
the exact N where times are equal
Tie — solve for it
C4
N = 0 / N = 1 (edge)
empty or single element
Pure setup / almost pure setup
C5
O ( N 2 ) trap
np.append in a loop
Neither — it's quadratic
C6
Memory-bound ceiling
arithmetic is free, data movement isn't
Speedup capped below τ py / τ vec
C7
Word problem
real timing from a benchmark
Forecast then check
C8
Exam twist
np.vectorize mislabelled as fast
It's a loop in disguise
C1. You process N = 1 0 7 floats. With τ py = 100 , τ vec = 1 , c setup = 1000 (all ns), find the speedup.
Forecast: N is huge — guess the speedup lands near the ceiling τ py / τ vec = 100 . Slightly under?
Loop time T py = N τ py = 1 0 7 × 100 = 1 0 9 ns = 1.0 s.
Why this step? The loop has no meaningful setup term — its whole cost is N slow ops.
Vector time T vec = c setup + N τ vec = 1000 + 1 0 7 × 1 = 10 , 001 , 000 ns ≈ 10.001 ms.
Why this step? Setup is 1000 ns; the real work is 1 0 7 ns. Setup is 0.01% — negligible, exactly as the limit predicts.
Speedup = T py / T vec = 1 0 9 /10 , 001 , 000 ≈ 99.99 .
Why this step? Dividing the two totals gives how many times faster.
Verify: 99.99 < 100 and the gap is tiny — the setup shaves a hair off the ceiling. Units: ns/ns is dimensionless ✅. As N → ∞ this creeps up to exactly 100 .
C2. Now N = 5 elements. Same constants. Does vectorizing win?
Forecast: With only 5 elements the fixed 1000 ns setup is bigger than any real work. Guess: loop wins .
Loop time T py = 5 × 100 = 500 ns.
Why? Five slow iterations, nothing else.
Vector time T vec = 1000 + 5 × 1 = 1005 ns.
Why? You pay the full 1000 ns entry fee just to touch 5 numbers.
Speedup = 500/1005 ≈ 0.498 .
Why? Below 1.0 means vectorized is slower — here about half the speed.
Verify: 0.498 < 1 → the loop is ≈ 2 × faster. This is the "don't vectorize a 5-element array" warning made concrete ✅.
C3. For what N do the loop and the vector op take equal time?
Forecast: Somewhere between the C2 loss and C1 win. Guess: a few dozen elements.
Set them equal: N τ py = c setup + N τ vec .
Why this step? "Break-even" literally means the two times match.
Collect N : N ( τ py − τ vec ) = c setup .
Why? Move both N -terms to one side so we can isolate N .
Solve: N ⋆ = τ py − τ vec c setup = 100 − 1 1000 = 99 1000 ≈ 10.10 .
Why? Divide. Because N must be a whole count, at N = 11 vectorization starts to win, at N ≤ 10 the loop wins.
Verify at N = 11 : loop = 1100 ns; vector = 1000 + 11 = 1011 ns. Vector is now faster ✅. At N = 10 : loop = 1000 ; vector = 1010 → loop faster ✅. The crossover sits right between, exactly as 10.10 says.
C4. What are the times for an empty array (N = 0 ) and a single element (N = 1 )?
Forecast: Empty → the loop does nothing, but the vector op still pays setup. Single → almost pure setup for the vector op.
N = 0 , loop: 0 × 100 = 0 ns. Why? Zero iterations = zero work.
N = 0 , vector: 1000 + 0 = 1000 ns. Why? You still enter the C routine; the entry fee is unavoidable.
N = 1 , loop: 1 × 100 = 100 ns. N = 1 , vector: 1000 + 1 = 1001 ns.
Why? One element cannot amortize a 1000 ns setup — the loop is ≈ 10 × faster here.
Verify: For N = 0 the loop "wins" 0 vs 1000 ns — vectorizing nothing still costs the call. Speedup = 0/1000 = 0 . Sanity: an empty operation should never be faster to vectorize ✅.
C5. You build an array by np.append-ing one element at a time, N times. Each append copies all elements present so far. How many total element-copies for N = 1000 , and how does that scale?
Forecast: Each append copies a growing array → sounds like 1 + 2 + 3 + … , the triangular number. Guess ≈ N 2 /2 .
Cost of the k -th append: it copies the k − 1 existing elements (then adds one), so ≈ k copies.
Why this step? np.append reallocates a fresh block and copies the old contents every call — see the parent mistake box.
Total copies = ∑ k = 1 N k = 2 N ( N + 1 ) .
Why? Sum the per-call costs. For N = 1000 : 2 1000 ⋅ 1001 = 500 , 500 .
Scaling: 2 N ( N + 1 ) ∼ 2 N 2 , i.e. O ( N 2 ) — see Big-O Complexity .
Why? The N 2 term dominates; this is worse than a plain Python list append which is O ( N ) total.
Verify: 500 , 500 ≈ 2 1 ( 1000 ) 2 = 500 , 000 ✅. Fix: build a Python list then np.array(lst) once — a single O ( N ) pass.
C6. Adding two big arrays should be 100 × faster by C1. In practice you measure only ≈ 30 × . Explain the gap using a memory model: reading/writing one 8 -byte float costs τ mem = 3 ns, while the add costs τ vec = 1 ns.
Forecast: If moving data costs more than the arithmetic, the CPU waits on memory, not on adds. Guess: the real per-element cost is dominated by τ mem , capping the speedup.
True vector per-element cost = max ( τ vec , τ mem ) = max ( 1 , 3 ) = 3 ns.
Why this step? For a+b you must read a[i], read b[i], write out[i] — the pipeline stalls on memory bandwidth, not arithmetic. See Memory Layout and Cache .
Realistic speedup = τ mem τ py = 3 100 ≈ 33.3 .
Why? Replace the idealized τ vec with the actual bottleneck.
Interpretation: the 100 × ceiling assumed arithmetic was the bottleneck; when data movement dominates, the real ceiling drops to ≈ 33 × .
Verify: 33.3 < 100 ✅ and matches the measured ≈ 30 × order of magnitude. This is why the parent said large vectorized ops are "memory-bandwidth bound."
C7. A colleague's loop over N = 2 × 1 0 6 floats runs in 0.30 s . You'll vectorize it. Using the memory-bound cost τ mem = 3 ns/elem plus c setup = 1000 ns, forecast the vectorized time and the speedup.
Forecast: 2 × 1 0 6 elements at a few ns each → a handful of milliseconds. Big win.
Vector time T vec = c setup + N τ mem = 1000 + 2 × 1 0 6 × 3 = 6 , 001 , 000 ns ≈ 6.001 ms.
Why this step? Use the realistic per-element cost from C6, since a plain a+b is memory-bound.
Speedup = 6.001 × 1 0 − 3 s 0.30 s ≈ 49.99 .
Why? Divide the measured loop time by the forecast vector time. Convert everything to seconds first: 6.001 ms = 0.006001 s.
Report: "Expect roughly 50× faster , landing near 6 ms."
Verify: 0.30/0.006001 ≈ 50.0 ✅. Units: s/s dimensionless. The forecast sits between the C6 ceiling (33 × for pure add) and the ideal 100 × — sensible because the loop here was already relatively slow at 150 ns/elem.
C8. An exam claims: "Wrapping f with np.vectorize(f) gives the 100 × speedup." If np.vectorize still runs f once per element in Python at τ py = 100 ns (plus a small c setup = 1000 ns), compute its real speedup over a hand-written loop for N = 1 0 6 . True or false?
Forecast: The name lies — if it's still a Python-level loop, the per-element cost is τ py , so no real speedup. Guess: ≈ 1 × , claim false .
Hand loop time = N τ py = 1 0 6 × 100 = 1 0 8 ns.
Why? Baseline: the straightforward loop.
np.vectorize time = c setup + N τ py = 1000 + 1 0 8 = 100 , 001 , 000 ns.
Why this step? np.vectorize does not compile f; it merely loops in Python — so it also pays τ py per element (see Universal Functions (ufuncs) for what real speed needs).
Speedup = 1 0 8 /100 , 001 , 000 ≈ 0.99999 .
Why? Essentially 1 × — the extra setup even makes it a whisker slower.
Verify: 0.99999 < 1 ✅ → the claim is FALSE . True speed needs a real ufunc or Numba and Cython compilation, whose per-element cost is τ vec , not τ py .
Recall Did we hit every cell of the matrix?
Which example covers the large-N limit? ::: C1 (≈ 99.99 × ).
Which shows vectorization losing ? ::: C2 (tiny N = 5 , speedup ≈ 0.498 ).
Where is the exact break-even N ? ::: C3 (N ⋆ = 1000/99 ≈ 10.10 ).
What happens at N = 0 ? ::: C4 — loop costs 0 , vector still pays 1000 ns setup.
Which example exposes the O ( N 2 ) trap? ::: C5 (np.append, 500 , 500 copies for N = 1000 ).
Why is real speedup below 100 × ? ::: C6 — memory-bound, ceiling ≈ 33 × .
The word-problem forecast? ::: C7 — ≈ 6 ms, ≈ 50 × .
The np.vectorize decoy result? ::: C8 — ≈ 1 × , claim false.
Mnemonic The one line to keep
"Big N flies, tiny N dies, memory caps the prize."
Parent — the speed model these examples exercise.
NumPy Arrays vs Python Lists — why the O ( N 2 ) append trap (C5) exists.
Big-O Complexity — the N 2 scaling in C5.
Universal Functions (ufuncs) / Numba and Cython — the real fix for C8.
Memory Layout and Cache — the bandwidth ceiling in C6.
Broadcasting Rules — vectorizing multi-dimensional cases.