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

AVX512分块矩阵乘法kend从144变152性能骤降及MOV预取疑问

分块矩阵乘法性能骤降问题的分析与解决方案建议

问题背景

在32 KiB L1缓存、1 MiB L2缓存的硬件环境下,采用AVX512指令集的_mm512_i32gather_pd实现分块矩阵乘法时,发现当kend值从144变为152时,矩阵乘法耗时显著增加。核心实现代码如下:

void matmul_tile(int i, int k, double *c, double const *a, double const *b, int iend, int jend, int kend){
    __m256i baseb = _mm256_setr_epi32(
        0 * j, 1 * j, 2 * j, 3 * j,
        4 * j, 5 * j, 6 * j, 7 * j
    );
    for(int ii = 0; ii <= iend - 10; ii += 10)
      for(int jj = 0; jj < jend; ++jj){
          __m512d accums[10] = {
              _mm512_setzero_pd(), _mm512_setzero_pd(), _mm512_setzero_pd(), _mm512_setzero_pd(), _mm512_setzero_pd(),
              _mm512_setzero_pd(), _mm512_setzero_pd(), _mm512_setzero_pd(), _mm512_setzero_pd(), _mm512_setzero_pd()
          };

          for(int kk = 0; kk <= kend - 8; kk += 8){
              __m512d b_vals = _mm512_i32gather_pd(baseb, &b[kk * j + jj], sizeof(double));
              double const *a_base = &a[(ii + 0) * k + kk];

              accums[0] = _mm512_fmadd_pd(b_vals, _mm512_loadu_pd(&a_base[0]), accums[0]);
              accums[1] = _mm512_fmadd_pd(b_vals, _mm512_loadu_pd(&a_base[k]), accums[1]);
              accums[2] = _mm512_fmadd_pd(b_vals, _mm512_loadu_pd(&a_base[2 * k]), accums[2]);
              accums[3] = _mm512_fmadd_pd(b_vals, _mm512_loadu_pd(&a_base[3 * k]), accums[3]);
              accums[4] = _mm512_fmadd_pd(b_vals, _mm512_loadu_pd(&a_base[4 * k]), accums[4]);
              accums[5] = _mm512_fmadd_pd(b_vals, _mm512_loadu_pd(&a_base[5 * k]), accums[5]);
              accums[6] = _mm512_fmadd_pd(b_vals, _mm512_loadu_pd(&a_base[6 * k]), accums[6]);
              accums[7] = _mm512_fmadd_pd(b_vals, _mm512_loadu_pd(&a_base[7 * k]), accums[7]);
              accums[8] = _mm512_fmadd_pd(b_vals, _mm512_loadu_pd(&a_base[8 * k]), accums[8]);
              accums[9] = _mm512_fmadd_pd(b_vals, _mm512_loadu_pd(&a_base[9 * k]), accums[9]);
          }

          double *c_base = &c[(ii + 0) * j + jj];
          c_base[0] += _mm512_reduce_add_pd(accums[0]);
          c_base[j] += _mm512_reduce_add_pd(accums[1]);
          c_base[2 * j] += _mm512_reduce_add_pd(accums[2]);
          c_base[3 * j] += _mm512_reduce_add_pd(accums[3]);
          c_base[4 * j] += _mm512_reduce_add_pd(accums[4]);
          c_base[5 * j] += _mm512_reduce_add_pd(accums[5]);
          c_base[6 * j] += _mm512_reduce_add_pd(accums[6]);
          c_base[7 * j] += _mm512_reduce_add_pd(accums[7]);
          c_base[8 * j] += _mm512_reduce_add_pd(accums[8]);
          c_base[9 * j] += _mm512_reduce_add_pd(accums[9]);
      }
}

性能下降原因分析

通过缓存占用计算:10行数据在kend=144时约占30 KiB(未达到L1缓存上限),kend=152时约占32 KiB(刚好填满L1缓存)。此时预取的B矩阵下一列元素会在jj迭代前被缓存驱逐,导致_mm512_i32gather_pd指令因频繁缓存缺失而变慢。

针对疑问的解答

1. 能否不使用volatile asm插入MOV指令?

可以,有两种更简洁且安全的替代方案:

  • 普通C加载操作:通过编译器属性保留加载行为,避免被优化掉。例如:
    __attribute__((unused)) double dummy = b[(kk + 8)*j + (jj + 1)];
    
    __attribute__((unused))会告诉编译器该变量未被使用但不要优化掉对应的加载指令,从而手动触发B矩阵下一列数据的缓存预取。
  • 编译器内置预取函数:使用_mm_prefetch系列函数,指定预取到L1缓存(通过_MM_HINT_T0参数)。例如:
    _mm_prefetch(&b[(kk + 16)*j + (jj + 1)], _MM_HINT_T0);
    
    注意不同处理器对预取提示的支持有差异,需要结合硬件实际测试效果。

2. 插入MOV指令是否会影响_mm512_loadu_pd的吞吐量?

通常不会产生显著负面影响,原因如下:

  • 现代x86处理器(如Skylake-X)具备多个独立的加载端口,只要MOV指令的加载地址和_mm512_loadu_pd的加载地址不冲突(比如不在同一缓存行),两者可以并行执行,不占用同一端口资源。
  • 你的场景中A、B矩阵属于不同的内存区域,地址重叠概率极低,因此不会出现缓存行竞争问题。
  • 需注意控制MOV指令的数量,避免过多占用指令窗口资源,反而拖慢整体执行效率。

额外优化建议

  • 调整分块大小:将i方向的分块从10行缩减至8行,此时kend=152时的缓存占用约为25.6 KiB,低于L1缓存容量,从根源上避免缓存驱逐问题。
  • 优化B矩阵访问模式:如果可能,将B矩阵的访问调整为连续加载,避免使用gather指令——gather指令本身吞吐量低于连续加载,缓存缺失时性能差距会被放大。
  • 多策略对比测试:针对手动MOV加载、编译器内置预取、无预取三种方案进行性能测试,结合目标硬件的缓存特性选择最优方案。

内容的提问来源于stack exchange,提问作者sridoo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 17:34:56