Chapter 05

SIMD 与向量化

掌握 Mojo 的核心性能武器:直接操控 CPU 向量寄存器,实现数十倍吞吐量提升

SIMD 原理深度解析

现代 CPU 的向量计算能力

现代 x86-64 处理器包含专用的向量处理单元(SIMD 执行单元),可以在一个时钟周期内对多个数据元素执行相同操作。这是 NumPy/PyTorch 等库能够快速处理大型数组的根本原因——它们的 C 底层充分利用了这些指令集。

SIMD 计算模型(以 AVX2 为例): 标量加法(1次/周期): [a0] + [b0] = [c0] SIMD 加法,256-bit寄存器(8次/周期): [a0 a1 a2 a3 a4 a5 a6 a7] + [b0 b1 b2 b3 b4 b5 b6 b7] = [c0 c1 c2 c3 c4 c5 c6 c7] → 吞吐量提升 8x(Float32,AVX2) → AVX-512 可达 16x(Float32)

主流 SIMD 指令集对比

SSE/SSE2
128-bit 寄存器(4× Float32 或 2× Float64)。几乎所有 x86-64 CPU 都支持,2000 年代引入,是最基础的 SIMD 能力。
AVX/AVX2
256-bit 寄存器(8× Float32 或 4× Float64)。2011 年引入(Intel Sandy Bridge),大多数现代桌面/服务器 CPU 支持。机器学习训练最常见的指令集。
AVX-512
512-bit 寄存器(16× Float32 或 8× Float64)。Intel Skylake-SP 及更新服务器 CPU 支持,部分消费级 CPU 也支持。理论峰值吞吐是 SSE 的 4 倍。
ARM NEON / SVE
ARM 处理器的 SIMD 指令集。NEON 是 128-bit,广泛用于移动端和 Apple Silicon。SVE(Scalable Vector Extension)是 ARM 服务器芯片的可变宽度 SIMD,支持 128-2048 bit。

SIMD[DType, size] 类型

Mojo 的 SIMD 一等公民

Mojo 将 SIMD 向量作为内置类型 SIMD[dtype, size],而不是像 C++ 那样依赖编译器内置函数(intrinsics)或 Python 那样通过 NumPy 间接调用。这让 SIMD 编程变得直观。

from sys.info import simdwidthof

fn simd_basics():
    # 创建 SIMD 向量(8个 Float32)
    var a = SIMD[DType.float32, 8](1.0, 2.0, 3.0, 4.0,
                                     5.0, 6.0, 7.0, 8.0)
    var b = SIMD[DType.float32, 8](2.0)  # 广播:全部设为 2.0

    # 向量运算(一条指令处理全部 8 个元素)
    var c = a + b   # [3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
    var d = a * b   # [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0]
    var e = a / b   # [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0]

    print(c)

    # 访问单个元素
    print(c[0])    # 3.0
    print(c[7])    # 10.0

    # reduce_add: 水平求和
    print(c.reduce_add())  # 63.0 = 3+4+5+6+7+8+9+10

DType 枚举:支持的数据类型

# DType 支持的元素类型
alias F16Vec = SIMD[DType.float16, 16]  # 16× FP16 = 256 bit
alias F32Vec = SIMD[DType.float32, 8]   # 8× FP32 = 256 bit
alias F64Vec = SIMD[DType.float64, 4]   # 4× FP64 = 256 bit
alias I32Vec = SIMD[DType.int32, 8]     # 8× Int32 = 256 bit
alias U8Vec  = SIMD[DType.uint8, 32]    # 32× UInt8 = 256 bit(图像处理)
alias BF16Vec = SIMD[DType.bfloat16, 16] # 16× BF16(AI训练)

simd_width:自动检测最优向量宽度

跨平台自适应代码

不同 CPU 的 SIMD 宽度不同(128/256/512 bit)。手动写死向量宽度会导致代码无法最优利用当前硬件。Mojo 提供 simdwidthof[DType]() 在编译时获取当前硬件对指定类型最优的 SIMD 宽度。

from sys.info import simdwidthof
from algorithm import vectorize

fn adaptive_add(
    a: List[Float32],
    b: List[Float32],
    inout result: List[Float32]
):
    # 编译期自动确定最优向量宽度
    alias SIMD_WIDTH = simdwidthof[DType.float32]()
    # SSE:  SIMD_WIDTH = 4
    # AVX2: SIMD_WIDTH = 8
    # AVX512: SIMD_WIDTH = 16

    let n = len(a)

    @parameter
    fn vectorized_add[simd_width: Int](i: Int):
        # 加载 simd_width 个 Float32 到向量寄存器
        var va = SIMD[DType.float32, simd_width].load(a, i)
        var vb = SIMD[DType.float32, simd_width].load(b, i)
        (va + vb).store(result, i)

    vectorize[vectorized_add, SIMD_WIDTH](n)

fma:融合乘加指令

FMA 为什么重要?

FMA(Fused Multiply-Add,融合乘加) 是现代 CPU 的关键指令:result = a * b + c 在一个时钟周期内完成,精度优于分两步计算(中间结果不舍入)。神经网络推理中 95% 以上的计算是矩阵乘法,而矩阵乘法的核心就是大量的乘加操作——FMA 是其中的性能基础。

from math import fma

fn fma_demo():
    # 标量 FMA
    var result = fma(3.0, 4.0, 5.0)  # 3.0 * 4.0 + 5.0 = 17.0
    print(result)

    # 向量 FMA(SIMD)
    var va = SIMD[DType.float32, 8](1.0, 2.0, 3.0, 4.0,
                                     5.0, 6.0, 7.0, 8.0)
    var vb = SIMD[DType.float32, 8](2.0)
    var vc = SIMD[DType.float32, 8](10.0)

    # a[i] * b[i] + c[i],一条 VFMADD 指令处理 8 个元素
    var vd = fma(va, vb, vc)
    print(vd)  # [12.0, 14.0, 16.0, 18.0, 20.0, 22.0, 24.0, 26.0]

实战:向量化矩阵乘法

from sys.info import simdwidthof
from algorithm import vectorize
from math import fma
from time import now

alias F32 = DType.float32
alias SIMD_W = simdwidthof[F32]()

fn matmul_vectorized(
    C: UnsafePointer[Float32],
    A: UnsafePointer[Float32],
    B: UnsafePointer[Float32],
    M: Int, N: Int, K: Int
):
    """C[M×N] = A[M×K] @ B[K×N]"""
    for m in range(M):
        for k in range(K):
            var a_val = SIMD[F32, SIMD_W](A[m * K + k])

            @parameter
            fn inner[sw: Int](n: Int):
                var b_vec = SIMD[F32, sw].load(B + k * N + n)
                var c_vec = SIMD[F32, sw].load(C + m * N + n)
                # FMA: C[m,n:n+sw] += A[m,k] * B[k,n:n+sw]
                fma(a_val, b_vec, c_vec).store(C + m * N + n)

            vectorize[inner, SIMD_W](N)

fn benchmark():
    alias M = 512; alias N = 512; alias K = 512
    var A = UnsafePointer[Float32].alloc(M * K)
    var B = UnsafePointer[Float32].alloc(K * N)
    var C = UnsafePointer[Float32].alloc(M * N)

    let start = now()
    matmul_vectorized(C, A, B, M, N, K)
    let elapsed = (now() - start) / 1_000_000

    let flops = 2 * M * N * K
    let gflops = Float64(flops) / (Float64(elapsed) * 1e6)
    print("耗时:", elapsed, "ms")
    print("GFLOPS:", gflops)

    A.free(); B.free(); C.free()

def main():
    benchmark()
性能数据参考(512×512 矩阵乘法,MacBook Pro M2)

纯 Python:~45,000ms(45秒)
NumPy(调用 BLAS):~2ms
Mojo vectorize(无并行):~3-5ms
Mojo vectorize + parallelize:~0.5ms(见第6章)
纯 Python → Mojo 优化版:约 10,000-90,000 倍加速(取决于矩阵大小和硬件)

本章小结

SIMD 原理:一条指令同时处理多个数据(4/8/16 个),是 CPU 并行性的核心来源。
SIMD[DType, size]:Mojo 的内置 SIMD 向量类型,直接映射到 CPU 向量寄存器,支持算术、比较、归约等操作。
simdwidthof:编译期查询当前硬件对指定 DType 的最优 SIMD 宽度,写出跨平台自适应代码。
FMA:融合乘加指令,a * b + c 一步完成,是矩阵乘法/神经网络计算的性能基础。
vectorize:自动将循环向量化,配合 simd_width 实现最优 SIMD 利用率。