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)¶
-
写一个标量点积和一个 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; } -
实现 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; } -
把自动向量化的代码和手写 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,编译器通常够用;关键内层循环再手写内建函数。