Mojo Vectorization

Vectorization rewrites a loop so that each iteration processes multiple data elements simultaneously using SIMD hardware instructions. Instead of computing one result per loop cycle, vectorized code computes 4, 8, or 16 results per cycle. This is one of Mojo's most powerful performance tools.

Scalar vs Vectorized Loop

Scalar loop (one element at a time):
  for i in range(8):
      result[i] = a[i] + b[i]

  Cycle 1: result[0] = a[0] + b[0]
  Cycle 2: result[1] = a[1] + b[1]
  ...
  Cycle 8: result[7] = a[7] + b[7]
  Total: 8 CPU additions

Vectorized loop (8 elements at once with SIMD width=8):
  result[0:8] = a[0:8] + b[0:8]

  Cycle 1: ALL 8 additions happen simultaneously
  Total: 1 CPU instruction
  Speedup: ~8×

Manual Vectorization with SIMD

from memory import UnsafePointer

fn add_arrays_scalar(a: UnsafePointer[Float32],
                     b: UnsafePointer[Float32],
                     result: UnsafePointer[Float32],
                     n: Int):
    for i in range(n):
        result[i] = a[i] + b[i]

fn add_arrays_vector(a: UnsafePointer[Float32],
                     b: UnsafePointer[Float32],
                     result: UnsafePointer[Float32],
                     n: Int):
    alias width = 8   # process 8 floats at once
    var i = 0
    while i + width <= n:
        var va = SIMD[DType.float32, width].load(a + i)
        var vb = SIMD[DType.float32, width].load(b + i)
        (va + vb).store(result + i)
        i += width
    # Handle remaining elements
    while i < n:
        result[i] = a[i] + b[i]
        i += 1
Vectorized loop iteration diagram (width=4):

  i=0:   a[0,1,2,3] + b[0,1,2,3] → result[0,1,2,3]   ← 1 instruction
  i=4:   a[4,5,6,7] + b[4,5,6,7] → result[4,5,6,7]   ← 1 instruction
  i=8:   scalar fallback for any remaining elements

Using the vectorize Higher-Order Function

Mojo's standard library provides a vectorize function that handles the SIMD width selection and loop structure automatically.

from algorithm import vectorize
from memory import UnsafePointer

fn scale_array(data: UnsafePointer[Float32], n: Int, factor: Float32):
    @parameter
    fn scale_simd[width: Int](i: Int):
        var chunk = SIMD[DType.float32, width].load(data + i)
        (chunk * factor).store(data + i)

    vectorize[scale_simd, 8](n)

fn main():
    let n = 16
    var arr = UnsafePointer[Float32].alloc(n)
    for i in range(n):
        arr.init_pointee_copy(Float32(i + 1))

    scale_array(arr, n, 2.0)

    for i in range(n):
        print(arr[i], end=" ")   # 2 4 6 8 10 12 14 16 ...

    for i in range(n):
        (arr + i).destroy_pointee()
    arr.free()

Choosing SIMD Width

Width | Bits used | Typical hardware support
------|-----------|-----------------------------
  4   | 128 bits  | SSE (Intel), NEON (ARM)
  8   | 256 bits  | AVX2 (modern Intel/AMD)
  16  | 512 bits  | AVX-512 (server CPUs)

Query the hardware width at compile time:
  from sys.info import simdwidthof
  alias W = simdwidthof[DType.float32]()   # auto-detect

Vectorized Dot Product

from algorithm import vectorize
from memory import UnsafePointer

fn dot_product(a: UnsafePointer[Float32],
               b: UnsafePointer[Float32],
               n: Int) -> Float32:
    var accumulator = SIMD[DType.float32, 8].splat(0.0)

    @parameter
    fn multiply_add[width: Int](i: Int):
        var va = SIMD[DType.float32, width].load(a + i)
        var vb = SIMD[DType.float32, width].load(b + i)
        accumulator += va * vb

    vectorize[multiply_add, 8](n)
    return accumulator.reduce_add()
Diagram: Vectorized dot product (width=4, n=8)

  Iteration 1 (i=0):
    a[0,1,2,3] × b[0,1,2,3] → partial[0,1,2,3]
    accumulator += partial

  Iteration 2 (i=4):
    a[4,5,6,7] × b[4,5,6,7] → partial[4,5,6,7]
    accumulator += partial

  Final: accumulator.reduce_add() → scalar sum

Vectorization and Memory Alignment

SIMD loads are fastest when data sits at an aligned memory address — a multiple of the SIMD vector size in bytes. Misaligned loads still work but run slower on some hardware.

from memory import UnsafePointer

# alloc() returns aligned memory by default in Mojo
var arr = UnsafePointer[Float32].alloc(64)   # aligned for SIMD

Real-World Impact

Operation         | Scalar time | Vectorized time | Speedup
------------------|-------------|-----------------|--------
Add 1M floats     | 4 ms        | 0.5 ms          | ~8×
Scale image pixels| 12 ms       | 1.5 ms          | ~8×
Dot product (1M)  | 6 ms        | 0.4 ms          | ~15×
(Higher speedups when memory fits in CPU cache)

Key Takeaways

Vectorization processes multiple data elements per CPU instruction using SIMD. Manual vectorization loads chunks into SIMD variables, performs operations, and stores results. The vectorize higher-order function automates SIMD width selection and loop structure. Match your SIMD width to the target hardware — 8 float32 values at a time covers most modern CPUs. Aligned memory maximizes SIMD throughput. Vectorization delivers 4–16× speedups on numerical workloads and is one of the primary reasons Mojo dramatically outperforms Python.

Leave a Comment

Your email address will not be published. Required fields are marked *