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
相关产品推荐
相关产品推荐

