Eigen多线程矩阵乘法扩展异常:行主稀疏-稠密可扩展,列主稠密-稀疏不行?
为什么Eigen中行主序稀疏-稠密乘法线程扩展性更好?
这是个非常典型的Eigen性能优化问题,核心根源在于稀疏矩阵的存储格式特性和Eigen针对两种乘法场景的并行化策略差异,咱们一步步拆解:
1. 先明确核心前提:Eigen默认的稀疏矩阵存储格式
Eigen中默认的稀疏矩阵是CSR(行压缩存储),也就是行主序的稀疏存储——每一行的非零元素是连续存放在内存里的,而行与行之间通过行指针数组来索引。这种格式天生对行方向的计算友好,但对列方向的计算非常不友好。
2. 行主序稀疏×稠密乘法的并行化优势
当你计算 C = A_sparse_row * B_dense 时:
- Eigen会按输出矩阵C的行来拆分并行任务——每个线程负责计算C的一个连续行块。
- 每个线程访问A的行数据是连续的(CSR格式的天然优势),缓存命中率极高;如果B是行主序稠密矩阵,访问B的行数据也是连续的,完全契合CPU的缓存机制。
- 任务拆分无冲突:每个线程只修改自己负责的行块,不需要和其他线程共享内存资源,线程调度开销极小。
- 这种“大粒度、缓存友好、无冲突”的并行模式,自然能随着线程数增加线性提升性能,扩展性很好。
3. 列主序稠密×稀疏乘法的并行化瓶颈
当你计算 C = A_dense_col * B_sparse_row 时:
- 这里的B是默认的CSR格式(行主序稀疏),要计算C的某一列,需要用到B的对应列的非零元素——但CSR格式下,列的非零元素是分散在内存各个位置的,要遍历这些元素需要跳着访问整个稀疏矩阵的存储数组,缓存命中率极低。
- Eigen如果尝试按C的列拆分任务,会遇到两个问题:
- 任务粒度不均:如果B的某些列非零元素多、某些少,线程负载会严重不均衡;
- 内存访问混乱:每个线程访问A的列数据(A是列主序,连续存储)但需要搭配B的分散列元素,频繁的缓存失效会抵消多线程的收益。
- 更关键的是,Eigen对这种“稠密列主序×行主序稀疏”的乘法,没有做针对性的并行优化——因为CSR格式不适合列方向计算,要高效并行的话,需要先把B转成CSC(列压缩存储)格式,但Eigen不会自动做这个转换(除非你显式调用)。
验证优化方案
如果你想让列主序稠密×稀疏乘法也有好的线程扩展性,可以试试:
- 把稀疏矩阵B转成**CSC(列压缩存储)**格式:
Eigen::SparseMatrix<double, Eigen::ColMajor> B_csc = B_sparse_row; - 然后计算
C = A_dense_col * B_csc,这时候Eigen可以按C的列拆分任务,访问A的列和B_csc的列都是连续内存,缓存友好,线程扩展性会明显提升。
内容的提问来源于stack exchange,提问作者avgn
相关产品推荐
相关产品推荐

