simd实现条件分支的并行计算及其局限
南京大学 理论物理硕士
凡是并行计算遇到一层条件嵌套就要打个对折,simd,simt都一样。。以前觉得cpu很弱,但是分支预测真的很强,只会做简单大规模数值计算某种意义上才弱。
目录
收起
引言
SIMD 介绍
简单分支和复杂嵌套分支的simd实现
1. 基础条件分支测试(basic.c)
代码结构
目的
2. 四层嵌套条件测试(four_layer.c)
代码结构
目的
3. 复杂条件与多操作测试(complex.c)
代码结构
目的
4. 关键设计思想
5. 性能差异根源
6. 总结
7. 性能数据汇总表
8. 加速比分析
9. 关键原因总结
10. 性能对比示意图
总结
源码地址
引言
我们在进行数值算法加速的时候经常使用到SIMD(单指令多数据),对于大规模的矩阵,张量计算,simd已经展现了其强大的并行处理能力,诸如在一个cycle内计算8个float的乘累加计算,但是这些算法它们的逻辑其实都是较为简单的,它们只是计算加减乘除等算术运算,没有复杂的分支结构。比如使用位运算来计算float的加减法。我们便思考,对于这些算法simd是否还能有较好的加速效果,本文就探讨这一话题。
SIMD 介绍
- 是什么:单指令多数据,一条指令同时处理多个数据(如同时算8个数加法)
- 作用:数据并行加速,适合图像/科学计算等批量操作
- 条件分支处理:
- ✅ 可行:通过「掩码」选择不同结果(如条件满足选A,否则选B)
- ✅ 常用方法:预计算所有分支结果,最后用掩码混合
- 局限:
- ❗ 深层嵌套会显著增加掩码生成与混合操作
- ❗ 分支稀疏(如仅5%数据满足条件)时加速比下降甚至劣化
- 适用性:
- ✔️ 数据量大且条件可合并时有效(加速1.5x+)
- ❌ 复杂随机分支时可能比标量更慢(如百万级数据实测慢6%)
简单分支和复杂嵌套分支的simd实现
1. 基础条件分支测试(basic.c)
代码结构
// 标量版本
void scalar_nested_conditions(const float *a, const float *b, float *dst, int n) {
for (...) {
if (a[i] > b[i]) dst[i] = a + b;
else dst[i] = a - b;
}
}
// SIMD版本
void simd_nested_conditions(const float *a, const float *b, float *dst, int n) {
for (...) { // 每次处理8个元素
__m256 va = _mm256_loadu_ps(a + i); // 加载向量
__m256 vb = _mm256_loadu_ps(b + i);
__m256 mask = _mm256_cmp_ps(va, vb, _CMP_GT_OQ); // 生成条件掩码
__m256 add = _mm256_add_ps(va, vb); // 并行加法
__m256 sub = _mm256_sub_ps(va, vb); // 并行减法
__m256 final = _mm256_blendv_ps(sub, add, mask); // 按掩码混合结果
_mm256_storeu_ps(dst + i, final); // 存储结果
}
}
目的
验证 简单条件分支 的SIMD优化效果。通过向量化掩码操作(_mm256_cmp_ps
+ _mm256_blendv_ps
)消除分支预测失败的开销,实现8路并行计算。
2. 四层嵌套条件测试(four_layer.c)
代码结构
// 标量版本(四层条件)
void scalar_nested_conditions(...) {
for (...) {
if (a > 10) {
if (b < 5) {
if (c == 3) {
if (d != 0) final = a + b;
else final = a - c;
} else final = a * b;
} else final = a / b;
} else final = sqrt(a);
}
}
// SIMD版本
void simd_nested_conditions(...) {
// 生成四层条件掩码
__m256 mask1 = _mm256_cmp_ps(va, ten, _CMP_GT_OQ); // a > 10
__m256 mask2 = _mm256_cmp_ps(vb, five, _CMP_LT_OQ); // b < 5
__m256 mask3 = _mm256_cmp_ps(vc, three, _CMP_EQ_OQ);// c == 3
__m256 mask4 = _mm256_cmp_ps(vd, zero, _CMP_NEQ_OQ);// d != 0
// 逐层混合结果
__m256 cond4 = _mm256_and_ps(mask1, _mm256_and_ps(mask2, ...));
__m256 layer4 = _mm256_blendv_ps(sub, add, cond4);
// ...(类似处理其他层)
}
目的
测试 多层嵌套条件分支 的优化效果。SIMD版本需通过逻辑与操作(_mm256_and_ps
)组合多个掩码,并按层次混合结果。由于需要计算所有分支路径,指令数显著增加。
3. 复杂条件与多操作测试(complex.c)
代码结构
// 标量版本(含else if和复杂操作)
void scalar_nested_conditions(...) {
for (...) {
if (a > 4) {
if (b < 5) {
if (c == 10) {
if (d != 0) final = a + b;
else if (e < 7) final = a - e; // else-if分支
else final = a - c;
} else if (f >= -2) final = a * f; // 第三层else-if
// ...
} else if (g > 0) final = a / g; // 第二层else-if
// ...
} else if (h != 0) final = sqrt(a + h); // 第一层else-if
// ...
}
}
// SIMD版本(预计算所有可能结果)
void simd_nested_conditions(...) {
// 预计算所有分支结果
__m256 add_ab = _mm256_add_ps(va, vb);
__m256 sub_ae = _mm256_sub_ps(va, ve);
__m256 sub_ac = _mm256_sub_ps(va, vc);
// ...(共9种预计算结果)
// 多层混合逻辑(从最内层开始)
__m256 layer4_elseif = _mm256_blendv_ps(sub_ac, sub_ae, mask_e_lt_7);
__m256 layer4 = _mm256_blendv_ps(layer4_elseif, add_ab, mask_d_ne_zero);
// ...(共4层混合)
}
目的
验证 含else-if
分支和高延迟操作(除法、平方根) 的极端场景。SIMD版本需处理复杂的掩码组合关系,且预计算所有可能结果,导致指令数爆炸性增长。
4. 关键设计思想
1. SIMD消除分支预测
通过掩码(mask
)和混合指令(blendv
)替代条件跳转,避免分支预测失败导致的流水线清空。
// 标量分支
if (a > b) x = a + b; else x = a - b;
// SIMD等效
mask = compare(a, b);
x = blend(add(a,b), sub(a,b), mask);
2. 计算与选择分离
强制并行计算所有分支结果,最后通过掩码选择最终值。代价是计算冗余,但对简单条件可显著提升吞吐量。
3. 层次化掩码混合
对多层嵌套条件,从最内层开始逐层混合结果(见four_layer.c
):
cond4 = mask1 & mask2 & mask3 & mask4;
layer4 = blend(sub, add, cond4); // 第四层结果
layer3 = blend(mul, layer4, cond3); // 第三层使用第四层结果
5. 性能差异根源
测试案例 | SIMD优势场景 | SIMD劣势场景 |
---|---|---|
basic.c | 简单条件,无复杂操作 | - |
four_layer.c | 中等复杂度条件 | 掩码组合开销增加 |
complex.c | - | 高延迟操作+复杂掩码逻辑 |
- 指令吞吐量差异
- SIMD的
blendv
指令吞吐量为1周期/指令(Skylake架构),而标量的条件跳转在分支预测成功时接近0周期。 - 复杂条件导致SIMD需要更多
blendv
和逻辑运算指令。
- SIMD的
- 高延迟操作影响
如_mm256_div_ps
(除法)和_mm256_sqrt_ps
(平方根)的延迟为10-20周期,且SIMD无法乱序执行隐藏延迟。 - 内存访问模式
当数据规模超过L3缓存时,SIMD的8路并行会更快耗尽内存带宽,加剧性能下降。
6. 总结
通过这三个测试案例,可以看出: - SIMD在简单条件分支中优势显著(basic.c加速比8-33x), - 随着条件复杂度和计算操作延迟增加,收益逐渐消失(complex.c在1M数据后性能反降), - 分支预测和乱序执行使标量代码在复杂场景中更具韧性。
7. 性能数据汇总表
测试案例 | 数据规模 | 标量版本耗时(ns) | SIMD版本耗时(ns) | 加速比 |
---|---|---|---|---|
basic.c | 1,000 | 1,150.68 | 207.33 | 5.55 |
10,000 | 55,165.34 | 1,670.70 | 33.01 | |
100,000 | 356,536.78 | 12,426.50 | 28.70 | |
1,000,000 | 3,818,940.21 | 231,796.26 | 16.47 | |
10,000,000 | 39,662,974.40 | 4,546,276.01 | 8.72 | |
four_layer.c | 1,000 | 1,362.54 | 867.95 | 1.57 |
10,000 | 13,559.03 | 6,078.75 | 2.23 | |
100,000 | 70,421.78 | 36,736.89 | 1.92 | |
1,000,000 | 738,778.05 | 489,409.95 | 1.51 | |
10,000,000 | 8,475,064.26 | 7,287,073.21 | 1.16 | |
complex.c | 1,000 | 2,077.73 | 1,763.25 | 1.18 |
10,000 | 21,281.38 | 12,238.88 | 1.74 | |
100,000 | 103,256.35 | 66,761.81 | 1.55 | |
1,000,000 | 1,049,778.39 | 1,090,990.97 | 0.96 | |
10,000,000 | 11,033,407.39 | 12,713,688.45 | 0.87 |
8. 加速比分析
- basic.c:
- 最高加速比33.01(10k数据),SIMD优势显著。
- 随着数据规模增大,加速比逐渐降低,但仍保持较高水平。
- 原因:简单条件分支,SIMD并行度高(8路并行),且无复杂计算。
- four_layer.c:
- 加速比峰值2.23(10k数据),之后逐渐降低。
- 在10M数据时加速比仅1.16,接近标量性能。
- 原因:四层嵌套条件导致SIMD需计算所有分支,混合操作引入额外开销。
- complex.c:
- 加速比峰值1.74(10k数据),1M数据后SIMD性能反降。
- 10M数据时SIMD比标量慢13%(加速比0.87)。
- 原因:多层
else if
分支导致掩码逻辑复杂,SIMD需预计算更多结果,且存在高延迟操作(如除法、平方根)。
9. 关键原因总结
- 分支预测与乱序执行:
- 标量代码可通过分支预测减少分支惩罚,SIMD则完全消除分支但需计算所有路径。
- 当分支预测成功率较高时(如随机数据),标量性能更优。
- SIMD并行度与开销:
- SIMD理论加速比8x,但实际受限于:
- 混合操作(
blendv
)的指令开销。 - 高延迟操作(如除法)的流水线阻塞。
- 每个分支都需要预计算,计算冗余较大。
- 数据规模影响:
- 小数据规模:SIMD优势明显(循环展开充分)。
- 大数据规模:内存带宽限制和缓存失效导致加速比下降。
- 操作复杂度:
- 复杂条件(如
complex.c
)导致SIMD需处理更多掩码逻辑,指令数激增,最终性能可能劣于标量版本。
- 复杂条件(如
10. 性能对比示意图
basic.c : ■■■■■■■■ (最高33x加速)
four_layer.c : ■■■■ (峰值2.23x)
complex.c : ■■ (后期反降)
总结
综上所述,SIMD在简单条件分支中优势显著,但随着条件复杂度增加,其收益逐渐被额外计算和指令开销抵消,最终可能劣于优化后的标量代码。
可以这样认为,条件分支每嵌套一层,并行度就要打对这,同时因为额外的blend计算,加速比进一步下降,基本上达到4层以上的嵌套,使用avx2来实现条件分支就没有优势了。
实际上可以采用标量和simd混合计算的方式来实现复杂的条件分支。