文档目录

这是从**“算法对”到“代码跑得快”的最后一公里。你之前学的都是宏观架构**(缓存、内存、调度),而 SIMD 和预取是微观指令级的优化——让 CPU 的每个流水线周期都塞满有用的工作。


1. SIMD(单指令多数据流):一条指令干一堆活

什么是 SIMD?

SIMD(Single Instruction, Multiple Data)是 CPU 的并行处理扩展,允许一条指令同时对多个数据执行相同的操作。

标量(Scalar):一条指令处理一个数据
+--------+     +--------+
| ADD R1 | --> |  A + B  |  (1 次操作)
+--------+     +--------+

SIMD(向量化):一条指令处理 N 个数据
+--------+     +--------+--------+--------+--------+
| ADD ZMM | --> | A1+B1  | A2+B2  | A3+B3  | A4+B4  |  (4 次操作,1 条指令)
+--------+     +--------+--------+--------+--------+

硬件实现(x86 / ARM 的 SIMD 单元)

架构 指令集 寄存器宽度 同时处理数据量(单精度浮点)
x86 (Intel/AMD) SSE 128-bit 4 个 float
x86 (Intel/AMD) AVX2 256-bit 8 个 float
x86 (Intel/AMD) AVX-512 512-bit 16 个 float
ARM (手机/服务器) NEON 128-bit 4 个 float
ARM (服务器) SVE2 可变(最高 2048-bit) 可变

硬件结构:

[CPU 核心]
    │
    ├── 标量处理单元(通用计算,处理 int/float 等)
    │
    └── SIMD 处理单元(向量计算)
        ├── 256-bit 向量寄存器 (YMM0~YMM15)
        ├── 512-bit 向量寄存器 (ZMM0~ZMM31)  ← AVX-512
        └── SIMD 算术单元(可同时执行多个浮点/整数运算)

2. SIMD 实战:手动向量化 vs 自动向量化

场景:两个数组相加

朴素 C++ 实现(标量):

void add_arrays_scalar(float* a, float* b, float* c, size_t n) {
    for (size_t i = 0; i < n; ++i) {
        c[i] = a[i] + b[i];  // 每次循环只处理 1 个 float
    }
}

编译器自动向量化(开启 -O3 -mavx2):

// 编译器可能自动生成 AVX2 指令:
// vmovups ymm0, [a]    ; 加载 8 个 float
// vaddps ymm0, ymm0, [b] ; 8 个 float 同时加法
// vmovups [c], ymm0   ; 存储 8 个结果

性能对比:

实现方式 吞吐量 说明
标量(无优化) 1x 4 个元素/周期
编译器自动向量化 4x~8x 依赖编译器判断
手写 SIMD 内联 8x~16x 极致优化

手写 SIMD 内联(<immintrin.h>)

#include <immintrin.h>  // Intel 内联函数

void add_arrays_simd(float* a, float* b, float* c, size_t n) {
    size_t i = 0;
    // 每次处理 8 个 float(AVX2)
    for (; i + 7 < n; i += 8) {
        // 加载 8 个 float 到向量寄存器
        __m256 va = _mm256_loadu_ps(&a[i]);  // 256-bit 加载
        __m256 vb = _mm256_loadu_ps(&b[i]);
        
        // 向量加法(8 个 float 同时执行)
        __m256 vc = _mm256_add_ps(va, vb);
        
        // 存储 8 个结果
        _mm256_storeu_ps(&c[i], vc);
    }
    // 处理剩余元素(标量)
    for (; i < n; ++i) {
        c[i] = a[i] + b[i];
    }
}

硬件行为:

  • _mm256_loadu_ps:加载 256 位(8 × 32-bit)数据到 YMM 寄存器(~1 个周期)。
  • _mm256_add_ps:256-bit 加法指令,CPU 的 SIMD ALU 同时执行 8 个加法(~1 个周期)。
  • _mm256_storeu_ps:写回内存(可能触发缓存写入)。

总吞吐:8 个 float 加法只需 3 个 CPU 周期(加载+计算+存储),而标量需要 8 × 3 = 24 个周期,加速 8 倍。


3. 软件预取(Prefetch):让数据提前进缓存

为什么需要预取?

CPU 访问主存时,数据会先从主存加载到 L3 → L2 → L1 缓存,然后才能被处理。如果数据不在缓存中,CPU 会**停顿(Stall)**等待数据(~100ns)。

预取是 CPU 硬件或软件提前发起内存加载,让数据在 CPU 需要时已经躺在 L1/L2 缓存里了。

硬件预取(Hardware Prefetch)

CPU 内部有自动预取器(Prefetcher),能检测到程序的内存访问模式(如顺序访问数组),主动提前加载后续缓存行。

[CPU 访问数组 arr[0], arr[1], arr[2]...]
        │
        ▼
[硬件预取器] 检测到顺序模式
        │
        ▼
[自动预取 arr[3], arr[4], arr[5] 到 L2/L3 缓存]
        │
        ▼
[CPU 访问 arr[3]] → L1 命中(已在缓存中)→ 无停顿

硬件预取的局限:

  • 只能检测简单顺序模式。
  • 对于不规则访问(如跳表、哈希表、链表),硬件预取基本失效。

软件预取(Software Prefetch)

程序员可以用指令显式告诉 CPU:“接下来我要访问这个地址,请提前加载到缓存。”

#include <xmmintrin.h>  // GCC/Clang

void process_data(float* data, size_t n) {
    for (size_t i = 0; i < n; ++i) {
        // 预取下一个迭代的数据(提前 4 个元素)
        _mm_prefetch(&data[i + 4], _MM_HINT_T0);  // T0 = L1 缓存
        
        // 处理当前元素
        float value = data[i];
        // ...
    }
}

预取指令的类型(x86 PREFETCH):

级别 含义 适用场景
_MM_HINT_T0 预取到 L1 缓存 数据即将被使用(1~2 次迭代内)
_MM_HINT_T1 预取到 L2 缓存 数据将在稍后使用(4~8 次迭代后)
_MM_HINT_T2 预取到 L3 缓存 数据将在较远使用(16+ 次迭代后)
_MM_HINT_NTA 非临时预取(不污染缓存) 数据只用一次(如流式处理)

4. 实战:在 MPSC 队列中结合 SIMD + 预取

批量处理 + SIMD + 预取

void batch_process_mpsc(float* buffer, size_t count) {
    // 批量取出 8 个 float(AVX2)
    size_t i = 0;
    
    // 预取第一批数据到 L1
    _mm_prefetch(&buffer[0], _MM_HINT_T0);
    
    for (; i + 7 < count; i += 8) {
        // 预取下一批数据(提前加载,隐藏延迟)
        _mm_prefetch(&buffer[i + 16], _MM_HINT_T0);
        
        // SIMD 加载 + 处理
        __m256 vec = _mm256_loadu_ps(&buffer[i]);
        vec = _mm256_mul_ps(vec, _mm256_set1_ps(2.0f));  // 乘以 2
        _mm256_storeu_ps(&buffer[i], vec);
    }
    // 处理剩余元素...
}

硬件流水线:

周期 N:  CPU 执行 SIMD 加法(处理 buffer[i])
周期 N+1: 预取 buffer[i+16] 的数据已经开始从主存加载到 L3
周期 N+2: 预取数据进入 L2
周期 N+3: 预取数据进入 L1
...
周期 N+16: CPU 处理到 buffer[i+16] → 数据已在 L1 缓存 → L1 命中,无停顿!

性能收益:在不规则内存访问(如哈希表、链表遍历)中,软件预取可以减少 50%~70% 的 Cache Miss。


5. SIMD + 预取的适用场景

场景 SIMD 适用性 预取适用性 加速比
数组运算(逐元素) ✅ 极佳 ✅ 硬件预取已足够 4x~16x
矩阵乘法(可分块) ✅ 极佳 ✅ 手工分块+预取 10x~20x
图像处理(像素遍历) ✅ 极佳 ✅ 硬件预取有效 4x~8x
链表的遍历 ❌ 无法 SIMD ✅ 软件预取很关键 1.5x~2x
哈希表查找 ❌ 无法 SIMD ⚠️ 预取有部分帮助 1.2x~1.5x
JSON 解析 ❌ 无法 SIMD ⚠️ 预取帮助有限 1x
随机指针跳转 ❌ 无法 SIMD ❌ 无法预知目标 1x

6. 笔记存档:SIMD / 预取核心概念速查

概念 解释
SIMD 单指令多数据流,一条 CPU 指令处理多个数据(如 8 个 float 同时加法)。
向量寄存器 保存多个数据的寄存器(x86: YMM/ZMM,ARM: NEON/SVE)。
编译器自动向量化 -O3 开启,编译器尝试将循环转换为 SIMD 指令(需要循环内无复杂分支)。
手写内联函数 <immintrin.h>(x86)或 arm_neon.h(ARM),获得最强性能。
硬件预取 CPU 自动检测顺序访问模式,主动加载后续数据到缓存。
软件预取 _mm_prefetch(address, hint) 指令,显式触发数据加载。
预取目标 T0→L1, T1→L2, T2→L3, NTA→非临时缓存(不污染 L3)。
适用场景 数据处理管道、音视频编解码、科学计算、矩阵运算、批量日志处理。

本节关键词:SIMD、AVX、向量化、预取、硬件预取、软件预取、缓存命中率优化。 与前期知识的关联:

  • 缓存行与 MESI:预取确保数据在 L1/L2 缓存中,减少 RFO 延迟。
  • SPSC / MPSC 批量处理:SIMD 可加速批量数据的处理(如多个检测框的同时缩放)。
  • 内存分配器:SIMD 需要内存对齐(16/32/64 字节),posix_memalign 或 aligned_alloc 分配。