稀疏 x 密集矩阵乘以 Armadillo 出乎意料地慢

Posted

技术标签:

【中文标题】稀疏 x 密集矩阵乘以 Armadillo 出乎意料地慢【英文标题】:Sparse x dense matrix multiply unexpectedly slow with Armadillo 【发布时间】:2018-04-11 09:13:37 【问题描述】:

这是我刚刚遇到的。出于某种原因,在犰狳中将密集矩阵与稀疏矩阵相乘比将稀疏矩阵和密集矩阵相乘(即颠倒顺序)要慢得多。

// [[Rcpp::depends(RcppArmadillo)]]
#include <RcppArmadillo.h>

// [[Rcpp::export]]
arma::sp_mat mult_sp_den_to_sp(arma::sp_mat& a, arma::mat& b)

    // sparse x dense -> sparse
    arma::sp_mat result(a * b);
    return result;


// [[Rcpp::export]]
arma::sp_mat mult_den_sp_to_sp(arma::mat& a, arma::sp_mat& b)

    // dense x sparse -> sparse
    arma::sp_mat result(a * b);
    return result;

我正在使用 RcppArmadillo 包将 Arma 与 R 接口; RcppArmadillo.h 包括 armadillo。以下是 R 中的一些计时,在几个相当大的垫子上:

set.seed(98765)
# 10000 x 10000 sparse matrices, 99% sparse
a <- rsparsematrix(1e4, 1e4, 0.01, rand.x=function(n) rpois(n, 1) + 1)
b <- rsparsematrix(1e4, 1e4, 0.01, rand.x=function(n) rpois(n, 1) + 1)

# dense copies
a_den <- as.matrix(a)
b_den <- as.matrix(b)

system.time(mult_sp_den_to_sp(a, b_den))
#   user  system elapsed 
# 508.66    0.79  509.95 

system.time(mult_den_sp_to_sp(a_den, b))
#   user  system elapsed 
#  13.52    0.74   14.29 

所以第一次乘法比第二次长大约 35 倍(所有时间都以秒为单位)。

有趣的是,如果我只是简单地制作密集矩阵的临时稀疏副本,性能会大大提高:

// [[Rcpp::export]]
arma::sp_mat mult_sp_den_to_sp2(arma::sp_mat& a, arma::mat& b)

    // sparse x dense -> sparse
    // copy dense to sparse, then multiply
    arma::sp_mat temp(b);
    arma::sp_mat result(a * temp);
    return result;

system.time(mult_sp_den_to_sp2(a, b_den))
#   user  system elapsed 
#   5.45    0.41    5.86 

这是预期的行为吗?我知道,对于稀疏矩阵,你做事的确切方式会对你的代码效率产生很大的影响,比密集矩阵要大得多。不过,35 倍的速度差异似乎相当大。

【问题讨论】:

我不会使用稀疏矩阵作为结果,但这并不会真正影响您示例中的性能。 【参考方案1】:

稀疏矩阵和稠密矩阵的存储方式截然不同。 Armadillo 使用 CMS(主要列存储)存储密集矩阵,使用 CSC(压缩稀疏列)存储稀疏矩阵。来自犰狳的文档:

垫子垫子cx_mat 密集矩阵的类,元素以列优先顺序存储(即逐列)

SpMat sp_mat sp_cx_mat 稀疏矩阵的类,元素以压缩稀疏列 (CSC) 格式存储

我们首先要了解的是每种格式需要多少存储空间:

给定数量 element_size(单精度 4 个字节,双精度 8 个字节),index_size(如果使用 32 位整数,则为 4 个字节,如果使用 64 位整数,则为 8 个字节),num_rows (矩阵的行数)、num_cols(矩阵的列数)和num_nnz(矩阵的非零元素数),下面的公式给我们每种格式的存储空间:

storage_cms = num_rows * num_cols * element_size
storage_csc = num_nnz * element_size + num_nnz * index_size + num_cols * index_size

有关存储格式的更多详细信息,请参阅wikipedia 或netlib。

假设双精度和 32 位 indeces,在您的情况下,这意味着:

storage_cms = 800MB
storage_csc = 12.04MB

因此,当您乘以稀疏 x 密集(或密集 x 稀疏)矩阵时,您将访问约 812MB 的内存,而在乘以稀疏 x 稀疏矩阵时仅访问约 24MB 的内存。

请注意,这不包括您写入结果的内存,这可能是很大一部分(在两种情况下都高达 ~800MB),但我不太熟悉犰狳以及它用于矩阵的算法乘法,所以不能确切地说它是如何存储中间结果的。

不管是什么算法,它肯定需要多次访问两个输入矩阵,这就解释了为什么将密集矩阵转换为稀疏矩阵(只需要一次访问 800MB 的密集矩阵),然后做一个稀疏 x 稀疏乘积 (需要多次访问 24MB 内存)比密集 x 稀疏和稀疏 x 密集产品更有效。

这里还有各种各样的缓存效果,这需要了解算法的确切实现和硬件(以及大量时间)才能正确解释,但以上是大致思路。

至于为什么dense x sparsesparse x dense快,是因为稀​​疏矩阵的CSC存储格式。如scipy's documentation 中所述,CSC 格式对于列切片是有效的,而对于行切片则很慢。 dense x sparse 乘法算法需要对稀疏矩阵进行列切片,sparse x dense 需要对稀疏矩阵进行行切片。请注意,如果犰狳使用 CSR 而不是 CSC,sparse x dense 会很有效,而dense x sparse 则不会。

我知道这不是您所看到的所有性能影响的完整答案,但应该让您大致了解正在发生的事情。正确的分析需要更多的时间和精力来完成,并且必须包括算法的具体实现,以及有关运行它的硬件的信息。

【讨论】:

然而,a %*% b_den 在 R 中速度很快。【参考方案2】:

这应该会在即将发布的Armadillo 8.500 中得到解决,该问题将很快包含在 RcppArmadillo 0.8.5 Real 中。具体来说:

稀疏矩阵转置要快得多 (sparse x dense) 重新实现为 ((dense^T) x (sparse^T))^T,利用相对快速的 (dense x sparse) 代码

当我测试它时,所用时间从大约 500 秒下降到大约 18 秒,这与其他时间相当。

【讨论】:

以上是关于稀疏 x 密集矩阵乘以 Armadillo 出乎意料地慢的主要内容,如果未能解决你的问题,请参考以下文章

MATLAB 稀疏矩阵

使用 Armadillo C++ 加载稀疏矩阵

TypeError:传递了稀疏矩阵,但需要密集数据。使用 X.toarray() 转换为密集的 numpy 数组。使用 NaiveBayes 分类器

numpy.ndarray稀疏矩阵到密集

如何通过在 Rcpp 或 Armadillo 中将矩阵乘以向量元素来复制 R 的功能?

armadillo c++:将矩阵的每一行乘以向量的有效而简洁的方法?