You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

向量化FMADD应用输入数据量增大时GFLOPS/sec下降原因探究

向量化FMADD函数性能下降原因分析

问题背景

我正在分析一段向量化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/12127.004/s
FMADD/24123.528/s
FMADD/32124.203/s
FMADD/48125.133/s
FMADD/56125.023/s
FMADD/128109.202/s
FMADD/25691.519/s
FMADD/102486.1444/s
FMADD/204885.9394/s
FMADD/409678.2796/s
FMADD/819253.0381/s
FMADD/1638439.7985/s
FMADD/3276834.2683/s

使用perf测量L1缓存丢失率,发现丢失率始终维持在50%左右,且与性能下降无明显关联:

每个输入向量的KB数L1-dcache-load-missesGFLOPS/Sec
51250.66%99
102450.71%96
512050.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 17:17:01