Eigen中的SparseMatrix(稀疏矩阵),直接使用insert方法或者coeffRef方法插入元素会导致非常严重的性能问题。因为稀疏矩阵通常采用的存储方式为行压缩或者列压缩,可以理解为将每行/列中的非0元素拼接成一个数组,再结合一些其它的辅助信息来实现对稀疏矩阵的存取。
对于行/列压缩的稀疏矩阵,直接插入元素在没有剩余空间的情形下会导致内存的重新分配和拷贝,插入单个元素的时间复杂度为O(n),因此是非常操蛋的。对此,Eigen的官方文档提供了2种插入SparseMatrix的推荐方法:
1.使用三元组
通过Eigen::Triplet,提供所有非0元素的行、列、值,然后使用setFromTriplets方法插入元素。且看网上代码:
Eigen::SparseMatrix<float> m(3, 3);
std::vector<Eigen::Triplet<float> > triple;
for (int i = 0; i < 3; i ) {
for (int j = 0; j < 3; j ) {
triple.push_back(Eigen::Triplet<float>(i, j, 0.1));
}
}
triple.push_back(Eigen::Triplet<float>(1, 1, 0.1)); //相同下标重复插入,则表示相加
m.setFromTriplets(triple.begin(), triple.end());
2.预分配空间
通过reserve方法,制定SparseMatrix每行、列的最大非0元素个数。当实际插入的非0元素个数少于reserve的值时,使用insert方法或者coeffRef方法不会导致内存的重新分配。
Eigen::SparseMatrix<float> m(4, 3);
Eigen::VectorXi sizes(3);
sizes[0] = 1; //第0列有1个元素
sizes[1] = 2; //第1列有2个元素
sizes[2] = 3; //第2列有3个元素
m.reserve(sizes);
m.insert(0, 0) = 1.0f;
m.insert(0, 1) = 2.0f;
m.insert(1, 1) = 2.0f;
m.insert(0, 2) = 3.0f;
m.insert(1, 2) = 3.0f;
m.insert(2, 2) = 3.0f;
注意上面的例子中,sizes的长度应当和m的列数相等,因为SparseMatrix默认是列压缩存储。当SparseMatrix显式钦定为行压缩存储时:
Eigen::SparseMatrix<float, Eigen::RowMajor> m(4, 3);
则对应的sizes的长度应该和行数相等。
总结
Triplet方法无需事先了解矩阵整体的非0元素的分布情况,并且自带数值相加的功能。而reserve方法在使用得当时整体的性能比Triplet方法更高。
值得注意的是,reserve方法可以优化为并行插入——在reserve完成后只要满足:
- 对于行压缩RowMajor的矩阵,同一行同一时间只由一个线程插入
- 对于列压缩ColMajor的矩阵,同一列同一时间只由一个线程插入
那么SparseMatrix的插入是线程安全的