Knowing each stage tells you where an error comes from and how to verify that code was vectorized.
When a dispatcher sees a new argument type signature it runs a compilation pipeline:
- Bytecode analysis. Numba disassembles the function's CPython bytecode and builds a control flow graph. That is why Numba support depends on the Python version: each new CPython release changes the bytecode.
- Numba IR. The bytecode is translated into a register-based intermediate representation, then rewritten by passes (inlining, constant folding, and for
parallel=True, array-expression fusion and loop parallelisation). - Type inference. Starting from the argument types, Numba propagates types through the IR, resolving each operation and call to a typed implementation. Failure here is your
TypingError. - Lowering. Each typed operation is lowered to LLVM IR, calling into Numba's runtime (NRT) for memory management of arrays: arrays are reference-counted, and their buffers live in NRT-managed memory.
- LLVM optimises (inlining, loop vectorization with SIMD, constant propagation) and generates machine code for the host CPU, which is linked and wrapped for calls from Python.
You can inspect each stage of a compiled function: inspect_types() for typed IR, inspect_llvm() and inspect_asm() for the LLVM IR and assembly per signature. Searching the assembly for vector instructions like vaddpd (AVX) confirms SIMD vectorization.
The dispatcher boxes and unboxes arguments at the boundary, which costs a microsecond or so per call. Call compiled functions from other compiled functions to avoid it, and keep work per call large.
Example
import numpy as np
from numba import njit
@njit(fastmath=True)
def dot(a, b):
s = 0.0
for i in range(a.size):
s += a[i] * b[i]
return s
x = np.ones(1000)
print(dot(x, x)) # 1000.0
sig = dot.signatures[0]
llvm_ir = dot.inspect_llvm(sig)
asm = dot.inspect_asm(sig)
print("fmul" in llvm_ir) # True: multiply in LLVM IR
print(any(op in asm for op in ("vfmadd", "vmulpd", "mulpd", "fmla")))
# True on most CPUs: SIMD instructions in the generated assemblyRemember
- Pipeline: bytecode, Numba IR, type inference, lowering to LLVM IR, machine code
- inspect_types, inspect_llvm and inspect_asm show each stage
- Each call from Python pays a boxing cost; keep calls coarse-grained
Watch out for
- Calling a tiny compiled function millions of times from Python, where the call overhead dominates.