mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1812 字
5 分钟
SIMD 与向量化
2026-02-08

超标量和乱序执行是指令级并行(ILP)——多条指令同时执行。SIMD 是数据级并行(DLP)——一条指令同时处理多个数据。当 ILP 的天花板逼近时,SIMD 提供了另一条性能增长路径。

一条 AVX-512 指令可以同时处理 16 个 32 位浮点数或 8 个 64 位双精度数——理论上 16 倍的加速。但实际加速比取决于数据布局、对齐、依赖链等因素。

一、SIMD 的基本原理#

1.1 标量 vs 向量#

graph LR subgraph 标量["标量处理"] S1["ADD r1, r2, r3<br/>1 对数据"] --> S2["ADD r4, r5, r6<br/>1 对数据"] S2 --> S3["ADD r7, r8, r9<br/>1 对数据"] S3 --> S4["ADD r10,r11,r12<br/>1 对数据"] end subgraph SIMD["SIMD 处理"] V1["VADD zmm0, zmm1, zmm2<br/>16 对数据同时处理!"] end style 标量 fill:#ffcdd2,stroke:#c62828 style SIMD fill:#e8f5e9,stroke:#2e7d32

1.2 SIMD 寄存器演进#

ISA寄存器宽度32 位浮点数/指令年份
x86 MMXMM0-MM764 位21997
x86 SSEXMM0-XMM15128 位41999
x86 AVXYMM0-YMM15256 位82011
x86 AVX-512ZMM0-ZMM31512 位162017
ARM NEONV0-V31128 位42005
ARM SVEZ0-Z31128-2048 位可变2016
RISC-V VV0-V31可变可变2021

1.3 SIMD 的性能潜力#

操作标量吞吐量AVX2 吞吐量AVX-512 吞吐量加速比
32 位浮点加法2/周期16/周期32/周期8-16x
32 位浮点乘法2/周期16/周期32/周期8-16x
32 位浮点 FMA2/周期16/周期32/周期8-16x
32 位整数加法2/周期16/周期32/周期8-16x
Note

实际加速比通常低于理论值,原因包括:数据未对齐、循环尾部处理、内存带宽瓶颈、AVX-512 降频等。

二、x86 SIMD 指令集#

2.1 SSE(Streaming SIMD Extensions)#

128 位寄存器(XMM),支持 4 个单精度浮点或 2 个双精度浮点:

#include <immintrin.h>
// SSE: 4 个浮点数同时加法
__m128 a = _mm_load_ps(src); // 加载 4 个 float
__m128 b = _mm_set1_ps(2.0f); // 广播 2.0 到 4 个位置
__m128 c = _mm_mul_ps(a, b); // 同时乘法
_mm_store_ps(dst, c); // 存储 4 个 float

2.2 AVX / AVX2#

256 位寄存器(YMM),支持 8 个单精度浮点或 4 个双精度浮点:

// AVX: 8 个浮点数同时加法
__m256 a = _mm256_load_ps(src); // 加载 8 个 float
__m256 b = _mm256_set1_ps(2.0f); // 广播
__m256 c = _mm256_mul_ps(a, b); // 同时乘法
_mm256_store_ps(dst, c); // 存储 8 个 float
// AVX2: 8 个 32 位整数同时加法
__m256i a = _mm256_load_si256((__m256i*)src);
__m256i b = _mm256_set1_epi32(1);
__m256i c = _mm256_add_epi32(a, b);
_mm256_store_si256((__m256i*)dst, c);

2.3 AVX-512#

512 位寄存器(ZMM),32 个寄存器,掩码寄存器(k0-k7):

// AVX-512: 16 个浮点数 + 掩码操作
__m512 a = _mm512_load_ps(src);
__m512 b = _mm512_set1_ps(2.0f);
__m512 c = _mm512_mul_ps(a, b);
// 掩码操作:只处理满足条件的元素
__mmask16 mask = _mm512_cmp_ps_mask(a, _mm512_setzero_ps(), _MM_CMPINT_GT);
// mask 的每一位表示对应元素是否 > 0
__m512 d = _mm512_mask_mul_ps(c, mask, a, b); // 只对 mask=1 的元素执行乘法
_mm512_store_ps(dst, d);

2.4 AVX-512 的降频问题#

Warning

AVX-512 指令的功耗极高,Intel CPU 在执行 AVX-512 代码时会降低频率(AVX-512 downclocking)。典型降频幅度 100-300 MHz。如果代码中 AVX-512 和标量代码混合,频繁的频率切换可能导致整体性能下降。建议:要么全程使用 AVX-512,要么不使用。

CPUAVX-512 降频AVX2 降频标量频率
Skylake-X-200~-300 MHz-100 MHz基准
Ice Lake-100~-200 MHz0基准
Sapphire Rapids~00基准

三、ARM NEON#

3.1 NEON 指令#

128 位寄存器,支持 4 个单精度浮点或 2 个双精度浮点:

#include <arm_neon.h>
// NEON: 4 个浮点数同时乘法
float32x4_t a = vld1q_f32(src); // 加载 4 个 float
float32x4_t b = vdupq_n_f32(2.0f); // 广播
float32x4_t c = vmulq_f32(a, b); // 同时乘法
vst1q_f32(dst, c); // 存储 4 个 float
// NEON: 条件选择
uint32x4_t mask = vcgtq_f32(a, vdupq_n_f32(0.0f)); // a > 0 ?
float32x4_t d = vbslq_f32(mask, c, a); // mask ? c : a

3.2 NEON vs SSE/AVX#

特性NEONSSEAVX2
寄存器宽度128 位128 位256 位
寄存器数量321616
掩码操作vbslq
FMAvfmaq_mm256_fmadd_ps
对齐要求无(但推荐对齐)16 字节32 字节

四、自动向量化#

4.1 编译器自动向量化的条件#

编译器能自动将循环向量化,但需要满足以下条件:

  1. 循环次数已知(或有明确的退出条件)
  2. 无循环携带依赖(每次迭代独立)
  3. 无函数调用(除非是编译器已知的内联函数)
  4. 简单的控制流(无复杂分支)
  5. 数据对齐(或编译器能处理非对齐)

4.2 帮助编译器自动向量化#

// 阻碍向量化的代码
void add_arrays(float *a, float *b, float *c, int n) {
for (int i = 0; i < n; i++) {
c[i] = a[i] + b[i]; // 编译器不知道 a/b/c 是否重叠
}
}
// 使用 restrict 告知编译器指针不重叠
void add_arrays(float *restrict a, float *restrict b,
float *restrict c, int n) {
for (int i = 0; i < n; i++) {
c[i] = a[i] + b[i]; // 编译器可以安全向量化
}
}
// 使用编译器提示
#pragma GCC ivdep // 告知编译器无依赖
#pragma clang loop vectorize(enable) // Clang 专用
for (int i = 0; i < n; i++) {
c[i] = a[i] + b[i];
}

4.3 检查自动向量化结果#

# GCC: 查看向量化报告
gcc -O3 -ftree-vectorize -fopt-info-vec-optimized your_code.c
# Clang: 查看向量化报告
clang -O3 -Rpass=loop-vectorize your_code.c
# 查看生成的 SIMD 指令
objdump -d your_program | grep -E "vmul|vadd|movaps|vmovaps"

五、SIMD 编程的实践技巧#

5.1 数据对齐#

// 对齐分配
float *arr = aligned_alloc(32, N * sizeof(float)); // AVX: 32 字节对齐
float *arr512 = aligned_alloc(64, N * sizeof(float)); // AVX-512: 64 字节对齐
// 对齐加载 vs 非对齐加载
__m256 a = _mm256_load_ps(aligned_ptr); // 对齐加载,快
__m256 b = _mm256_loadu_ps(unaligned_ptr); // 非对齐加载,稍慢

5.2 SoA 布局配合 SIMD#

// AoS 布局:不利于 SIMD
struct Particle {
float x, y, z, w; // 交织存储
};
Particle particles[N];
// 加载所有 x 值需要 gather 指令,效率低
// SoA 布局:SIMD 友好
struct Particles {
float x[N]; // 连续存储
float y[N];
float z[N];
float w[N];
};
// 加载 8 个 x 值:_mm256_load_ps(x + i),一次加载!

详见第 14 章:数据导向设计

5.3 循环尾部处理#

void add_arrays_avx(float *a, float *b, float *c, int n) {
int i = 0;
// AVX2: 每次处理 8 个 float
for (; i + 8 <= n; i += 8) {
__m256 va = _mm256_loadu_ps(a + i);
__m256 vb = _mm256_loadu_ps(b + i);
__m256 vc = _mm256_add_ps(va, vb);
_mm256_storeu_ps(c + i, vc);
}
// 处理剩余元素
for (; i < n; i++) {
c[i] = a[i] + b[i];
}
}

5.4 Gather/Scatter 操作#

// AVX2 Gather: 从非连续地址收集数据
// 根据索引数组从源数组中收集 8 个元素
__m256i indices = _mm256_loadu_si256((__m256i*)idx_array);
__m256 result = _mm256_i32gather_ps(src, indices, 4); // scale=4 (float 大小)
// Gather 比连续加载慢 3-5 倍,但比标量逐个加载快

六、SIMD 的性能实测#

6.1 向量加法基准测试#

#include <stdio.h>
#include <immintrin.h>
#include <time.h>
#define N 100000000
void add_scalar(float *a, float *b, float *c, int n) {
for (int i = 0; i < n; i++) c[i] = a[i] + b[i];
}
void add_avx(float *a, float *b, float *c, int n) {
int i = 0;
for (; i + 8 <= n; i += 8) {
__m256 va = _mm256_loadu_ps(a + i);
__m256 vb = _mm256_loadu_ps(b + i);
_mm256_storeu_ps(c + i, _mm256_add_ps(va, vb));
}
for (; i < n; i++) c[i] = a[i] + b[i];
}
int main() {
float *a = aligned_alloc(32, N * sizeof(float));
float *b = aligned_alloc(32, N * sizeof(float));
float *c = aligned_alloc(32, N * sizeof(float));
for (int i = 0; i < N; i++) { a[i] = 1.0f; b[i] = 2.0f; }
clock_t start = clock();
add_scalar(a, b, c, N);
clock_t end = clock();
printf("标量: %.3f\n", (double)(end - start) / CLOCKS_PER_SEC);
start = clock();
add_avx(a, b, c, N);
end = clock();
printf("AVX2: %.3f\n", (double)(end - start) / CLOCKS_PER_SEC);
free(a); free(b); free(c);
return 0;
}

6.2 典型加速比#

操作标量SSE (4x)AVX2 (8x)AVX-512 (16x)
浮点加法1.0x3.5x6.5x10-12x
浮点乘法1.0x3.5x6.5x10-12x
FMA1.0x3.8x7.0x12-14x
整数加法1.0x3.2x6.0x9-11x

实际加速比低于理论值的原因:内存带宽瓶颈、非对齐访问、循环尾部、指令延迟。

七、跨平台 SIMD 编程#

7.1 抽象层库#

支持 ISA特点
HighwaySSE/AVX/NEON/SVE/RVVGoogle 开源,C++
xsimdSSE/AVX/NEONC++ 表达式模板
VcSSE/AVX/NEONC++ 抽象
SimdjsonSSE/AVX/NEONJSON 解析专用

7.2 使用 Highway 的示例#

#include <hwy/highway.h>
namespace hw = hwy::HWY_NAMESPACE;
void add_arrays(const float* a, const float* b, float* c, int n) {
const int lanes = hw::Lanes(hw::FloatTag());
for (int i = 0; i + lanes <= n; i += lanes) {
auto va = hw::Load(hw::FloatTag(), a + i);
auto vb = hw::Load(hw::FloatTag(), b + i);
auto vc = hw::Add(va, vb);
hw::Store(vc, hw::FloatTag(), c + i);
}
// 处理尾部...
}

八、动手实验#

8.1 实验 1:编译器自动向量化#

# 编译并查看向量化报告
gcc -O3 -mavx2 -ftree-vectorize -fopt-info-vec-optimized test.c
# 查看生成的指令
gcc -O3 -mavx2 -S test.c -o test.s
grep -c "vmulps\|vaddps\|vmovaps" test.s

8.2 实验 2:SIMD vs 标量性能#

# 编译 SIMD 基准测试
gcc -O3 -mavx2 -o simd_test simd_test.c
perf stat -e cycles,instructions,flops_any ./simd_test

8.3 实验 3:查看 CPU 支持的 SIMD 扩展#

# Linux
cat /proc/cpuinfo | grep flags | head -1
# 查找 sse4_1, avx, avx2, avx512f 等标志
# 或使用
lscpu | grep -i flags

九、AVX-512 深入解析#

9.1 AVX-512 的掩码寄存器#

AVX-512 引入了 8 个专用的掩码寄存器 k0-k7,每个 16 位宽。这是 AVX-512 相比 AVX2 最重要的架构创新:

k0 = 0b1111111111111111 ← 全选(16 个元素全部操作)
k1 = 0b0000000011111111 ← 只操作低 8 个元素
k2 = 0b1010101010101010 ← 只操作偶数位元素

掩码操作的优势:一条指令完成条件操作,不需要分支

// AVX2:条件加法需要分支或 blend
__m256 a = _mm256_loadu_ps(src);
__m256 b = _mm256_loadu_ps(threshold);
__m256 mask = _mm256_cmp_ps(a, b, _CMP_GT_OS); // 比较结果为全 1 或全 0
__m256 result = _mm256_blendv_ps(a, _mm256_add_ps(a, b), mask); // blend 选择
// AVX-512:掩码直接嵌入操作
__m512 a = _mm512_loadu_ps(src);
__m512 b = _mm512_loadu_ps(threshold);
__mmask16 k = _mm512_cmp_ps_mask(a, b, _MM_CMPINT_GT); // 16 位掩码
__m512 result = _mm512_mask_add_ps(a, k, a, b); // k=1 的位置加法,k=0 保留原值

9.2 AVX-512 的子集扩展#

AVX-512 不是单一扩展,而是多个子集的组合:

子集功能支持的 CPU
AVX-512F基础指令(加减乘除、比较、blend)Skylake-X, Ice Lake
AVX-512BW字节/字操作(8/16 位整数)Skylake-X
AVX-512DQ双字/四字操作(32/64 位整数)Skylake-X
AVX-512VL128/256 位向量支持Skylake-X
AVX-512VNNI神经网络指令(点积加速)Cooper Lake, Sapphire Rapids
AVX-512BF16BFloat16 支持Sapphire Rapids
Note

检查 AVX-512 支持时,需要检查具体的子集。cat /proc/cpuinfo | grep avx512f 只检查基础支持。

9.3 AVX-512 降频的深入分析#

AVX-512 的功耗极高,因为 512 位运算需要同时激活更多的执行单元和寄存器文件。Intel CPU 使用许可证(License)机制控制频率:

stateDiagram-v2 [*] --> L0: 标量/轻量指令 L0 --> L1: AVX2 指令 L1 --> L2: AVX-512 指令 L2 --> L1: AVX-512 指令结束 L1 --> L0: AVX2 指令结束 note right of L0: 全频率 (如 3.5 GHz) note right of L1: -100 MHz (如 3.4 GHz) note right of L2: -200~300 MHz (如 3.2 GHz)
CPU 代AVX-512 降频恢复延迟建议
Skylake-X-300 MHz~2ms全程 AVX-512 或不用
Ice Lake-200 MHz~1ms可混合使用
Sapphire Rapids~0~0自由使用

十、自动向量化的编译器提示#

10.1 帮助编译器向量化的 pragma#

// GCC: 告知编译器无循环依赖
#pragma GCC ivdep
for (int i = 0; i < n; i++) {
a[i] = b[i] + c[i];
}
// GCC: 指定向量化宽度
#pragma GCC vector aligned
#pragma GCC unroll 4
for (int i = 0; i < n; i++) {
a[i] = b[i] * c[i];
}
// Clang: 显式启用/禁用向量化
#pragma clang loop vectorize(enable)
#pragma clang loop interleave(enable)
for (int i = 0; i < n; i++) {
a[i] = b[i] + c[i];
}
// OpenMP: 便携的向量化提示
#pragma omp simd
for (int i = 0; i < n; i++) {
a[i] = b[i] + c[i];
}
// OpenMP: 指定 SIMD 宽度
#pragma omp simd simdlen(8)
for (int i = 0; i < n; i++) {
a[i] = b[i] + c[i];
}

10.2 阻碍向量化的常见原因#

// 原因 1:指针别名(编译器不确定 a/b/c 是否重叠)
void add(float *a, float *b, float *c, int n) {
for (int i = 0; i < n; i++)
a[i] = b[i] + c[i]; // 编译器可能不向量化
}
// 使用 restrict 消除别名
void add(float *restrict a, float *restrict b,
float *restrict c, int n) {
for (int i = 0; i < n; i++)
a[i] = b[i] + c[i]; // 编译器可以安全向量化
}
// 原因 2:循环携带依赖
for (int i = 1; i < n; i++) {
a[i] = a[i-1] + b[i]; // a[i] 依赖 a[i-1],无法向量化
}
// 原因 3:复杂控制流
for (int i = 0; i < n; i++) {
if (complex_condition(a[i])) { // 不可预测的分支
a[i] = b[i] + c[i];
} else {
a[i] = b[i] - c[i];
}
}
// 用数学运算替代分支
for (int i = 0; i < n; i++) {
int mask = (a[i] > 0); // 0 或 1
a[i] = b[i] + (2 * mask - 1) * c[i]; // 无分支
}

10.3 查看向量化报告#

# GCC: 优化信息
gcc -O3 -mavx2 -ftree-vectorize -fopt-info-vec-optimized test.c
# 输出: test.c:8:3: note: loop vectorized
# GCC: 查看未向量化的原因
gcc -O3 -mavx2 -ftree-vectorize -fopt-info-vec-missed test.c
# 输出: test.c:15:3: missed: not vectorized: loop carried dependency
# Clang: 向量化报告
clang -O3 -mavx2 -Rpass=loop-vectorize test.c
# 输出: test.c:8:3: remark: vectorized loop (vectorization width: 8)
# Clang: 查看失败原因
clang -O3 -mavx2 -Rpass-missed=loop-vectorize test.c

十一、SIMD 优化的常见陷阱#

11.1 陷阱汇总#

陷阱原因解决方案
非对齐访问性能损失 2-3xaligned_alloc + _mm256_load_ps
混合域转换整数↔浮点转换代价高避免频繁转换,统一数据类型
部分寄存器停顿只使用 YMM 低 128 位避免混用 SSE 和 AVX 指令
Gather/Scatter非连续访问,3-5x 慢于连续加载SoA 布局避免 Gather
AVX-512 降频高功耗导致频率降低全程 AVX-512 或不用
循环尾部剩余元素标量处理掩码操作处理尾部

11.2 SSE/AVX 混合使用的惩罚#

// 混合使用 SSE 和 AVX 指令导致部分寄存器停顿
__m128 a = _mm_load_ps(src); // 只写 XMM 低 128 位
__m256 b = _mm256_load_ps(src2); // 使用完整 YMM 256 位
// CPU 需要插入 vzeroupper 指令清除 YMM 高位
// 如果不插入,后续 AVX 指令会有 2-3 周期停顿
// 统一使用 AVX 指令
__m256 a = _mm256_load_ps(src); // 统一 256 位
__m256 b = _mm256_load_ps(src2);
# 检查是否有 vzeroupper 插入
objdump -d your_program | grep vzeroupper
# 如果大量出现,说明有 SSE/AVX 混合
Warning

现代 CPU(Zen 2+ / Ice Lake+)已经消除了 SSE/AVX 转换惩罚,但在旧 CPU 上这仍然是一个重要问题。如果你的代码需要在 Skylake 或 Zen 1 上运行,务必避免混合使用。

graph LR SCALAR["标量处理<br/>1次1个元素"] --> SIMD2["SIMD 处理<br/>1次N个元素"] SIMD2 --> REG["SIMD 寄存器<br/>128/256/512 bit"] REG --> OP["SIMD 指令<br/>addps/vaddps"] style SCALAR fill:#ffcdd2,stroke:#c62828 style SIMD2 fill:#c8e6c9,stroke:#2e7d32
flowchart LR LOOP["标量循环<br/>for i in 0..N"] --> VEC["向量化循环<br/>for i in 0..N;step=4"] VEC --> LOAD["vload 4元素"] --> VOP["SIMD 运算"] --> STORE["vstore 4元素"] LOAD --> VOP --> STORE style VEC fill:#bbdefb,stroke:#1565c0 style VOP fill:#c8e6c9,stroke:#2e7d32

十二、小结#

上一章深入解读了内存排序与内存屏障的内部机制。

概念要点对软件的影响
SSE/AVX/AVX-512x86 SIMD 扩展128/256/512 位向量处理
NEONARM SIMD 扩展128 位向量处理
掩码操作AVX-512 的 k 寄存器条件 SIMD 操作
自动向量化编译器自动生成 SIMDrestrict + 简单循环
数据对齐16/32/64 字节对齐对齐加载更快
SoA 布局SIMD 友好的数据布局与 Ch14 配合
AVX-512 降频高功耗导致频率降低混合代码需谨慎

下一步TLB 与页表——虚拟地址如何翻译为物理地址?TLB 未命中的代价有多大?Huge Pages 如何提升性能?

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

SIMD 与向量化
https://blog.souloss.com/posts/cpu-architecture/simd-vectorization/
作者
Souloss
发布于
2026-02-08
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时