网站怎么做反链,精准资料网,企业网站建设公司丰台,海口哪里做网站公司序章#xff1a;一个让你秒懂SIMD的故事 想象你是一个高考阅卷老师。┌══════════════════════════════════════════════════════════┐ ║ ║ ║ 传统…序章一个让你秒懂SIMD的故事想象你是一个高考阅卷老师。 ┌══════════════════════════════════════════════════════════┐ ║ ║ ║ 传统方式标量运算 ║ ║ ║ ║ 你面前有一摞试卷。 ║ ║ 你拿起第1张判分放下。 ║ ║ 你拿起第2张判分放下。 ║ ║ 你拿起第3张判分放下。 ║ ║ 你拿起第4张判分放下。 ║ ║ ... ║ ║ 1000张试卷你判了1000次。 ║ ║ ║ ║ ║ ║ SIMD方式向量运算 ║ ║ ║ ║ 你突然长出了4只手别害怕这是超能力。 ║ ║ 你同时拿起4张试卷。 ║ ║ 4只手同时判分。 ║ ║ 同时放下4张。 ║ ║ 同时拿起下4张。 ║ ║ ... ║ ║ 1000张试卷你只判了250次。 ║ ║ 速度快了4倍但你的判分动作只做了250次。 ║ ║ ║ ║ 如果你长出8只手 125次。快8倍。 ║ ║ 如果你长出16只手 63次。 快16倍。 ║ ║ ║ ║ 这就是SIMD ║ ║ Single Instruction, Multiple Data ║ ║ 一条指令同时处理多个数据。 ║ ║ ║ ╚══════════════════════════════════════════════════════════╝第一章CPU里的宽车道┌══════════════════════════════════════════════════════════┐ ║ ║ ║ 要理解SIMD先看看CPU里的寄存器。 ║ ║ ║ ║ 普通寄存器标量 ║ ║ ┌────────────────────────────────┐ ║ ║ │ 32位 1个float │ ║ ║ │ 例如3.14159 │ ║ ║ └────────────────────────────────┘ ║ ║ 一次只能装一个浮点数。 ║ ║ 就像一条单车道公路一次只能过一辆车。 ║ ║ ║ ║ ║ ║ SSE寄存器128位 ║ ║ ┌────────┬────────┬────────┬────────┐ ║ ║ │ float0 │ float1 │ float2 │ float3 │ ║ ║ │ 3.14 │ 2.71 │ 1.41 │ 1.73 │ ║ ║ └────────┴────────┴────────┴────────┘ ║ ║ 128位 4个32位float并排坐。 ║ ║ 就像一条4车道高速公路4辆车同时通过。 ║ ║ ║ ║ ║ ║ AVX寄存器256位 ║ ║ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ ║ ║ │ f0 │ f1 │ f2 │ f3 │ f4 │ f5 │ f6 │ f7 │ ║ ║ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ ║ ║ 256位 8个float并排坐。 ║ ║ 8车道高速公路。 ║ ║ ║ ║ ║ ║ AVX-512寄存器512位 ║ ║ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ ║ ║ │f0│f1│f2│f3│f4│f5│f6│f7│f8│f9│fA│fB│fC│fD│fE│fF│ ║ ║ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ ║ ║ 512位 16个float并排坐。 ║ ║ 16车道超级高速公路。 ║ ║ ║ ║ ║ ║ 进化时间线 ║ ║ ║ ║ 1999年 ──── SSE ────── 128位 ── 4路float ── Intel ║ ║ │ ║ ║ 2011年 ──── AVX ────── 256位 ── 8路float ── Intel ║ ║ │ ║ ║ 2016年 ──── AVX-512 ── 512位 ── 16路float ── Intel ║ ║ │ ║ ║ ARM端 ──── NEON ───── 128位 ── 4路float ── 手机/主机 ║ ║ ║ ║ ║ ║ 关键认知 ║ ║ 这些宽寄存器不是更快的寄存器。 ║ ║ 它们是更宽的寄存器。 ║ ║ 速度没变但一次搬运的货物量翻了4-16倍。 ║ ║ ║ ║ 就像 ║ ║ 卡车的速度和轿车一样都是100km/h ║ ║ 但卡车一次拉的货是轿车的16倍。 ║ ║ 总运输效率 速度 × 载货量。 ║ ║ SIMD提升的是载货量。 ║ ║ ║ ╚══════════════════════════════════════════════════════════╝第二章第一个SIMD程序——从加法开始┌══════════════════════════════════════════════════════════┐ ║ ║ ║ 任务把两个数组对应元素相加 ║ ║ C[i] A[i] B[i]共1024个元素 ║ ║ ║ ║ ║ ║ ═══ 标量版本一次一个═══ ║ ║ ║ ║ for (int i 0; i 1024; i) { ║ ║ C[i] A[i] B[i]; ║ ║ } ║ ║ ║ ║ CPU内部发生了什么 ║ ║ ║ ║ 第1次循环 ║ ║ ┌──────┐ ┌──────┐ ┌──────┐ ║ ║ │ A[0] │ │ B[0] │ │ C[0] │ ║ ║ │ 3.0 │ │ 1.0 │ │ 4.0 │ ║ ║ └──────┘ └──────┘ └──────┘ ║ ║ ║ ║ 第2次循环 ║ ║ ┌──────┐ ┌──────┐ ┌──────┐ ║ ║ │ A[1] │ │ B[1] │ │ C[1] │ ║ ║ │ 5.0 │ │ 2.0 │ │ 7.0 │ ║ ║ └──────┘ └──────┘ └──────┘ ║ ║ ║ ║ ...重复1024次。 ║ ║ 每次循环1次加载A1次加载B1次加法1次存储C。 ║ ║ 总共1024次加法操作。 ║ ║ ║ ║ ║ ║ ═══ SSE版本一次四个═══ ║ ║ ║ ║ #include xmmintrin.h // SSE头文件 ║ ║ ║ ║ for (int i 0; i 1024; i 4) { ║ ║ __m128 a _mm_load_ps(A[i]); // 一次读4个A ║ ║ __m128 b _mm_load_ps(B[i]); // 一次读4个B ║ ║ __m128 c _mm_add_ps(a, b); // 一次加4对 ║ ║ _mm_store_ps(C[i], c); // 一次存4个C ║ ║ } ║ ║ ║ ║ CPU内部发生了什么 ║ ║ ║ ║ 第1次循环处理i0,1,2,3 ║ ║ ┌─────┬─────┬─────┬─────┐ ║ ║ │A[0] │A[1] │A[2] │A[3] │ ← 一次读4个 ║ ║ │ 3.0 │ 5.0 │ 2.0 │ 8.0 │ ║ ║ └─────┴─────┴─────┴─────┘ ║ ║ ← 一条指令4个加法 ║ ║ ┌─────┬─────┬─────┬─────┐ ║ ║ │B[0] │B[1] │B[2] │B[3] │ ← 一次读4个 ║ ║ │ 1.0 │ 2.0 │ 3.0 │ 4.0 │ ║ ║ └─────┴─────┴─────┴─────┘ ║ ║ ║ ║ ┌─────┬─────┬─────┬─────┐ ║ ║ │C[0] │C[1] │C[2] │C[3] │ ← 一次存4个 ║ ║ │ 4.0 │ 7.0 │ 5.0 │12.0 │ ║ ║ └─────┴─────┴─────┴─────┘ ║ ║ ║ ║ 总共256次循环1024÷4256次加法操作。 ║ ║ 快了4倍。 ║ ║ ║ ║ ║ ║ ═══ AVX版本一次八个═══ ║ ║ ║ ║ #include immintrin.h // AVX头文件 ║ ║ ║ ║ for (int i 0; i 1024; i 8) { ║ ║ __m256 a _mm256_load_ps(A[i]); // 一次读8个 ║ ║ __m256 b _mm256_load_ps(B[i]); // 一次读8个 ║ ║ __m256 c _mm256_add_ps(a, b); // 一次加8对 ║ ║ _mm256_store_ps(C[i], c); // 一次存8个 ║ ║ } ║ ║ ║ ║ 总共128次循环128次加法操作。快了8倍。 ║ ║ ║ ║ ║ ║ 命名规则解密 ║ ║ ┌──────────────────────────────────────────────────┐ ║ ║ │ _mm_add_ps │ ║ ║ │ │ │ │ │ ║ ║ │ │ │ └── ps Packed Single-precision │ ║ ║ │ │ │ (打包的单精度浮点) │ ║ ║ │ │ └── add 加法操作 │ ║ ║ │ └── mm 多媒体扩展历史命名 │ ║ ║ │ │ ║ ║ │ _mm256_mul_ps │ ║ ║ │ │ │ │ │ ║ ║ │ │ │ └── ps 单精度浮点 │ ║ ║ │ │ └── mul 乘法 │ ║ ║ │ └── mm256 256位寄存器AVX │ ║ ║ │ │ ║ ║ │ 常见后缀 │ ║ ║ │ ps packed single (4/8个float) │ ║ ║ │ pd packed double (2/4个double) │ ║ ║ │ epi32 packed 32-bit integer (4/8个int) │ ║ ║ │ ss scalar single (1个float不并行) │ ║ ║ │ │ ║ ║ └──────────────────────────────────────────────────┘ ║ ║ ║ ╚══════════════════════════════════════════════════════════╝第三章SIMD能做什么运算┌══════════════════════════════════════════════════════════┐ ║ ║ ║ SIMD不只是加法。它几乎能做所有基础运算。 ║ ║ 每一种运算都是一条指令同时处理一批数据。 ║ ║ ║ ║ ┌─────────────────────────────────────────────────┐ ║ ║ │ │ ║ ║ │ 算术运算以SSE 128位 4个float为例 │ ║ ║ │ ════════════════════════════════════ │ ║ ║ │ │ ║ ║ │ 加法_mm_add_ps(a, b) │ ║ ║ │ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐│ ║ ║ │ │3 │5 │2 │8 │ │1 │2 │3 │4 │ │4 │7 │5 │12││ ║ ║ │ └──┴──┴──┴──┘ └──┴──┴──┴──┘ └──┴──┴──┴──┘│ ║ ║ │ │ ║ ║ │ 减法_mm_sub_ps(a, b) │ ║ ║ │ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐│ ║ ║ │ │3 │5 │2 │8 │ - │1 │2 │3 │4 │ │2 │3 │-1│4 ││ ║ ║ │ └──┴──┴──┴──┘ └──┴──┴──┴──┘ └──┴──┴──┴──┘│ ║ ║ │ │ ║ ║ │ 乘法_mm_mul_ps(a, b) │ ║ ║ │ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐ ┌──┬──┬──┬───┐│ ║ ║ │ │3 │5 │2 │8 │ × │1 │2 │3 │4 │ │3 │10│6 │32 ││ ║ ║ │ └──┴──┴──┴──┘ └──┴──┴──┴──┘ └──┴──┴──┴───┘│ ║ ║ │ │ ║ ║ │ 除法_mm_div_ps(a, b) │ ║ ║ │ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐ ┌──┬──┬───┬──┐│ ║ ║ │ │8 │9 │6 │5 │ ÷ │2 │3 │4 │5 │ │4 │3 │1.5│1 ││ ║ ║ │ └──┴──┴──┴──┘ └──┴──┴──┴──┘ └──┴──┴───┴──┘│ ║ ║ │ │ ║ ║ │ 平方根_mm_sqrt_ps(a) │ ║ ║ │ ┌──┬──┬───┬───┐ ┌──┬──┬────┬────┐ │ ║ ║ │ │4 │9 │16 │25 │ √→ │2 │3 │ 4 │ 5 │ │ ║ ║ │ └──┴──┴───┴───┘ └──┴──┴────┴────┘ │ ║ ║ │ │ ║ ║ │ 取最大值_mm_max_ps(a, b) │ ║ ║ │ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐│ ║ ║ │ │3 │5 │2 │8 │max│7 │1 │9 │4 │ │7 │5 │9 │8 ││ ║ ║ │ └──┴──┴──┴──┘ └──┴──┴──┴──┘ └──┴──┴──┴──┘│ ║ ║ │ │ ║ ║ │ 取最小值_mm_min_ps(a, b) │ ║ ║ │ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐│ ║ ║ │ │3 │5 │2 │8 │min│7 │1 │9 │4 │ │3 │1 │2 │4 ││ ║ ║ │ └──┴──┴──┴──┘ └──┴──┴──┴──┘ └──┴──┴──┴──┘│ ║ ║ │ │ ║ ║ │ │ ║ ║ │ FMA乘加融合_mm_fmadd_ps(a, b, c) │ ║ ║ │ result a × b c一条指令完成乘法和加法 │ ║ ║ │ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐│ ║ ║ │ │2 │3 │4 │5 │ × │3 │2 │1 │2 │ │1 │1 │1 │1 ││ ║ ║ │ └──┴──┴──┴──┘ └──┴──┴──┴──┘ └──┴──┴──┴──┘│ ║ ║ │ │ ║ ║ │ ┌──┬──┬──┬───┐ │ ║ ║ │ │7 │7 │5 │11 │ (2×31, 3×21, 4×11, 5×21) │ ║ ║ │ └──┴──┴──┴───┘ │ ║ ║ │ │ ║ ║ │ FMA为什么重要 │ ║ ║ │ ├── 一条指令代替两条乘法加法 │ ║ ║ │ ├── 只有一次舍入误差而不是两次 │ ║ ║ │ ├── 延迟更低5周期 vs 乘法4加法37周期 │ ║ ║ │ └── 矩阵乘法、点积、多项式求值全靠它 │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ 比较运算_mm_cmpgt_ps(a, b) │ ║ ║ │ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐ │ ║ ║ │ │3 │5 │2 │8 │ │7 │1 │9 │4 │ │ ║ ║ │ └──┴──┴──┴──┘ └──┴──┴──┴──┘ │ ║ ║ │ │ ║ ║ │ ┌────────┬────────┬────────┬────────┐ │ ║ ║ │ │00000000│FFFFFFFF│00000000│FFFFFFFF│ │ ║ ║ │ │ false │ true │ false │ true │ │ ║ ║ │ └────────┴────────┴────────┴────────┘ │ ║ ║ │ 结果不是0/1而是全0或全F的掩码。 │ ║ ║ │ 这个掩码可以直接用于后续的位运算选择。 │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ 混合选择_mm_blendv_ps(a, b, mask) │ ║ ║ │ 根据掩码从a或b中选择 │ ║ ║ │ mask为0选amask为F选b │ ║ ║ │ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐ │ ║ ║ │ │3 │5 │2 │8 │ │7 │1 │9 │4 │ │0 │F │0 │F │ │ ║ ║ │ └──┴──┴──┴──┘ └──┴──┴──┴──┘ └──┴──┴──┴──┘ │ ║ ║ │ 结果 │ ║ ║ │ ┌──┬──┬──┬──┐ │ ║ ║ │ │3 │1 │2 │4 │ ← 掩码0选a掩码F选b │ ║ ║ │ └──┴──┴──┴──┘ │ ║ ║ │ │ ║ ║ │ 这就是SIMD版的无分支if-else │ ║ ║ │ 不需要跳转不需要分支预测。 │ ║ ║ │ 比较混合 无分支条件选择。 │ ║ ║ │ │ ║ ║ └─────────────────────────────────────────────────┘ ║ ║ ║ ╚══════════════════════════════════════════════════════════╝第四章游戏中的SIMD实战实战一3D向量归一化┌══════════════════════════════════════════════════════════┐ ║ ║ ║ 归一化 把向量变成长度为1的单位向量 ║ ║ 公式v_normalized v / length(v) ║ ║ length(v) sqrt(x² y² z²) ║ ║ ║ ║ 游戏中每帧要归一化成千上万个向量 ║ ║ 法线、方向、速度、光线... ║ ║ ║ ║ ║ ║ 标量版本一次处理1个向量 ║ ║ ┌─────────────────────────────────────────────────┐ ║ ║ │ │ ║ ║ │ void normalize(float* vx, float* vy, float* vz,│ ║ ║ │ int count) { │ ║ ║ │ for (int i 0; i count; i) { │ ║ ║ │ float x vx[i], y vy[i], z vz[i]; │ ║ ║ │ float len sqrtf(x*x y*y z*z); │ ║ ║ │ float inv 1.0f / len; │ ║ ║ │ vx[i] x * inv; │ ║ ║ │ vy[i] y * inv; │ ║ ║ │ vz[i] z * inv; │ ║ ║ │ } │ ║ ║ │ } │ ║ ║ │ │ ║ ║ │ 每个向量需要 │ ║ ║ │ 3次乘法 2次加法 1次sqrt 1次除法 3次乘法│ ║ ║ │ 10次浮点运算 │ ║ ║ │ 其中sqrt约15周期除法约25周期。 │ ║ ║ │ 10000个向量 ≈ 很慢。 │ ║ ║ │ │ ║ ║ └─────────────────────────────────────────────────┘ ║ ║ ║ ║ ║ ║ SSE版本一次处理4个向量 ║ ║ ┌─────────────────────────────────────────────────┐ ║ ║ │ │ ║ ║ │ void normalize_sse(float* vx, float* vy, │ ║ ║ │ float* vz, int count) { │ ║ ║ │ for (int i 0; i count; i 4) { │ ║ ║ │ │ ║ ║ │ // 一次加载4个向量的x/y/z分量 │ ║ ║ │ __m128 x _mm_load_ps(vx[i]); │ ║ ║ │ __m128 y _mm_load_ps(vy[i]); │ ║ ║ │ __m128 z _mm_load_ps(vz[i]); │ ║ ║ │ │ ║ ║ │ // 同时算4个 x²y²z² │ ║ ║ │ __m128 len_sq _mm_mul_ps(x, x); │ ║ ║ │ len_sq _mm_add_ps(len_sq, │ ║ ║ │ _mm_mul_ps(y, y)); │ ║ ║ │ len_sq _mm_add_ps(len_sq, │ ║ ║ │ _mm_mul_ps(z, z)); │ ║ ║ │ │ ║ ║ │ // 关键黑科技快速倒数平方根 │ ║ ║ │ // rsqrt 1/sqrt(x)一条指令搞定 │ ║ ║ │ __m128 inv_len _mm_rsqrt_ps(len_sq); │ ║ ║ │ │ ║ ║ │ // 精度不够做一次牛顿迭代 │ ║ ║ │ // 精度从11位提升到22位接近完美 │ ║ ║ │ __m128 half _mm_set1_ps(0.5f); │ ║ ║ │ __m128 three _mm_set1_ps(3.0f); │ ║ ║ │ __m128 muls _mm_mul_ps( │ ║ ║ │ _mm_mul_ps(len_sq, inv_len), │ ║ ║ │ inv_len); │ ║ ║ │ inv_len _mm_mul_ps( │ ║ ║ │ _mm_mul_ps(half, inv_len), │ ║ ║ │ _mm_sub_ps(three, muls)); │ ║ ║ │ │ ║ ║ │ // 同时归一化4个向量 │ ║ ║ │ vx[i..i3] x * inv_len │ ║ ║ │ _mm_store_ps(vx[i], │ ║ ║ │ _mm_mul_ps(x, inv_len)); │ ║ ║ │ _mm_store_ps(vy[i], │ ║ ║ │ _mm_mul_ps(y, inv_len)); │ ║ ║ │ _mm_store_ps(vz[i], │ ║ ║ │ _mm_mul_ps(z, inv_len)); │ ║ ║ │ } │ ║ ║ │ } │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ 图解这段代码在CPU里的样子 │ ║ ║ │ │ ║ ║ │ 4个向量同时处理 │ ║ ║ │ │ ║ ║ │ 向量0: (1, 0, 0) → 长度1 → (1, 0, 0) │ ║ ║ │ 向量1: (3, 4, 0) → 长度5 → (0.6, 0.8, 0) │ ║ ║ │ 向量2: (0, 0, 5) → 长度5 → (0, 0, 1) │ ║ ║ │ 向量3: (1, 1, 1) → 长度√3 → (0.577...) │ ║ ║ │ │ ║ ║ │ x寄存器: ┌───┬───┬───┬───┐ │ ║ ║ │ │ 1 │ 3 │ 0 │ 1 │ │ ║ ║ │ └───┴───┴───┴───┘ │ ║ ║ │ y寄存器: ┌───┬───┬───┬───┐ │ ║ ║ │ │ 0 │ 4 │ 0 │ 1 │ │ ║ ║ │ └───┴───┴───┴───┘ │ ║ ║ │ z寄存器: ┌───┬───┬───┬───┐ │ ║ ║ │ │ 0 │ 0 │ 5 │ 1 │ │ ║ ║ │ └───┴───┴───┴───┘ │ ║ ║ │ │ ║ ║ │ len_sq: ┌───┬────┬────┬───┐ │ ║ ║ │ │ 1 │ 25 │ 25 │ 3 │ ← 4个长度²同时算 │ ║ ║ │ └───┴────┴────┴───┘ │ ║ ║ │ │ ║ ║ │ inv_len: ┌───┬─────┬─────┬──────┐ │ ║ ║ │ │ 1 │ 0.2 │ 0.2 │0.577 │ ← 4个1/√x │ ║ ║ │ └───┴─────┴─────┴──────┘ │ ║ ║ │ │ ║ ║ │ 全程没有分支没有除法没有标准sqrt。 │ ║ ║ │ rsqrt 牛顿迭代 ≈ 8个周期4个向量 │ ║ ║ │ 标准 sqrt div ≈ 40个周期1个向量 │ ║ ║ │ 加速比40×4 / 8 20倍 │ ║ ║ │ │ ║ ║ └─────────────────────────────────────────────────┘ ║ ║ ║ ╚══════════════════════════════════════════════════════════╝实战二粒子系统更新┌══════════════════════════════════════════════════════════┐ ║ ║ ║ 场景10000个粒子每帧更新位置和生命值 ║ ║ ║ ║ 每个粒子的更新逻辑 ║ ║ position velocity × deltaTime ║ ║ life - deltaTime ║ ║ if (life 0) → 标记为死亡 ║ ║ ║ ║ ║ ║ ═══ 数据布局SoAStructure of Arrays═══ ║ ║ ║ ║ 为什么不用AoS回顾一下 ║ ║ ║ ║ AoS不好 ║ ║ struct Particle { float x,y,z, vx,vy,vz, life; }; ║ ║ Particle particles[10000]; ║ ║ ║ ║ 内存中[x y z vx vy vz life] [x y z vx vy vz life]... ║ ║ 读4个粒子的x要跳过y,z,vx,vy,vz,life才能读下一个x。 ║ ║ SIMD没法一次读4个连续的x因为它们不连续。 ║ ║ ║ ║ SoA好 ║ ║ float px[10000]; // 所有粒子的x ║ ║ float py[10000]; // 所有粒子的y ║ ║ float pz[10000]; // 所有粒子的z ║ ║ float vx[10000]; // 所有粒子的vx ║ ║ float vy[10000]; // 所有粒子的vy ║ ║ float vz[10000]; // 所有粒子的vz ║ ║ float life[10000]; // 所有粒子的生命值 ║ ║ ║ ║ 内存中[x x x x x x x x x x ...] ← 连续完美 ║ ║ SIMD一次读4个x它们紧挨着缓存友好。 ║ ║ ║ ║ ║ ║ ═══ AVX版本一次处理8个粒子═══ ║ ║ ║ ║ ┌─────────────────────────────────────────────────┐ ║ ║ │ │ ║ ║ │ void update_particles_avx( │ ║ ║ │ float* px, float* py, float* pz, │ ║ ║ │ float* vx, float* vy, float* vz, │ ║ ║ │ float* life, int* alive, │ ║ ║ │ float dt, int count) │ ║ ║ │ { │ ║ ║ │ // 把deltaTime广播到8个槽位 │ ║ ║ │ __m256 vdt _mm256_set1_ps(dt); │ ║ ║ │ // ┌────┬────┬────┬────┬────┬────┬────┬────┐│ ║ ║ │ // │ dt │ dt │ dt │ dt │ dt │ dt │ dt │ dt ││ ║ ║ │ // └────┴────┴────┴────┴────┴────┴────┴────┘│ ║ ║ │ │ ║ ║ │ __m256 zero _mm256_setzero_ps(); │ ║ ║ │ │ ║ ║ │ for (int i 0; i count; i 8) { │ ║ ║ │ │ ║ ║ │ // ═══ 更新位置 ═══ │ ║ ║ │ // position velocity × dt │ ║ ║ │ │ ║ ║ │ __m256 x _mm256_load_ps(px[i]); │ ║ ║ │ __m256 dx _mm256_load_ps(vx[i]); │ ║ ║ │ x _mm256_fmadd_ps(dx, vdt, x); │ ║ ║ │ // x dx * dt x FMA一条指令 │ ║ ║ │ _mm256_store_ps(px[i], x); │ ║ ║ │ │ ║ ║ │ __m256 y _mm256_load_ps(py[i]); │ ║ ║ │ __m256 dy _mm256_load_ps(vy[i]); │ ║ ║ │ y _mm256_fmadd_ps(dy, vdt, y); │ ║ ║ │ _mm256_store_ps(py[i], y); │ ║ ║ │ │ ║ ║ │ __m256 z _mm256_load_ps(pz[i]); │ ║ ║ │ __m256 dz _mm256_load_ps(vz[i]); │ ║ ║ │ z _mm256_fmadd_ps(dz, vdt, z); │ ║ ║ │ _mm256_store_ps(pz[i], z); │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ // ═══ 更新生命值 ═══ │ ║ ║ │ // life - dt │ ║ ║ │ │ ║ ║ │ __m256 l _mm256_load_ps(life[i]); │ ║ ║ │ l _mm256_sub_ps(l, vdt); │ ║ ║ │ _mm256_store_ps(life[i], l); │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ // ═══ 判断是否死亡无分支═══ │ ║ ║ │ // if (life 0) alive 0 │ ║ ║ │ │ ║ ║ │ __m256 mask _mm256_cmp_ps( │ ║ ║ │ l, zero, _CMP_GT_OQ); │ ║ ║ │ // mask: 活着的位置0xFFFFFFFF │ ║ ║ │ // 死亡的位置0x00000000 │ ║ ║ │ │ ║ ║ │ // 把浮点掩码转成整数掩码存入alive数组 │ ║ ║ │ __m256i imask _mm256_castps_si256(mask);│ ║ ║ │ _mm256_store_si256( │ ║ ║ │ (__m256i*)alive[i], imask); │ ║ ║ │ │ ║ ║ │ // 8个粒子的生死判断零分支完成。 │ ║ ║ │ } │ ║ ║ │ } │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ 性能对比 │ ║ ║ │ ┌──────────────────┬──────────┬──────────┐ │ ║ ║ │ │ 版本 │ 10000粒子│ 加速比 │ │ ║ ║ │ ├──────────────────┼──────────┼──────────┤ │ ║ ║ │ │ 标量AoS │ 0.82ms │ 1x │ │ ║ ║ │ │ 标量SoA │ 0.35ms │ 2.3x │ │ ║ ║ │ │ SSESoA │ 0.11ms │ 7.5x │ │ ║ ║ │ │ AVXSoAFMA │ 0.06ms │ 13.7x │ │ ║ ║ │ └──────────────────┴──────────┴──────────┘ │ ║ ║ │ │ ║ ║ │ 注意从AoS到SoA就快了2.3倍纯数据布局优化。│ ║ ║ │ 再加上SIMD又快了6倍。 │ ║ ║ │ 数据布局 SIMD 双重加速。 │ ║ ║ │ │ ║ ║ └─────────────────────────────────────────────────┘ ║ ║ ║ ╚══════════════════════════════════════════════════════════╝实战三4×4矩阵乘法┌══════════════════════════════════════════════════════════┐ ║ ║ ║ 矩阵乘法是3D游戏的心脏。 ║ ║ 每个物体每帧至少做一次矩阵乘法模型→世界→视图→投影。 ║ ║ 1000个物体 1000次4×4矩阵乘法。 ║ ║ ║ ║ 4×4矩阵乘法需要64次乘法 48次加法 112次浮点运算。 ║ ║ ║ ║ ║ ║ SIMD的思路把矩阵的每一行看作一个128位向量 ║ ║ ║ ║ 矩阵A ║ ║ ┌─────────────────────────────────┐ ║ ║ │ row0: [a00, a01, a02, a03] │ ← 一个__m128 ║ ║ │ row1: [a10, a11, a12, a13] │ ← 一个__m128 ║ ║ │ row2: [a20, a21, a22, a23] │ ← 一个__m128 ║ ║ │ row3: [a30, a31, a32, a33] │ ← 一个__m128 ║ ║ └─────────────────────────────────┘ ║ ║ ║ ║ 结果矩阵C的第0行怎么算 ║ ║ C_row0 a00 × B_row0 ║ ║ a01 × B_row1 ║ ║ a02 × B_row2 ║ ║ a03 × B_row3 ║ ║ ║ ║ 每一项都是标量 × 向量然后4项相加。 ║ ║ ║ ║ ║ ║ ┌─────────────────────────────────────────────────┐ ║ ║ │ │ ║ ║ │ SSE实现 │ ║ ║ │ │ ║ ║ │ // 计算结果矩阵的一行 │ ║ ║ │ static inline __m128 mat_row_mul( │ ║ ║ │ __m128 a_row, │ ║ ║ │ __m128 b_row0, __m128 b_row1, │ ║ ║ │ __m128 b_row2, __m128 b_row3) │ ║ ║ │ { │ ║ ║ │ // 把a_row的第0个元素广播到4个槽位 │ ║ ║ │ // a_row [a00, a01, a02, a03] │ ║ ║ │ // 广播后 [a00, a00, a00, a00] │ ║ ║ │ __m128 s0 _mm_shuffle_ps( │ ║ ║ │ a_row, a_row, _MM_SHUFFLE(0,0,0,0)); │ ║ ║ │ __m128 s1 _mm_shuffle_ps( │ ║ ║ │ a_row, a_row, _MM_SHUFFLE(1,1,1,1)); │ ║ ║ │ __m128 s2 _mm_shuffle_ps( │ ║ ║ │ a_row, a_row, _MM_SHUFFLE(2,2,2,2)); │ ║ ║ │ __m128 s3 _mm_shuffle_ps( │ ║ ║ │ a_row, a_row, _MM_SHUFFLE(3,3,3,3)); │ ║ ║ │ │ ║ ║ │ // 图解shuffle操作 │ ║ ║ │ // a_row: ┌────┬────┬────┬────┐ │ ║ ║ │ // │a00 │a01 │a02 │a03 │ │ ║ ║ │ // └────┴────┴────┴────┘ │ ║ ║ │ // │ ║ ║ │ // s0: ┌────┬────┬────┬────┐ │ ║ ║ │ // │a00 │a00 │a00 │a00 │ ← 广播 │ ║ ║ │ // └────┴────┴────┴────┘ │ ║ ║ │ // │ ║ ║ │ // s1: ┌────┬────┬────┬────┐ │ ║ ║ │ // │a01 │a01 │a01 │a01 │ ← 广播 │ ║ ║ │ // └────┴────┴────┴────┘ │ ║ ║ │ │ ║ ║ │ // 每个标量 × B的一行然后全部加起来 │ ║ ║ │ return _mm_add_ps( │ ║ ║ │ _mm_add_ps( │ ║ ║ │ _mm_mul_ps(s0, b_row0), │ ║ ║ │ _mm_mul_ps(s1, b_row1)), │ ║ ║ │ _mm_add_ps( │ ║ ║ │ _mm_mul_ps(s2, b_row2), │ ║ ║ │ _mm_mul_ps(s3, b_row3))); │ ║ ║ │ } │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ // 完整的4×4矩阵乘法 │ ║ ║ │ void mat4_mul_sse(const float* A, │ ║ ║ │ const float* B, │ ║ ║ │ float* C) │ ║ ║ │ { │ ║ ║ │ __m128 b0 _mm_load_ps(B[0]); │ ║ ║ │ __m128 b1 _mm_load_ps(B[4]); │ ║ ║ │ __m128 b2 _mm_load_ps(B[8]); │ ║ ║ │ __m128 b3 _mm_load_ps(B[12]); │ ║ ║ │ │ ║ ║ │ __m128 a0 _mm_load_ps(A[0]); │ ║ ║ │ __m128 a1 _mm_load_ps(A[4]); │ ║ ║ │ __m128 a2 _mm_load_ps(A[8]); │ ║ ║ │ __m128 a3 _mm_load_ps(A[12]); │ ║ ║ │ │ ║ ║ │ _mm_store_ps(C[0], │ ║ ║ │ mat_row_mul(a0, b0, b1, b2, b3)); │ ║ ║ │ _mm_store_ps(C[4], │ ║ ║ │ mat_row_mul(a1, b0, b1, b2, b3)); │ ║ ║ │ _mm_store_ps(C[8], │ ║ ║ │ mat_row_mul(a2, b0, b1, b2, b3)); │ ║ ║ │ _mm_store_ps(C[12], │ ║ ║ │ mat_row_mul(a3, b0, b1, b2, b3)); │ ║ ║ │ } │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ 指令数对比 │ ║ ║ │ ┌──────────────┬──────────┬──────────┐ │ ║ ║ │ │ 版本 │ 浮点运算 │ 指令数 │ │ ║ ║ │ ├──────────────┼──────────┼──────────┤ │ ║ ║ │ │ 标量 │ 112次 │ ~112条 │ │ ║ ║ │ │ SSE │ 112次 │ ~28条 │ │ ║ ║ │ │ SSEFMA │ 112次 │ ~20条 │ │ ║ ║ │ └──────────────┴──────────┴──────────┘ │ ║ ║ │ │ ║ ║ │ 同样的计算量指令数减少到1/4甚至1/5。 │ ║ ║ │ 更少的指令 更少的取指解码 更快。 │ ║ ║ │ │ ║ ║ └─────────────────────────────────────────────────┘ ║ ║ ║ ╚══════════════════════════════════════════════════════════╝第五章SIMD的陷阱与注意事项┌══════════════════════════════════════════════════════════┐ ║ ║ ║ SIMD不是银弹。用不好反而更慢。 ║ ║ ║ ║ ┌─────────────────────────────────────────────────┐ ║ ║ │ │ ║ ║ │ 陷阱1内存对齐 │ ║ ║ │ ═══════════════ │ ║ ║ │ │ ║ ║ │ _mm_load_ps 要求地址是16字节对齐的。 │ ║ ║ │ _mm256_load_ps 要求地址是32字节对齐的。 │ ║ ║ │ │ ║ ║ │ 对齐的地址0x00, 0x10, 0x20, 0x30... │ ║ ║ │ 不对齐的地址0x04, 0x13, 0x27... │ ║ ║ │ │ ║ ║ │ 如果地址不对齐 │ ║ ║ │ ├── 用 _mm_load_ps → 直接崩溃段错误 │ ║ ║ │ ├── 用 _mm_loadu_ps → 不崩溃但更慢 │ ║ ║ │ │ 现代CPU上慢10-20%老CPU上慢50% │ ║ ║ │ └── 最佳方案确保数据对齐 │ ║ ║ │ │ ║ ║ │ 怎么确保对齐 │ ║ ║ │ │ ║ ║ │ // C11/C11 │ ║ ║ │ alignas(32) float data[1024]; │ ║ ║ │ │ ║ ║ │ // 动态分配 │ ║ ║ │ float* data (float*)_mm_malloc( │ ║ ║ │ 1024 * sizeof(float), 32); │ ║ ║ │ // 用完后 │ ║ ║ │ _mm_free(data); │ ║ ║ │ │ ║ ║ │ // 不要用普通的malloc │ ║ ║ │ // malloc不保证32字节对齐。 │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ 陷阱2数据不够宽 │ ║ ║ │ ═══════════════ │ ║ ║ │ │ ║ ║ │ 如果你只有3个float要加用SIMD反而更慢。 │ ║ ║ │ │ ║ ║ │ 标量3次加法直接算。 │ ║ ║ │ SIMD把3个float装进128位寄存器 │ ║ ║ │ 第4个槽位浪费了 │ ║ ║ │ 装载和提取的开销比计算本身还大。 │ ║ ║ │ │ ║ ║ │ SIMD的甜蜜点 │ ║ ║ │ ├── 数据量大几百到几百万个元素 │ ║ ║ │ ├── 操作统一所有元素做相同的运算 │ ║ ║ │ ├── 数据连续内存中紧密排列 │ ║ ║ │ └── 没有复杂的条件分支 │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ 陷阱3水平运算很慢 │ ║ ║ │ ═══════════════════ │ ║ ║ │ SIMD擅长垂直运算对应位置的元素互相运算 │ ║ ║ │ SIMD不擅长水平运算同一个寄存器内部求和 │ ║ ║ │ │ ║ ║ │ 垂直运算快 │ ║ ║ │ ┌──┬──┬──┬──┐ ┌──┬──┬──┬──┐ │ ║ ║ │ │a0│a1│a2│a3│ │b0│b1│b2│b3│ ← 一条指令 │ ║ ║ │ └──┴──┴──┴──┘ └──┴──┴──┴──┘ │ ║ ║ │ ┌──────┬──────┬──────┬──────┐ │ ║ ║ │ │a0b0│a1b1│a2b2│a3b3│ │ ║ ║ │ └──────┴──────┴──────┴──────┘ │ ║ ║ │ │ ║ ║ │ 水平运算慢 │ ║ ║ │ ┌──┬──┬──┬──┐ │ ║ ║ │ │a0│a1│a2│a3│ → 求 a0a1a2a3 ? │ ║ ║ │ └──┴──┴──┴──┘ │ ║ ║ │ │ ║ ║ │ 没有一条指令能直接做到。需要多次shuffleadd │ ║ ║ │ │ ║ ║ │ 步骤1shuffle把高位挪到低位 │ ║ ║ │ 原始: ┌──┬──┬──┬──┐ │ ║ ║ │ │a0│a1│a2│a3│ │ ║ ║ │ 移位: ┌──┬──┬──┬──┐ │ ║ ║ │ │a2│a3│??│??│ │ ║ ║ │ 相加: ┌──────┬──────┬──┬──┐ │ ║ ║ │ │a0a2│a1a3│ │ │ │ ║ ║ │ │ ║ ║ │ 步骤2再shuffle一次 │ ║ ║ │ 原始: ┌──────┬──────┬──┬──┐ │ ║ ║ │ │a0a2│a1a3│ │ │ │ ║ ║ │ 移位: ┌──────┬──┬──┬──┐ │ ║ ║ │ │a1a3│ │ │ │ │ ║ ║ │ 相加: ┌──────────────┬──┬──┬──┐ │ ║ ║ │ │a0a1a2a3 │ │ │ │ │ ║ ║ │ │ ║ ║ │ 2次shuffle 2次add 4条指令。 │ ║ ║ │ AVX8个float需要更多步骤。 │ ║ ║ │ │ ║ ║ │ 所以尽量避免水平运算。 │ ║ ║ │ 如果必须求和用多个累加器延迟水平求和 │ ║ ║ │ 在循环结束后只做一次。 │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ 陷阱4分支是SIMD的天敌 │ ║ ║ │ ═══════════════════════ │ ║ ║ │ │ ║ ║ │ 标量代码可以用if跳过不需要的计算 │ ║ ║ │ if (x 0) result x * 2; │ ║ ║ │ else result 0; │ ║ ║ │ │ ║ ║ │ SIMD同时处理8个元素其中3个05个≤0。 │ ║ ║ │ 你不能只算其中3个。 │ ║ ║ │ SIMD必须把8个都算完然后用掩码选择结果。 │ ║ ║ │ │ ║ ║ │ ┌──┬──┬──┬──┬──┬──┬──┬──┐ │ ║ ║ │ │ 3│-1│ 5│-2│ 7│-4│-3│ 1│ ← 8个输入值 │ ║ ║ │ └──┴──┴──┴──┴──┴──┴──┴──┘ │ ║ ║ │ │ ║ ║ │ 全部计算 x*2 │ ║ ║ │ ┌──┬──┬───┬──┬───┬──┬──┬──┐ │ ║ ║ │ │ 6│-2│10 │-4│14 │-8│-6│ 2│ │ ║ ║ │ └──┴──┴───┴──┴───┴──┴──┴──┘ │ ║ ║ │ │ ║ ║ │ 比较 x 0生成掩码 │ ║ ║ │ ┌──┬──┬──┬──┬──┬──┬──┬──┐ │ ║ ║ │ │FF│00│FF│00│FF│00│00│FF│ (FFtrue, 00false) │ ║ ║ │ └──┴──┴──┴──┴──┴──┴──┴──┘ │ ║ ║ │ │ ║ ║ │ 用掩码混合blend │ ║ ║ │ 掩码FF → 取计算结果 │ ║ ║ │ 掩码00 → 取零 │ ║ ║ │ ┌──┬──┬───┬──┬───┬──┬──┬──┐ │ ║ ║ │ │ 6│ 0│10 │ 0│14 │ 0│ 0│ 2│ ← 最终结果 │ ║ ║ │ └──┴──┴───┴──┴───┴──┴──┴──┘ │ ║ ║ │ │ ║ ║ │ 代码 │ ║ ║ │ __m256 x _mm256_load_ps(data); │ ║ ║ │ __m256 doubled _mm256_mul_ps(x, │ ║ ║ │ _mm256_set1_ps(2.0f)); │ ║ ║ │ __m256 zero _mm256_setzero_ps(); │ ║ ║ │ __m256 mask _mm256_cmp_ps(x, zero, │ ║ ║ │ _CMP_GT_OQ); │ ║ ║ │ __m256 result _mm256_blendv_ps( │ ║ ║ │ zero, doubled, mask); │ ║ ║ │ │ ║ ║ │ 注意即使5个元素不需要计算我们也全算了。 │ ║ ║ │ 这是SIMD的代价用多余的计算换取零分支。 │ ║ ║ │ 只要多余计算的成本 分支预测失败的成本 │ ║ ║ │ SIMD就是赢的。 │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ 陷阱5不是所有循环都能向量化 │ ║ ║ │ ═══════════════════════════ │ ║ ║ │ │ ║ ║ │ 能向量化的循环 │ ║ ║ │ ✓ for (i) C[i] A[i] B[i] // 独立 │ ║ ║ │ ✓ for (i) C[i] A[i] * 2 1 // 独立 │ ║ ║ │ ✓ for (i) sum A[i] // 可归约 │ ║ ║ │ │ ║ ║ │ 不能向量化的循环 │ ║ ║ │ ✗ for (i) A[i] A[i-1] A[i-2] // 依赖前项 │ ║ ║ │ ✗ for (i) A[f(i)] 1 // 随机写入 │ ║ ║ │ ✗ for (i) if (complex) do_x else do_y // 复杂分支│ ║ ║ │ │ ║ ║ │ 核心判断标准 │ ║ ║ │ 每次迭代的计算是否独立于其他迭代 │ ║ ║ │ 如果是 → 可以向量化。 │ ║ ║ │ 如果不是 → 很难或不可能向量化。 │ ║ ║ │ │ ║ ║ └─────────────────────────────────────────────────┘ ║ ║ ║ ╚══════════════════════════════════════════════════════════╝第六章让编译器帮你写SIMD┌══════════════════════════════════════════════════════════┐ ║ ║ ║ 手写SIMD intrinsics很强大但也很痛苦。 ║ ║ 好消息现代编译器能自动向量化简单的循环。 ║ ║ ║ ║ ┌─────────────────────────────────────────────────┐ ║ ║ │ │ ║ ║ │ 方法1写出编译器能识别的代码模式 │ ║ ║ │ ════════════════════════════════ │ ║ ║ │ │ ║ ║ │ // 编译器能自动向量化这个 │ ║ ║ │ void add_arrays(float* __restrict__ c, │ ║ ║ │ const float* __restrict__ a, │ ║ ║ │ const float* __restrict__ b, │ ║ ║ │ int n) { │ ║ ║ │ for (int i 0; i n; i) { │ ║ ║ │ c[i] a[i] b[i]; │ ║ ║ │ } │ ║ ║ │ } │ ║ ║ │ │ ║ ║ │ 关键要素 │ ║ ║ │ ├── __restrict__告诉编译器指针不重叠 │ ║ ║ │ ├── 简单循环没有复杂的控制流 │ ║ ║ │ ├── 独立迭代每次循环不依赖上一次 │ ║ ║ │ └── 编译选项-O2 -mavx2 -mfma │ ║ ║ │ │ ║ ║ │ 用 gcc -O2 -mavx2 -S 编译看生成的汇编 │ ║ ║ │ vmovups ymm0, [rsi rax] ; 读8个a │ ║ ║ │ vaddps ymm0, ymm0, [rdxrax] ; 加8个b │ ║ ║ │ vmovups [rdi rax], ymm0 ; 存8个c │ ║ ║ │ │ ║ ║ │ 编译器自动用了AVX256位8路并行 │ ║ ║ │ 你写的是普通C代码编译器帮你变成了SIMD。 │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ 方法2用编译器提示 │ ║ ║ │ ═══════════════ │ ║ ║ │ │ ║ ║ │ // GCC/Clang │ ║ ║ │ #pragma GCC ivdep │ ║ ║ │ // 我保证这个循环没有依赖放心向量化 │ ║ ║ │ for (int i 0; i n; i) { │ ║ ║ │ c[i] a[i] * b[i] d[i]; │ ║ ║ │ } │ ║ ║ │ │ ║ ║ │ // MSVC │ ║ ║ │ #pragma loop(ivdep) │ ║ ║ │ │ ║ ║ │ // OpenMP SIMD跨平台 │ ║ ║ │ #pragma omp simd │ ║ ║ │ for (int i 0; i n; i) { │ ║ ║ │ c[i] a[i] * b[i] d[i]; │ ║ ║ │ } │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ 方法3检查编译器是否真的向量化了 │ ║ ║ │ ═══════════════════════════════ │ ║ ║ │ │ ║ ║ │ // GCC编译时加 -fopt-info-vec │ ║ ║ │ gcc -O2 -mavx2 -fopt-info-vec mycode.c │ ║ ║ │ │ ║ ║ │ 输出 │ ║ ║ │ mycode.c:12: optimized: loop vectorized │ ║ ║ │ using 32 byte vectors │ ║ ║ │ mycode.c:25: missed: couldnt vectorize loop │ ║ ║ │ mycode.c:25: missed: possible aliasing │ ║ ║ │ │ ║ ║ │ 第12行成功向量化了用了256位AVX。 │ ║ ║ │ 第25行失败了原因是可能有指针重叠。 │ ║ ║ │ → 加上 __restrict__ 再试。 │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ 方法4在 godbolt.org 上实时查看 │ ║ ║ │ ═══════════════════════════════ │ ║ ║ │ │ ║ ║ │ godbolt.org 是一个在线编译器浏览器。 │ ║ ║ │ 左边写C代码右边实时显示生成的汇编。 │ ║ ║ │ 看到 vmovps、vaddps、vmulps 就说明向量化了。 │ ║ ║ │ 看到 movss、addss、mulss 就说明还是标量。 │ ║ ║ │ │ ║ ║ │ 指令名称速查 │ ║ ║ │ ├── v开头 AVX指令向量化的 │ ║ ║ │ ├── ps结尾 packed single多个float │ ║ ║ │ ├── ss结尾 scalar single单个float │ ║ ║ │ ├── 256 256位操作8个float │ ║ ║ │ └── 看到ymm寄存器 AVX在工作 │ ║ ║ │ │ ║ ║ └─────────────────────────────────────────────────┘ ║ ║ ║ ╚══════════════════════════════════════════════════════════╝终章SIMD的全景图┌══════════════════════════════════════════════════════════┐ ║ ║ ║ ┌─────────────────────────────────────────────────┐ ║ ║ │ │ ║ ║ │ 什么时候用SIMD │ ║ ║ │ │ ║ ║ │ ✓ 粒子系统成千上万个粒子做相同运算 │ ║ ║ │ ✓ 物理引擎碰撞检测、约束求解 │ ║ ║ │ ✓ 动画系统骨骼变换、蒙皮 │ ║ ║ │ ✓ 音频处理混音、滤波、FFT │ ║ ║ │ ✓ 图像处理模糊、色彩转换、缩放 │ ║ ║ │ ✓ AI寻路大量距离计算 │ ║ ║ │ ✓ 数学库向量、矩阵、四元数运算 │ ║ ║ │ ✓ 网络序列化批量数据转换 │ ║ ║ │ │ ║ ║ │ 什么时候不用SIMD │ ║ ║ │ │ ║ ║ │ ✗ 数据量很小只有几个元素 │ ║ ║ │ ✗ 逻辑复杂大量不规则分支 │ ║ ║ │ ✗ 数据不连续随机访问、链表遍历 │ ║ ║ │ ✗ 每个元素的处理逻辑不同 │ ║ ║ │ ✗ 代码可读性比性能更重要的场景 │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ SIMD优化的三个层次 │ ║ ║ │ │ ║ ║ │ ┌──────────────────────────────────────────┐ │ ║ ║ │ │ 层次1让编译器自动向量化 │ │ ║ ║ │ │ 难度★☆☆☆☆ │ │ ║ ║ │ │ 收益2-4倍 │ │ ║ ║ │ │ 方法写简单循环 __restrict__ │ │ ║ ║ │ │ 编译选项 -O2 -mavx2 │ │ ║ ║ │ │ 适合大部分场景 │ │ ║ ║ │ ├──────────────────────────────────────────┤ │ ║ ║ │ │ 层次2用SIMD intrinsics手写 │ │ ║ ║ │ │ 难度★★★☆☆ │ │ ║ ║ │ │ 收益4-8倍 │ │ ║ ║ │ │ 方法_mm256_xxx 系列函数 │ │ ║ ║ │ │ 适合热点函数、数学库、物理引擎 │ │ ║ ║ │ ├──────────────────────────────────────────┤ │ ║ ║ │ │ 层次3手写汇编 微架构调优 │ │ ║ ║ │ │ 难度★★★★★ │ │ ║ ║ │ │ 收益在层次2基础上再提升10-30% │ │ ║ ║ │ │ 方法内联汇编精确控制指令调度 │ │ ║ ║ │ │ 适合极少数极端热点如编解码器内核 │ │ ║ ║ │ └──────────────────────────────────────────┘ │ ║ ║ │ │ ║ ║ │ 大多数游戏开发者只需要掌握层次1和层次2。 │ ║ ║ │ 层次3是引擎底层开发者和编解码器专家的领域。 │ ║ ║ │ │ ║ ║ │ │ ║ ║ │ 最后的比喻 │ ║ ║ │ │ ║ ║ │ 标量运算是一个人用一双筷子吃饭。 │ ║ ║ │ SIMD是一个人同时举着8双筷子 │ ║ ║ │ 每双筷子夹起一块不同的菜 │ ║ ║ │ 同时送进8张嘴里。 │ ║ ║ │ │ ║ ║ │ 听起来荒谬 │ ║ ║ │ 但CPU就是这么干的。 │ ║ ║ │ 而且它干得又快又准。 │ ║ ║ │ │ ║ ║ │ 你要做的就是把菜整整齐齐地摆成一排 │ ║ ║ │ 让那8双筷子能同时夹到。 │ ║ ║ │ │ ║ ║ │ 这就是SIMD编程的全部哲学 │ ║ ║ │ ┌──────────────────────────────────────────┐ │ ║ ║ │ │ │ │ ║ ║ │ │ 把数据排列好让硬件一口吃掉一批。 │ │ ║ ║ │ │ │ │ ║ ║ │ └──────────────────────────────────────────┘ │ ║ ║ │ │ ║ ║ └─────────────────────────────────────────────────┘ ║ ║ ║ ╚══════════════════════════════════════════════════════════╝— 全文完 —