跳转至

ARM 与 NEON

一句话总结: ARM 是手机、Mac 与云服务器的主流架构,本节从架构基础到 NEON / I8MM / SVE / SME 一路讲清 ARM SIMD 编程,并演示实战 kernel.

ARM 处理器驱动着每一部智能手机、大多数平板、苹果的笔记本,以及越来越多的数据中心服务器。本节涵盖 ARM 架构、用 C++ 内建函数写 NEON SIMD、用于可伸缩向量处理的 SVE/SVE2、Apple Silicon 的独特之处,以及若干实用的向量化 kernel 示例.

  • 如果你有一部 iPhone、一台 MacBook,或者用过 AWS Graviton 实例,那你就在跑 ARM。ARM 的能效优势让它在移动与嵌入式领域一统天下,也在服务器和 ML 推理里越来越有竞争力。搞懂 ARM SIMD,你写的代码就能在大多数人真正在用的硬件上飞起来。

  • 想看 ARM SIMD kernel 在生产环境里的真实案例,看看 Cactus —— 一个面向移动设备和可穿戴设备的低延迟 AI 引擎:github.com/cactus-compute/cactus。Cactus 为注意力、KV 缓存量化和分块 prefill 实现了定制的 ARM NEON 与 NPU 加速 kernel,在 ARM CPU 上跑出了业界最快的推理速度,内存占用只有其他引擎的十分之一。它的三层架构 (Engine → Graph → Kernels) 就是一个很好的例子,展示了本节讲的 SIMD 概念如何被用来搭建生产级的 ML 基础设施。

ARM 架构基础

  • ARM 是一种精简指令集计算机 (Reduced Instruction Set Computer, RISC) 架构 (见第 13 章)。关键特点:

    • 加载-存储架构 (load-store architecture):算术指令只能操作寄存器,不能直接操作内存。想把内存里两个数相加,你必须:(1) 把它们加载到寄存器,(2) 寄存器相加,(3) 把结果写回内存。这比 x86 (可以在一条指令里让寄存器和内存位置相加) 更简单,但也让流水线设计更干净。

    • 定长指令:每一条 ARMv8 (AArch64) 指令都恰好 32 位。这让解码又快又可预测 (不像 x86 那种变长指令,1 到 15 字节都有可能).

    • 32 个通用寄存器 (x0-x30,每个 64 位),外加栈指针 (sp) 和零寄存器 (xzr)。对比 x86 只有 16 个通用寄存器。寄存器越多 = 访存越少 = 代码越快。

    • 32 个 SIMD/浮点寄存器 (v0-v31,每个 128 位),用于 NEON 与浮点运算。

// ARM 汇编 (只为让你感受一下 —— 实际你会用内建函数, 不会写汇编)
// 两个寄存器相加
add x0, x1, x2    // x0 = x1 + x2

// 从内存加载
ldr x0, [x1]      // x0 = *x1 (从 x1 指向的地址加载 64 位)

// NEON: 加四个浮点
fadd v0.4s, v1.4s, v2.4s  // v0 = v1 + v2 (四个 32 位浮点)
  • 你不会手写汇编。你会用内建函数 (intrinsics):一种 C/C++ 函数,与特定指令一一对应。编译器负责寄存器分配、调度等底层细节。

NEON: 128 位 SIMD

  • NEON 是 ARM 的 SIMD 扩展。每个 NEON 寄存器宽 128 位,能装下:
数据类型 每寄存器元素数 记法
float32 4 float32x4_t
float16 8 float16x8_t
int32 4 int32x4_t
int16 8 int16x8_t
int8 16 int8x16_t
  • 128 位比 x86 的高级向量扩展 (Advanced Vector Extensions, AVX) 256 位或 AVX-512 的 512 位要窄。但 ARM 靠出色的能效和广泛的普及率补了回来。

NEON 内建函数:基础

  • NEON 内建函数遵循一种命名约定:v[操作][修饰]_[类型]
#include <arm_neon.h>

// 从内存加载 4 个浮点到一个 NEON 寄存器
float32x4_t a = vld1q_f32(ptr);        // vld1q = vector load 1, q = 128 位 (quad)

// 从 NEON 寄存器存 4 个浮点到内存
vst1q_f32(out_ptr, a);                   // vst1q = vector store 1, q = 128 位

// 算术
float32x4_t c = vaddq_f32(a, b);        // c = a + b (4 个浮点)
float32x4_t d = vmulq_f32(a, b);        // d = a * b (4 个浮点)
float32x4_t e = vfmaq_f32(c, a, b);     // e = c + a * b (融合乘加, 4 个浮点)

// 比较 (返回掩码: true 全 1, false 全 0)
uint32x4_t mask = vcgtq_f32(a, b);      // mask[i] = (a[i] > b[i]) ? 0xFFFFFFFF : 0

// 按掩码选择元素 (类似 numpy.where)
float32x4_t result = vbslq_f32(mask, a, b);  // result[i] = mask[i] ? a[i] : b[i]

// 归约: 把 4 个元素加成一个标量
float total = vaddvq_f32(a);             // total = a[0] + a[1] + a[2] + a[3]
  • vfmaq_f32 (融合乘加,fused multiply-add) 是对 ML 最重要的 SIMD 指令。它一条指令算出 \(c = c + a \times b\),只做一次舍入 (比先乘再加分开做更精确)。点积、矩阵乘、卷积,全都建立在 FMA 之上。

实战示例:向量化点积

  • 点积是矩阵乘法的内层循环。我们先写标量版 C++,再用 NEON 把它向量化。
#include <arm_neon.h>

// 标量点积
float dot_scalar(const float* a, const float* b, int n) {
    float sum = 0.0f;
    for (int i = 0; i < n; i++) {
        sum += a[i] * b[i];
    }
    return sum;
}

// NEON 向量化点积
float dot_neon(const float* a, const float* b, int n) {
    float32x4_t sum_vec = vdupq_n_f32(0.0f);  // 4 个累加器初始化为 0

    int i = 0;
    for (; i + 4 <= n; i += 4) {
        float32x4_t va = vld1q_f32(a + i);     // 从 a 加载 4 个元素
        float32x4_t vb = vld1q_f32(b + i);     // 从 b 加载 4 个元素
        sum_vec = vfmaq_f32(sum_vec, va, vb);   // sum_vec += va * vb
    }

    // 把 4 个累加器归约成一个标量
    float sum = vaddvq_f32(sum_vec);

    // 处理剩余元素 (n 不是 4 的倍数时)
    for (; i < n; i++) {
        sum += a[i] * b[i];
    }

    return sum;
}
  • 关键 C++ 概念:

    • const float*:指向只读浮点数据的指针。const 表示我们保证不会通过这个指针修改数据。
    • a + i:指针算术。a + i 指向数组第 \(i\) 个元素 (等价于 &a[i]).
    • 末尾的"清理循环"处理 \(n\) 不是 4 的倍数的情况。这是 SIMD 代码里通用的套路:主体用向量化分块处理,余数用标量代码收尾。
  • 为什么 sum_vec 里要用 4 个累加器:我们不用一个标量累加器,而是用 4 个相互独立的累加器 (每个 SIMD 通道一个)。这样可以避开数据依赖:单累加器版本每一轮的 FMA 都依赖 sum_vec,而 4 个独立通道让 CPU 可以把 FMA 流水线并行起来。最后我们再把 4 个部分和归约为一。

实战示例:向量化 ReLU

#include <arm_neon.h>

void relu_neon(const float* input, float* output, int n) {
    float32x4_t zero = vdupq_n_f32(0.0f);

    int i = 0;
    for (; i + 4 <= n; i += 4) {
        float32x4_t x = vld1q_f32(input + i);
        float32x4_t result = vmaxq_f32(x, zero);  // max(x, 0) = ReLU
        vst1q_f32(output + i, result);
    }

    // 标量收尾
    for (; i < n; i++) {
        output[i] = input[i] > 0 ? input[i] : 0;
    }
}
  • vmaxq_f32 逐元素取两个向量的最大值。因为其中一个向量全是零,结果就是 ReLU (Rectified Linear Unit, ReLU)。没有分支、没有比较 —— 就一条指令。

I8MM:整数矩阵乘

  • I8MM (Int8 Matrix Multiply, INT8 矩阵乘) 是 ARMv8.6 的一个扩展,新增了专门的指令做 INT8 矩阵乘 + INT32 累加 —— 正好对应量化 ML 推理的需求。

  • 关键指令是 SMMLA (Signed Matrix Multiply-Accumulate,有符号矩阵乘累加):它拿两个 8×2 的 INT8 块,把结果累加到一个 2×2 的 INT32 块:

#include <arm_neon.h>

// I8MM: 两个 8 元素的 INT8 向量相乘, 累加得到 4 个 INT32 结果
// 用 2x8 x 8x2 的输入 tile 算出输出矩阵的 2x2 tile
void matmul_i8mm_tile(const int8_t* A, const int8_t* B, int32_t* C) {
    // 从 A 加载 8 字节 (2 行, 每行打包 4 个元素)
    int8x16_t va = vld1q_s8(A);   // 16 字节 = 2 行 × 8 元素
    int8x16_t vb = vld1q_s8(B);   // 16 字节 = 2 行 × 8 元素

    // 加载已有的累加器 (2x2 = 4 个 int32)
    int32x4_t acc = vld1q_s32(C);

    // I8MM 指令: acc += A_tile × B_tile^T
    // 从 2×8 × 8×2 的输入算出 2×2 的输出
    acc = vmmlaq_s32(acc, va, vb);  // 这就是 I8MM 指令

    vst1q_s32(C, acc);
}
  • I8MM 为什么重要:没有 I8MM 的时候,NEON 上做 INT8 矩阵乘要先做扩宽乘 (vmull),再做两两加 —— 每个输出元素要好几条指令。有了 I8MM,硬件一条指令就做一次 8 元素点积 (2×8 × 8×2 = 2×2)。对 INT8 推理负载,这比纯 NEON 快 4-8 倍。

  • 可用范围: Apple M1+ (所有 Apple Silicon)、ARM Cortex-A510/A710/X2+ (ARMv9)、AWS Graviton3+。用 #ifdef __ARM_FEATURE_MATMUL_INT8 判断。

  • 对 ML 推理:跑在 ARM 服务器 (Graviton) 或 Apple Silicon 上的 INT8 量化模型 (第 18 章) 会从 I8MM 里获得巨大收益。ONNX Runtime、llama.cpp 这类框架会在运行时检测 I8MM,自动切到优化过的 kernel.

SME 与 SME2:可伸缩矩阵扩展

  • SME (Scalable Matrix Extension,可伸缩矩阵扩展) 是 ARM 对标 Intel AMX 和 NVIDIA Tensor Core 的答案:专门做矩阵运算的硬件单元。SME2 (ARMv9.2) 又进一步扩展。

  • SME 引入了 ZA tile 寄存器:存在硬件里的 2D 矩阵,最大 SVL×SVL 字节 (SVL 是流式向量长度 (streaming vector length),每维通常 128 到 512 位)。跟 NEON (1D 向量) 甚至 SVE (1D 可伸缩向量) 不同,SME 原生就在 2D tile 上操作。

  • 编程模型有两种模式:

    • 普通模式:标准 ARM 执行 (NEON、SVE 正常工作).
    • 流式 SVE 模式:通过 smstart 进入,开启 SME 指令。这个模式下 SVE 指令也能用,但寄存器宽度可能不同。
#include <arm_sme.h>

// SME2: 用外积累加做矩阵乘
// 把 A_col × B_row 累加到 ZA tile 寄存器
void sme2_matmul_outer(const float* A_col, const float* B_row, int K) {
    // 进入流式模式
    // smstart;  // (通过编译器内建函数或内联汇编触发)

    // 清空 ZA tile 累加器
    svzero_za();

    for (int k = 0; k < K; k++) {
        // 把 A 的一列和 B 的一行加载到 SVE 寄存器
        svfloat32_t a = svld1_f32(svptrue_b32(), &A_col[k * SVL]);
        svfloat32_t b = svld1_f32(svptrue_b32(), &B_row[k * SVL]);

        // 外积: ZA += a × b^T
        // 这一条指令累加一整个 SVL×SVL 的 tile
        svmopa_za32_f32_m(0, svptrue_b32(), svptrue_b32(), a, b);
    }

    // 把 ZA tile 存回内存
    // svst1_za(...);

    // 退出流式模式
    // smstop;
}
  • 关键概念:

    • svmopa (外积累加,outer product accumulate): SME 的核心指令。它算出两个向量的完整外积,并累加到 ZA tile。当 SVL=512 位 (16 个浮点),这就是 16×16 的外积 —— 一条指令 256 次 FMA.
    • ZA tile:在流式模式内跨指令持久保留。你在 K 维每一轮做一次外积,累加到同一个 tile 里,一步步搭出一个完整的矩阵乘 tile.
    • 流式模式: SME 指令只在流式模式下工作。进出流式模式的开销意味着 SME 更适合持续的矩阵计算,不适合短促的爆发式调用。
  • SME2 的新增:多向量操作 (一次处理 2 或 4 个 SVE 向量)、更多 tile 操作,以及与普通模式更好的整合。

  • 可用范围: ARM Neoverse V2 (AWS Graviton4),一些即将发布的移动芯片。截至 2026 年,Apple Silicon 上还没有。SME 还很早期 —— 大多数 ML 框架还没有 SME 优化过的 kernel.

  • 演化脉络: NEON (128 位向量,逐元素) → I8MM (INT8 矩阵 tile) → SVE (可伸缩向量) → SME (可伸缩 2D 矩阵 tile)。每一代都在向"硬件原生支持矩阵运算"更近一步。

SVE 与 SVE2:可伸缩向量扩展

  • NEON 的宽度固定 128 位。SVE (Scalable Vector Extension,可伸缩向量扩展) 引入了向量长度无关 (vector-length agnostic, VLA) 编程:你只写一遍代码,它在任意向量宽度 (128 到 2048 位) 的硬件上都能跑。宽度由硬件在运行时决定。
#include <arm_sve.h>

void add_sve(const float* a, const float* b, float* c, int n) {
    int i = 0;
    svbool_t pred = svwhilelt_b32(i, n);  // 谓词: 哪些通道生效

    while (svptest_any(svptrue_b32(), pred)) {
        svfloat32_t va = svld1(pred, a + i);
        svfloat32_t vb = svld1(pred, b + i);
        svst1(pred, c + i, svadd_x(pred, va, vb));

        i += svcntw();  // 按硬件向量宽度前进 (以 32 位元素计)
        pred = svwhilelt_b32(i, n);
    }
}
  • 谓词寄存器 (predicate register, svbool_t) 取代了标量收尾循环。每个通道有一个谓词位:生效的通道参与运算,失效的通道被屏蔽掉。svwhilelt_b32(i, n) 指令生成一个谓词,让对应下标 i, i+1, ..., n-1 的通道生效。这样尾部自动就处理好了。

  • svcntw() 在运行时返回每个向量寄存器里的 32 位元素数。在 256 位 SVE 的 CPU 上返回 8,在 512 位 SVE 上返回 16。你的代码自动适配。

  • SVE 目前可用于 ARM Neoverse V1/V2 (AWS Graviton3/4,一些服务器芯片)。Apple Silicon 上还没有。

Apple Silicon 的独特之处

  • 苹果的 M 系列芯片 (M1、M2、M3、M4) 基于 ARM,但用了自家定制的微架构:

  • 性能核与效率核: P 核 (Firestorm/Avalanche 等) 负责重型计算,E 核 (Icestorm/Blizzard 等) 处理后台任务。调度器会把线程分配到合适的核心类型。

  • AMX (Apple Matrix eXtensions,苹果矩阵扩展):专门的矩阵乘单元,与 NEON 是分开的两套硬件。AMX 没有公开文档 (苹果没有公开 ISA),但 Accelerate 框架内部用了它做 BLAS 运算。你在 Mac 上调 np.dot,底下走的是 Accelerate,用的就是 AMX。你没法直接编程 AMX (除非做逆向工程).

  • 统一内存 (unified memory): CPU 和 GPU 共用同一块物理 RAM。其他系统上,数据必须从 CPU 内存拷贝到 GPU 内存 (通过 PCIe,约 32 GB/s)。Apple Silicon 上没有拷贝 —— GPU 读的就是 CPU 写过的那块内存。这为 ML 负载消除了一大瓶颈。

  • 神经引擎 (Neural Engine):一个 16 核的专用 ML 加速器。INT8 推理约 30 TOPS (trillion operations per second,每秒万亿次操作)。Core ML 就用它做端侧推理。

  • 在 Apple Silicon 上做 ML:用 MLX (苹果的 ML 框架),它是专门为统一内存架构设计的。PyTorch 也有 MPS (Metal Performance Shaders) 后端,不过还不如 CUDA 成熟。

自动向量化

  • 手写 SIMD 内建函数很啰嗦。编译器能不能自动帮我们向量化?

  • 能,但有前提。现代编译器 (GCC、Clang) 可以自动向量化简单的循环:

// 编译器可以自动向量化这段 (加上 -O3 -march=native)
void add_auto(const float* a, const float* b, float* c, int n) {
    for (int i = 0; i < n; i++) {
        c[i] = a[i] + b[i];
    }
}
  • 有利于自动向量化的模式:
    • 循环次数已知的简单循环。
    • 迭代之间没有数据依赖 (c[i] 不依赖 c[i-1]).
    • 连续内存访问 (不是 scatter/gather).
    • 用 const 和 restrict 修饰指针 (告诉编译器数组之间不重叠).
// restrict 告诉编译器: a, b, c 指向不重叠的内存
void add_restrict(const float* __restrict__ a,
                  const float* __restrict__ b,
                  float* __restrict__ c, int n) {
    for (int i = 0; i < n; i++) {
        c[i] = a[i] + b[i];
    }
}
  • 没有 restrict,编译器只能假设 c 可能与 a 或 b 重叠 (写 c[i] 可能改到 a[i+1]),这样就没法向量化了。

  • 阻碍自动向量化的模式:

    • 数据依赖:a[i] = a[i-1] + b[i] (每一轮依赖上一轮).
    • 复杂控制流:循环内的 if 语句 (除非编译器能转成谓词化).
    • 循环内的函数调用 (除非函数被内联).
    • 指针混叠 (数组可能重叠,而且没写 restrict).
  • 查看自动向量化情况:用编译器 flag 看哪些循环被向量化了:

# GCC: 打印向量化决策
g++ -O3 -march=native -fopt-info-vec-optimized code.cpp

# Clang: 打印向量化报告
clang++ -O3 -march=native -Rpass=loop-vectorize code.cpp
  • 何时用内建函数,何时靠自动向量化:先写干净的 C++ 加编译器优化。如果编译器能向量化,万事大吉。如果性能还不够,先看编译器的向量化报告搞清楚为什么,然后再对关键内层循环手写内建函数。过早上内建函数会让代码难读,收益也不一定值。

编程任务 (在 ARM 上用 g++ 或 clang++ 编译 —— Mac M 系列或 Linux aarch64)

  1. 写一个标量点积和一个 NEON 向量化点积。跑 benchmark 测出加速比。

    // task1_neon_dot.cpp
    // 编译 (Mac/ARM Linux): clang++ -O3 -o task1 task1_neon_dot.cpp
    // 注意: AArch64 默认开启 NEON, 不用加额外 flag
    
    #include <iostream>
    #include <chrono>
    #include <vector>
    #include <arm_neon.h>
    
    float dot_scalar(const float* a, const float* b, int n) {
        float sum = 0.0f;
        for (int i = 0; i < n; i++) {
            sum += a[i] * b[i];
        }
        return sum;
    }
    
    float dot_neon(const float* a, const float* b, int n) {
        float32x4_t sum_vec = vdupq_n_f32(0.0f);
        int i = 0;
        for (; i + 4 <= n; i += 4) {
            float32x4_t va = vld1q_f32(a + i);
            float32x4_t vb = vld1q_f32(b + i);
            sum_vec = vfmaq_f32(sum_vec, va, vb);
        }
        float sum = vaddvq_f32(sum_vec);
        for (; i < n; i++) sum += a[i] * b[i];
        return sum;
    }
    
    int main() {
        const int N = 10'000'000;
        std::vector<float> a(N, 1.0f), b(N, 2.0f);
    
        // 预热
        volatile float s1 = dot_scalar(a.data(), b.data(), N);
        volatile float s2 = dot_neon(a.data(), b.data(), N);
    
        // 标量 benchmark
        auto start = std::chrono::high_resolution_clock::now();
        for (int t = 0; t < 100; t++) {
            s1 = dot_scalar(a.data(), b.data(), N);
        }
        auto end = std::chrono::high_resolution_clock::now();
        double scalar_ms = std::chrono::duration<double, std::milli>(end - start).count() / 100;
    
        // NEON benchmark
        start = std::chrono::high_resolution_clock::now();
        for (int t = 0; t < 100; t++) {
            s2 = dot_neon(a.data(), b.data(), N);
        }
        end = std::chrono::high_resolution_clock::now();
        double neon_ms = std::chrono::duration<double, std::milli>(end - start).count() / 100;
    
        std::cout << "Scalar: " << scalar_ms << " ms (result: " << s1 << ")\n";
        std::cout << "NEON:   " << neon_ms << " ms (result: " << s2 << ")\n";
        std::cout << "Speedup: " << scalar_ms / neon_ms << "x\n";
        return 0;
    }
    

  2. 实现 NEON 版 ReLU 和 softmax 里找最大值的操作。用不同的操作练一练 load→compute→store 的模式。

    // task2_neon_ops.cpp
    // 编译: clang++ -O3 -o task2 task2_neon_ops.cpp
    
    #include <iostream>
    #include <vector>
    #include <cmath>
    #include <arm_neon.h>
    
    void relu_neon(const float* in, float* out, int n) {
        float32x4_t zero = vdupq_n_f32(0.0f);
        int i = 0;
        for (; i + 4 <= n; i += 4) {
            float32x4_t x = vld1q_f32(in + i);
            vst1q_f32(out + i, vmaxq_f32(x, zero));
        }
        for (; i < n; i++) out[i] = in[i] > 0 ? in[i] : 0;
    }
    
    float max_neon(const float* data, int n) {
        float32x4_t max_vec = vdupq_n_f32(-INFINITY);
        int i = 0;
        for (; i + 4 <= n; i += 4) {
            max_vec = vmaxq_f32(max_vec, vld1q_f32(data + i));
        }
        float result = vmaxvq_f32(max_vec);
        for (; i < n; i++) result = result > data[i] ? result : data[i];
        return result;
    }
    
    int main() {
        std::vector<float> data = {-3, 1, -1, 4, 2, -5, 0, 7, -2, 3};
        std::vector<float> out(data.size());
    
        relu_neon(data.data(), out.data(), data.size());
        std::cout << "ReLU: ";
        for (float x : out) std::cout << x << " ";
        std::cout << "\n";
    
        float mx = max_neon(data.data(), data.size());
        std::cout << "Max: " << mx << " (expected: 7)\n";
        return 0;
    }
    

  3. 把自动向量化的代码和手写 NEON 内建函数做对比。用 -fopt-info-vec (GCC) 或 -Rpass=loop-vectorize (Clang) 编译,看看编译器都干了什么。

    // task3_auto_vs_manual.cpp
    // 编译: clang++ -O3 -Rpass=loop-vectorize -o task3 task3_auto_vs_manual.cpp
    //    (或): g++ -O3 -fopt-info-vec-optimized -o task3 task3_auto_vs_manual.cpp
    
    #include <iostream>
    #include <chrono>
    #include <vector>
    #include <arm_neon.h>
    
    // 让编译器自动向量化
    void add_auto(const float* __restrict__ a, const float* __restrict__ b,
                  float* __restrict__ c, int n) {
        for (int i = 0; i < n; i++) {
            c[i] = a[i] + b[i];
        }
    }
    
    // 手写 NEON
    void add_neon(const float* a, const float* b, float* c, int n) {
        int i = 0;
        for (; i + 4 <= n; i += 4) {
            vst1q_f32(c + i, vaddq_f32(vld1q_f32(a + i), vld1q_f32(b + i)));
        }
        for (; i < n; i++) c[i] = a[i] + b[i];
    }
    
    int main() {
        const int N = 10'000'000;
        std::vector<float> a(N, 1.0f), b(N, 2.0f), c(N);
    
        auto bench = [&](auto fn, const char* name) {
            fn(a.data(), b.data(), c.data(), N);  // 预热
            auto start = std::chrono::high_resolution_clock::now();
            for (int t = 0; t < 100; t++) fn(a.data(), b.data(), c.data(), N);
            auto end = std::chrono::high_resolution_clock::now();
            double ms = std::chrono::duration<double, std::milli>(end - start).count() / 100;
            std::cout << name << ": " << ms << " ms\n";
        };
    
        bench(add_auto, "Auto-vectorised");
        bench(add_neon, "Hand-written NEON");
        // 它们应该非常接近 —— 这种简单循环编译器自动向量化得很好
        return 0;
    }
    


📌 本节要点

  • ARM 架构: RISC 精简指令集,加载-存储、定长指令、32 通用寄存器 + 32 SIMD 寄存器,能效突出。
  • NEON: ARM 的 128 位 SIMD,每寄存器可放 4 个 float32、8 个 float16 或 16 个 int8,通过 C/C++ 内建函数调用。
  • FMA 是核心: vfmaq_f32 一条指令做融合乘加,是点积、矩阵乘、卷积的基石。
  • 多累加器技巧:用多个独立累加器打破数据依赖,让 CPU 流水线并行 FMA.
  • 收尾循环: SIMD 主体处理整块 + 标量循环处理余数,是 SIMD 编程的通用套路。
  • I8MM: ARMv8.6 起的 INT8 矩阵乘指令 (SMMLA),一条指令算 2×8×8×2 tile, INT8 推理提速 4-8 倍。
  • SVE / SVE2:向量长度无关 (VLA) 编程,谓词寄存器代替收尾循环,128-2048 位宽度由硬件决定。
  • SME / SME2:原生 2D 矩阵 tile 运算,流式模式 + ZA tile 寄存器,对标 Intel AMX 与 NVIDIA Tensor Core.
  • Apple Silicon: P/E 核 + 未公开的 AMX + 统一内存 + Neural Engine, MLX 是首选 ML 框架。
  • 自动向量化优先:简单循环写清楚 + -O3 + restrict,编译器通常够用;关键内层循环再手写内建函数。