向量化FMADD应用输入数据量增大时GFLOPS/sec下降原因探究
问题背景
我正在分析一段向量化FMADD函数的性能,函数代码如下:
std::pair<double, double> fmadd_256(const float* aptr, const float* bptr, uint64_t sz) { constexpr size_t num_lanes = 256 / (sizeof(float) * 8); constexpr size_t num_loads = 1; __m256 c[8]; initialize_buffer<8>(c, 0.0); __m256 a[2][num_loads]; __m256 b[2][num_loads]; initialize_buffer<1>(a[0], 1.0); initialize_buffer<1>(a[1], 1.0); initialize_buffer<1>(b[0], -1.0); initialize_buffer<1>(b[1], -1.0); load_buffer(aptr, bptr, a[0], b[0], 0); for (uint64_t i = 0; i < sz - 1; i++) { uint64_t curr_buffer = i % 2; __m256* curra = a[curr_buffer]; __m256* currb = b[curr_buffer]; uint64_t next_buffer = (i + 1) % 2; __m256* nexta = a[next_buffer]; __m256* nextb = b[next_buffer]; uint64_t load_position = (i + 1) * num_lanes * num_loads; load_buffer(aptr, bptr, nexta, nextb, load_position); c[0] = _mm256_fmadd_ps(curra[0], currb[0], c[0]); c[1] = _mm256_fmadd_ps(curra[0], currb[0], c[1]); c[2] = _mm256_fmadd_ps(curra[0], currb[0], c[2]); c[3] = _mm256_fmadd_ps(curra[0], currb[0], c[3]); c[4] = _mm256_fmadd_ps(curra[0], currb[0], c[4]); c[5] = _mm256_fmadd_ps(curra[0], currb[0], c[5]); c[6] = _mm256_fmadd_ps(curra[0], currb[0], c[6]); c[7] = _mm256_fmadd_ps(curra[0], currb[0], c[7]); } double total_flops = 8 * 2 * 8 * sz; double res = epilogue(c); return {total_flops, res}; }
测试环境为Intel Coffee Lake笔记本,按照Roofline模型设计了每次内存加载执行8次乘法的比例。小数据集时性能能达到机器峰值浮点运算性能,但数据集增大后,GFLOPS/Sec明显下降,具体数据如下:
| 操作 | GFLOPS/Sec |
|---|---|
| FMADD/12 | 127.004/s |
| FMADD/24 | 123.528/s |
| FMADD/32 | 124.203/s |
| FMADD/48 | 125.133/s |
| FMADD/56 | 125.023/s |
| FMADD/128 | 109.202/s |
| FMADD/256 | 91.519/s |
| FMADD/1024 | 86.1444/s |
| FMADD/2048 | 85.9394/s |
| FMADD/4096 | 78.2796/s |
| FMADD/8192 | 53.0381/s |
| FMADD/16384 | 39.7985/s |
| FMADD/32768 | 34.2683/s |
使用perf测量L1缓存丢失率,发现丢失率始终维持在50%左右,且与性能下降无明显关联:
| 每个输入向量的KB数 | L1-dcache-load-misses | GFLOPS/Sec |
|---|---|---|
| 512 | 50.66% | 99 |
| 1024 | 50.71% | 96 |
| 5120 | 50.81% | 70 |
另外,Intel Advisors显示该笔记本DRAM带宽为13 GB/sec,按当前每次加载8次乘法的比例,理论上应该能维持峰值浮点吞吐量,实际却未达到,疑问为何性能会随数据集增大而下降?
性能下降核心原因
1. 伪共享导致的缓存行频繁失效
代码中双缓冲a[2][num_loads]和b[2][num_loads]的两个缓冲区,每个__m256占32字节,刚好能塞进同一个64字节缓存行。循环中交替读写两个缓冲区时,会触发伪共享:修改其中一个缓冲区会导致整个缓存行标记为无效,下一次访问另一个缓冲区时必须从L2缓存重新加载,直接拉高了L1缓存丢失率(固定50%就是交替失效的直接体现),额外消耗了缓存带宽。
2. 内存延迟无法被运算完全掩盖
当数据集超过L3缓存容量后,数据需从DRAM加载。Coffee Lake的DRAM延迟约60-80ns,而单周期可执行2条256位FMA指令(共16次浮点运算)。代码中每次加载仅对应8次FMA运算,运算时间远短于内存延迟,即使双缓冲也无法完全掩盖等待时间,CPU会出现大量空闲周期,导致GFLOPS随数据量增大持续下降。
3. 运算强度仍未满足Roofline模型要求
虽然设计了8次运算/1次加载的比例,但实际换算为FLOPS/字节的话:每次加载64字节(2个__m256),对应128次浮点操作(8次FMA指令×8元素×2操作/元素),即2 FLOPS/字节。结合13 GB/s的DRAM带宽,理论最大支撑的吞吐量为26 GFLOPS,这与你大数据集下的34 GFLOPS接近(考虑实际带宽波动),说明此时已经触达内存带宽瓶颈,无法维持峰值浮点性能。
优化建议
- 解决伪共享:给双缓冲添加填充,确保每个缓冲区独占一个缓存行,比如用
alignas(64)修饰缓冲区数组,或者在两个缓冲区之间插入空元素。 - 提升运算强度:扩大
c数组的规模,在单次加载后执行更多次FMA运算,进一步提高FLOPS/字节比例,降低内存带宽的限制。 - 显式预取:在循环中提前预取下一批数据(使用
_mm_prefetch),掩盖DRAM访问延迟,但需注意预取时机,避免缓存污染。 - 多线程并行:如果当前是单线程运行,可拆分数据集到多个核心并行计算,利用多核提升整体吞吐量,同时注意数据划分的缓存友好性。
内容的提问来源于stack exchange,提问作者fabian

