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

CSR格式SpMV性能优于MKL但SpMM落后,求优化方案

问题分析与优化建议

一、自研实现与性能数据

1. 稀疏矩阵-向量乘积(SpMV)实现

void dcsrmv(SparseMatrixCSR *A, double *x, double *y) {
  for (int i=0; i<A->m; i++) {
    double sum = 0.;
    for (int jj=A->row_start[i]; jj<A->row_start[i+1]; jj++) {
      sum += A->val[jj] * x[A->col_idx[jj]];
    }
    y[i] = sum;
  }
}

测试结果:在SuiteSparse的ecology1矩阵上运行100次,平均耗时5.9ms,优于MKL的mkl_sparse_d_mv(8.4ms)。

2. 三种稀疏矩阵-多向量乘积(SpMM)实现

X、Y按列存储,当nvec=10时的性能数据:

dcsrmm0(向量循环外层)

void dcsrmm0(SparseMatrixCSR *A, double *X, double *Y, int nvec) {
  for (int vec=0; vec<nvec; vec++) {
    double *x = X + vec * A->n;
    double *y = Y + vec * A->m;
    for (int i=0; i<A->m; i++) {
      double sum = 0.;
      for (int jj=A->row_start[i]; jj<A->row_start[i+1]; jj++) {
        sum += A->val[jj] * x[A->col_idx[jj]];
      }
      y[i] = sum;
    }
  }
}

耗时:49.8ms

dcsrmm1(行循环外层,向量循环中层)

void dcsrmm1(SparseMatrixCSR *A, double *X, double *Y, int nvec) {
  for (int i=0; i<A->m; i++) {
    for (int vec=0; vec<nvec; vec++) {
      double sum = 0.;
      double *x = X + vec * A->n;
      double *y = Y + vec * A->m;
      for (int jj=A->row_start[i]; jj<A->row_start[i+1]; jj++) {
        sum += A->val[jj] * x[A->col_idx[jj]];
      }
      y[i] = sum;
    }
  }
}

耗时:38.3ms

dcsrmm2(行循环外层,非零元循环中层,向量循环内层)

void dcsrmm2(SparseMatrixCSR *A, double *X, double *Y, int nvec) {
  double *x[nvec], *y[nvec];
  for (int vec=0; vec<nvec; vec++) x[vec] = X + vec * A->n;
  for (int vec=0; vec<nvec; vec++) y[vec] = Y + vec * A->m;
  double sum[nvec];
  for (int i=0; i<A->m; i++) {
    for (int vec=0; vec<nvec; vec++) sum[vec] = 0.;
    for (int jj=A->row_start[i]; jj<A->row_start[i+1]; jj++) {
      for (int vec=0; vec<nvec; vec++) {
        sum[vec] += A->val[jj] * x[vec][A->col_idx[jj]];
      }
    }
    for (int vec=0; vec<nvec; vec++) y[vec][i] = sum[vec];
  }
}

耗时:34.8ms

MKL对比:mkl_sparse_d_mm耗时26.7ms,自研版本均落后。


二、性能差异原因分析

1. SpMV优于MKL的核心原因

  • 矩阵特性适配:ecology1的稀疏结构刚好适配自研代码的行遍历逻辑,行内非零元分布让缓存命中率极高;而MKL是通用实现,要兼容所有稀疏矩阵类型,额外的分支、格式校验逻辑带来了 overhead。
  • 无冗余抽象:自研代码直接操作CSR原始结构,没有MKL内部的格式转换、多层封装等额外开销。
  • 编译器优化适配:极简的代码结构让编译器(如GCC/Clang)能轻松做循环展开、寄存器分配等优化,而MKL的复杂代码在特定场景下优化效果反而受限。

2. SpMM落后MKL的关键缺陷

MKL的mkl_sparse_d_mm针对多向量场景做了深度定制优化,自研实现存在以下致命问题:

  • 内存访问模式低效:X按列存储时,x[vec][col_idx[jj]]是跨向量的随机访问,不同向量的同列元素间隔为矩阵列数A->n,远超缓存行大小,导致大量缓存 miss;dcsrmm0/1本质是重复调用SpMV,完全没利用多向量的复用性,每次都重复加载CSR的val和col_idx,浪费内存带宽。
  • SIMD利用不足:dcsrmm2的内层向量循环无法被编译器有效向量化,sum[nvec]的累加没有对齐SIMD寄存器,无法发挥CPU的向量计算能力。
  • 缺乏分块优化:MKL会对CSR矩阵做行分块,把非零元分布相似的行打包处理,提升缓存复用;还会对多向量做块对齐,让内存访问更连续。
  • 寄存器利用率低:自研代码用栈上的sum[nvec]存储累加值,频繁读写栈内存,而MKL会尽可能把多向量累加值放在寄存器中,减少内存交互。

三、SpMM优化方案

1. 调整数据存储格式(优先推荐)

如果允许修改输入格式,将X改为按行存储(同一列的nvec个向量元素连续存放),此时x[col_idx[jj]][vec]的访问是连续内存操作,缓存命中率会大幅提升。结合dcsrmm2的逻辑,能直接利用连续访问的优势。

2. 手动SIMD向量化

针对现有列存储X,利用编译器内置函数(如AVX/AVX512指令)手动实现向量累加,示例片段(以nvec=4、AVX2为例):

// 假设X格式调整为:每列的nvec个元素连续存储
__m256d sum_vec = _mm256_setzero_pd();
for (int jj=A->row_start[i]; jj<A->row_start[i+1]; jj++) {
    double val = A->val[jj];
    int col = A->col_idx[jj];
    // 加载该列的4个向量元素
    __m256d x_vec = _mm256_loadu_pd(X + col * nvec);
    // val * x_vec 并累加
    sum_vec = _mm256_add_pd(sum_vec, _mm256_mul_pd(_mm256_set1_pd(val), x_vec));
}
// 将结果写入Y的第i行
_mm256_storeu_pd(Y + i * nvec, sum_vec);

3. CSR分块优化(Block CSR)

将原CSR矩阵划分为若干行块,每个块内的非零元连续存储,对每个块的多向量做批量累加处理,提升val和col_idx的缓存复用率,减少缓存 miss。

4. 消除冗余内存操作

  • 提前缓存x[vec]和y[vec]的指针,避免循环内重复计算;
  • 用寄存器数组替代栈上的sum[nvec],只有在每行计算完成后一次性写入Y,减少内存读写次数。

5. 编译器优化选项加持

编译时开启最高级优化:

  • GCC/Clang:-O3 -march=native -ffast-math
  • MSVC:/O2 /arch:AVX2
    这些选项会让编译器自动做循环展开、寄存器分配、SIMD向量化等优化,大幅提升性能。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 15:39:55