Skip to content
当前页大纲

Mojo SIMD 向量化编程

本篇覆盖:SIMD 类型与基本运算、向量化的价值、vectorize 工具、benchmark 基准测试。SIMD 是 Mojo 对 AI 开发者最实用的能力——不用写汇编就能吃满 CPU 向量指令。

一、什么是 SIMD,为什么重要

SIMD(Single Instruction, Multiple Data):一条指令同时处理多个数据。现代 CPU 的 AVX2/AVX-512/NEON 指令集可以一次做 4~16 个浮点运算——理论上白拿 4~16 倍吞吐

问题在于:C++ 里手写 SIMD 要么依赖编译器自动向量化(玄学),要么手撸 intrinsic(痛苦);Python 里完全无门。Mojo 把 SIMD 做成了一等语言公民

二、SIMD 类型:向量是一等公民

SIMD 是标准库里的参数化类型(编译期参数:元素类型 + 宽度):

mojo
def main():
    # 4 个 float64 组成的向量
    var a = SIMD[DType.float64, 4](1.0, 2.0, 3.0, 4.0)
    var b = SIMD[DType.float64, 4](10.0, 20.0, 30.0, 40.0)

    var c = a + b         # 一条指令算 4 个加法
    print(c)              # [11.0, 22.0, 33.0, 44.0]

    var d = a * b         # 对应元素相乘
    print(d)              # [10.0, 40.0, 90.0, 160.0]

    # 归约:把向量压成一个标量
    print(c.reduce_add())         # 110.0
    print(a.reduce_max())         # 4.0

所有算术、比较、位运算都直接作用在向量上,写起来和标量没区别——但生成的机器码是真实的向量指令。

日常写代码时甚至不用感知它:Mojo 里的 IntFloat64 本质就是 SIMD[int64, 1]SIMD[float64, 1]——宽度为 1 的向量。标量和向量是同一套类型系统的两档。

三、标量循环 vs 向量化:直接看差距

两个数组逐元素相乘的例子。

标量写法(和 Python 一样):

mojo
def mul_scalar(a: List[Float64], b: List[Float64]) -> List[Float64]:
    var result = List[Float64]()
    for i in range(len(a)):
        result.append(a[i] * b[i])
    return result

SIMD 写法(借 UnsafePointer 做向量加载,宽度 4):

mojo
from memory import UnsafePointer

def mul_simd(a: UnsafePointer[Float64],
             b: UnsafePointer[Float64],
             result: UnsafePointer[Float64],
             size: Int):
    var width = 4
    for i in range(0, size, width):
        var va = a.load[width=4](i)     # 一次加载 4 个 float64
        var vb = b.load[width=4](i)
        result.store[width=4](i, va * vb)  # 一条指令算 4 个乘法

load[width=4](i) 一次读一个 4 元素向量,乘法在向量寄存器里完成,store 一次写回——同样长度的循环,迭代次数变为 1/4,每次迭代吞吐 ×4。

四、vectorize:自动帮你处理宽度

手写 SIMD 循环的麻烦在于宽度选择和尾部处理(长度不能被宽度整除时怎么办)。标准库的 vectorize 把这两件事都包了:

mojo
from algorithm import vectorize
from memory import UnsafePointer

def mul_simd_auto(a: UnsafePointer[Float64],
                  b: UnsafePointer[Float64],
                  result: UnsafePointer[Float64],
                  size: Int):
    @parameter
    @always_inline
    def compute(i: Int):
        var va = a.load[width=4](i)
        var vb = b.load[width=4](i)
        result.store[width=4](i, va * vb)

    vectorize[4, compute](size)

vectorize 负责按宽度切分区间并处理余数,你只需要写「第 i 个宽度为 4 的块」的计算。

想吃到机器的最大向量宽度(比如 AVX-512 的 16 路),用 simd_width_of[DType.float64]() 查询当前 CPU 的原生宽度,把它作为编译期参数传入,编译器即为你的机器量身生成指令。

五、用 benchmark 模块量化性能

标准库自带基准测试模块,run() 接受一个函数,自动处理预热和迭代统计:

mojo
from benchmark import run

def bench_workload():
    # 待测代码……
    pass

def main():
    var report = run(bench_workload)
    print("平均耗时 (ns):", report.mean)
    print("最小耗时 (ns):", report.min)

建议的实验路径:把第三节的标量版和 SIMD 版分别包进 benchmark,亲眼看看 3~8 倍的差距(具体倍数取决于 CPU 和数据规模)——这是理解「Mojo 为什么快」最直接的一课

六、SIMD 的适用边界

适合向量化的计算:

  • 逐元素运算:向量加/乘、激活函数、归一化
  • 归约:sum / max / min
  • 点积、矩阵乘内层循环(下一篇实战)

不适合的场景:

  • 分支密集的逻辑(每个元素走不同路径)
  • 数据依赖链长的串行计算(前后元素互相依赖)
  • 内存带宽瓶颈的任务(向量指令帮不了内存墙)

七、小结

  • SIMD[DType.float64, N] 是一等类型,算术运算直接映射向量指令
  • Int/Float64 就是宽度 1 的 SIMD——标量向量同一体系
  • vectorize 处理宽度切分和尾部,simd_width_of 查询硬件原生宽度
  • benchmark.run 量化优化效果,用数据说话

下一篇:与 Python 互操作。

MIT License.