Performance and Memory
Every millisecond the rest of your stack is borrowing
dtype, contiguous layouts, views vs copies, and the BLAS-backed operations decide whether a heavy computation takes seconds or stalls. Tuning numpy is tuning the whole data stack.
What you will learn
- Classify dtype and memory costs
- Convert hot loops to vectorized or numexpr forms
- Profile and shrink allocations that dominate runtime
What is this?
NumPy performance lives in its C arrays: choosing the right dtype sets memory; contiguous layouts keep cache-friendly data; operations composition avoids temporaries. numexpr accelerates chained expressions of large arrays; np.save/np.load or memmap handle persistence. Profiling (time, memory) finds the bottleneck before you optimize.
Why do you need it?
- Dtype choice alone moves memory 8x (float64→float32) with identical results at report precision.
- Chained temporaries (a + b)*c allocate intermediates you never needed.
- Cache misses dominate big-array performance more than algorithm fancy-ness.
- Most 'slow pandas' stories are numpy-in-the-box problems.
Mental model
A freight depot: crate size, packing order, and road width
numpy performance
Smaller crates (dtype) fit more per truck (memory); tightly packed, in-order crates (contiguous) unload fast (cache); one wide road is faster than three zigzag lanes (vectorized). Tuning the depot — not re-training the driver — is where the wins are.
How it works
- dtype downgrade: float32/int16 for storage when precision allows; money as cents ints.
- Contiguous C-order (default) beats strided/F-order for element-wise ops.
- Views (slices, reshape) avoid copies; explicit two-step ops create temporaries.
- numexpr evaluates expressions (a + b*c) with fused, multithreaded kernels.
- np.save binary storage beats CSV 10-100x serialize/reload.
- Time with time.perf_counter, memory with sys.getsizeof or memory-profiler.
The performance lever set.
Examples
Size the win before you code it
import numpy as np, timebig = np.random.default_rng(1).normal(size=(2000, 2000)).astype(np.float32)print(big.nbytes / 1e6, 'MB')t0 = time.perf_counter()out = (big * 2 + 1.0).sum()print('%.3fs %.0f' % (time.perf_counter() - t0, out)) import numexpr as net0 = time.perf_counter()out2 = ne.evaluate("sum(big * 2 + 1.0)")print('numexpr %.3fs %.0f' % (time.perf_counter() - t0, out2))float32 halves memory, and numexpr fuses the multiply-add-sum into fewer passes for large arrays.
In production
- A feature store stores float32 embeddings, trading tiny precision for big memory.
- A daily scoring job evaluates chained expressions with numexpr, dropping runtime 30%+.
- A pipeline persists intermediate arrays as .npy to avoid re-parsing huge CSVs.
Best practices
- Profile before optimizing: time and memory first, then dtype → layout → backend.
- Choose dtype at build time, not via astype after the fact.
- Keep trivial arrays small for a moment's clarity before scaling.
- Benchmark changes against a fixed seed and fixed workload.
Common mistakes
- Rounding everything to float64 because 'it's safe', paying 2-8x memory.
- Chaining ops that generate temporaries in a hot loop.
- Assuming a view shares memory and mutating the source by accident.
- Optimizing compile order before measuring whether dtype was the issue.
Debugging tips
- dtype OOM → step down bits after verifying value ranges.
- Element-wise op mysteriously slow → check strides (ascontiguousarray).
- Two processes fighting → memory true peak, not allocation sum; use memmap for huge arrays.
- If numexpr and numpy disagree, they shouldn't for the same expression — verify broadcasting alignment first.
Interview checkpoint
Quick quiz
Which change typically helps most on large numeric arrays?
Slicing a NumPy array returns a copy by default.