Quant Memo
Foundational

Vectorization vs Loops

Vectorization replaces an explicit element-by-element Python loop with one call into compiled, bulk array code, which is usually tens to hundreds of times faster for the same computation.

Prerequisites: NumPy Arrays and Dtypes

A for loop over a million rows in Python, computing something as simple as a rolling return, can take seconds. The identical computation expressed as one NumPy expression over the same million rows can take milliseconds. Nothing about the math changed — the same multiplications and additions happen either way. What changed is who's doing the work and how much overhead sits around each individual operation.

The idea: batch the overhead, not just the math

A Python for loop asks the interpreter to do real work on every single iteration that has nothing to do with your actual arithmetic: look up what + means for these particular objects (Python re-checks this every time, since any object could redefine it), check types, manage reference counts, unpack values from Python objects into raw numbers and back. That overhead is roughly constant per iteration — tens to low hundreds of nanoseconds — but it's paid once per element, every element, with no way to amortize it.

Vectorization means handing the entire array to a single function call written in compiled C, which does the type-checking and dispatch decision exactly once for the whole array, then runs a tight, predictable loop over raw memory with none of Python's per-element bookkeeping. The CPU also benefits mechanically: because the array is one contiguous block (NumPy Arrays and Dtypes), the processor can prefetch upcoming elements into cache before they're needed and, on top of that, use SIMD (single instruction, multiple data) instructions that apply one arithmetic operation to several numbers in a single CPU cycle. A Python loop, jumping between Python objects, gets none of that.

Worked example: computing 1 million returns

Suppose prices is an array of 1,000,001 daily prices and you want simple returns, ri=(pipi1)/pi1r_i = (p_i - p_{i-1}) / p_{i-1}.

Loop version:

returns = [0.0] * (len(prices) - 1)
for i in range(1, len(prices)):
    returns[i - 1] = (prices[i] - prices[i - 1]) / prices[i - 1]

Each of the 1,000,000 iterations does: a bounds-checked list index (twice), a Python-level subtraction (allocates a new Python float object), a Python-level division (allocates another), and a list write. At roughly 100-150 nanoseconds of pure interpreter overhead per arithmetic op on top of the actual work, and several ops per iteration, this routinely lands around 150-300 milliseconds for a million elements on ordinary hardware.

Vectorized version:

import numpy as np
p = np.asarray(prices)
returns = (p[1:] - p[:-1]) / p[:-1]

Trace what happens here. p[1:] and p[:-1] are views — they don't copy data, they're just a different starting offset and length over the same underlying buffer, created in constant time regardless of array size. The subtraction and division each become a single call into NumPy's compiled loop, which walks the raw memory once per operation with no Python object creation per element. On the same million-element array, this typically runs in 2-5 milliseconds — roughly a 50-100x speedup for identical output, because the number of Python-level operations dropped from millions to two.

import numpy as np, timeit

p = np.random.uniform(90, 110, 1_000_001)

def loop_version(p):
    out = [0.0] * (len(p) - 1)
    for i in range(1, len(p)):
        out[i - 1] = (p[i] - p[i - 1]) / p[i - 1]
    return out

def vec_version(p):
    return (p[1:] - p[:-1]) / p[:-1]

timeit.timeit(lambda: loop_version(p), number=1)   # ~0.20s
timeit.timeit(lambda: vec_version(p), number=1)    # ~0.003s
loop ~200 ms vectorized ~3 ms runtime for 1,000,000 elements
Same arithmetic, same output — the vectorized bar is barely visible next to the loop because most of the loop's time is Python overhead, not the actual math.

Where vectorization stops helping

The gain comes from batching simple, uniform operations. It shrinks or vanishes when each element genuinely needs a different code path — an if branching differently per element, a computation that depends on a running state that can't be expressed as an array operation (a true sequential recursion, like an EWMA update that depends on its own previous output in a way NumPy doesn't have a built-in for), or calling into Python objects per element anyway (e.g. .apply() with a Python function is barely better than a loop, because it still calls back into the interpreter once per row).

Vectorization doesn't change the arithmetic — it changes who pays the per-element overhead. A Python loop pays interpreter bookkeeping costs once per element; a vectorized call pays them once for the whole array and then runs a tight compiled loop over contiguous memory, which is why the same computation can be 50-100x faster with no change in the numbers it produces.

Where this shows up

Interviewers ask "why is this backtest slow" specifically to see if a candidate reaches for vectorization before reaching for a faster language — rewriting a for loop over a NumPy array as array operations is often the single highest-leverage fix available. In production, vectorization is the default assumption for any research or backtesting codebase working with historical price panels; the moments it breaks down — genuinely path-dependent state, per-row branching logic, streaming tick-by-tick updates — are exactly the moments a team reaches instead for compiled extensions, Numba, or a rewrite in C++ (C++ for Low-Latency Trading).

Vectorized code still allocates new arrays for every intermediate step (p[1:] - p[:-1] builds one temporary array before dividing), so a long chain of vectorized operations can spend more time on memory allocation and cache misses than the raw arithmetic — the fix, when profiling shows this, is fusing operations or using in-place operators (+=), not going back to a loop.

Related concepts

Practice in interviews

Further reading

  • Harris et al., Array Programming with NumPy, Nature 585 (2020)
  • VanderPlas, Python Data Science Handbook (ch. 2)
ShareTwitterLinkedIn