矩阵行求和与 RowMajor 和 ColMajor 数据排列的奇怪性能差异
Strange performance difference of matrix row summation with RowMajor and ColMajor data arrangement
我决定检查矩阵中的数据排列如何影响简单操作的性能。
我使用 Eigen::Matrix
作为数据存储编写了简单的行求和算法。
我认为 RowMajor 存储应该展示更好的性能,因为缓存利用率更高。
我使用了带有 -O2
选项的 g++
编译器,结果如下:
上校:40791546 µs
主要行:28790948 µs
还不错。但是 -O3
它给了我非常奇怪的区别:
上校:10353619 µs
主要行:28359348 µs
看起来 ColMajor 在 -O3
上变得非常快。为什么从 -O2
切换到 -O3
会如此显着地改变性能?
我的CPU:intel i7-6700K,gcc版本:7.5.0-3ubuntu1~19.10
我的"benchmark":
#include <iostream>
#include <vector>
#include <chrono>
#include "Eigen/Core"
template<typename DerivedMat, typename DerivedRes>
void runTest(const Eigen::MatrixBase<DerivedMat> &mat, Eigen::MatrixBase<DerivedRes> &res) {
const int64_t nRows = mat.rows();
const int64_t nCols = mat.cols();
for(int64_t row = 0; row < nRows; ++row){
for(int64_t col = 0; col < nCols; ++col){
res(row, 0) += mat(row, col);
}
}
}
const int64_t nRows = 300;
const int64_t nCols = 5000;
const int nAttempts = 20000;
template<int Alignment>
void bench() {
Eigen::Matrix<float, -1, -1, Alignment> mat(nRows, nCols);
srand(42);
mat.setRandom();
Eigen::VectorXf res(nRows);
res.setZero();
std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
for(int iter = 0; iter < nAttempts; ++iter)
runTest(mat, res);
std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
std::cout << "Elapsed " << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << "[µs]" << std::endl;
}
int main() {
bench<Eigen::ColMajor>();
//bench<Eigen::RowMajor>();
return 0;
}
基于 ColMajor
的循环在 -O3
中要快得多,因为 GCC 7.5 能够自动对其进行矢量化,而不是基于 RowMajor
的循环。您可以看到 in the assembly code(L11
标记的循环)。
自动矢量化为 not performed by GCC in -O2
.
实际上,提到的缓存效果尤其适用于 不适合缓存的大矩阵 ,对于相对较小的矩阵,矢量化可能比缓存效率更重要。问题是 GCC 在简单缩减的向量化方面有一些困难。
您可以帮助他使用 OpenMP 指令,例如 #pragma omp simd reduction(+:accumulatorVar)
。或者,您可以使用 Eigen 提供的按行求和,该求和应该被矢量化(尤其是对于连续数据)。
生成的代码应该是所有先前代码中最快的。
Here 是生成的汇编代码。
我决定检查矩阵中的数据排列如何影响简单操作的性能。
我使用 Eigen::Matrix
作为数据存储编写了简单的行求和算法。
我认为 RowMajor 存储应该展示更好的性能,因为缓存利用率更高。
我使用了带有 -O2
选项的 g++
编译器,结果如下:
上校:40791546 µs
主要行:28790948 µs
还不错。但是 -O3
它给了我非常奇怪的区别:
上校:10353619 µs
主要行:28359348 µs
看起来 ColMajor 在 -O3
上变得非常快。为什么从 -O2
切换到 -O3
会如此显着地改变性能?
我的CPU:intel i7-6700K,gcc版本:7.5.0-3ubuntu1~19.10
我的"benchmark":
#include <iostream>
#include <vector>
#include <chrono>
#include "Eigen/Core"
template<typename DerivedMat, typename DerivedRes>
void runTest(const Eigen::MatrixBase<DerivedMat> &mat, Eigen::MatrixBase<DerivedRes> &res) {
const int64_t nRows = mat.rows();
const int64_t nCols = mat.cols();
for(int64_t row = 0; row < nRows; ++row){
for(int64_t col = 0; col < nCols; ++col){
res(row, 0) += mat(row, col);
}
}
}
const int64_t nRows = 300;
const int64_t nCols = 5000;
const int nAttempts = 20000;
template<int Alignment>
void bench() {
Eigen::Matrix<float, -1, -1, Alignment> mat(nRows, nCols);
srand(42);
mat.setRandom();
Eigen::VectorXf res(nRows);
res.setZero();
std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
for(int iter = 0; iter < nAttempts; ++iter)
runTest(mat, res);
std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
std::cout << "Elapsed " << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << "[µs]" << std::endl;
}
int main() {
bench<Eigen::ColMajor>();
//bench<Eigen::RowMajor>();
return 0;
}
基于 ColMajor
的循环在 -O3
中要快得多,因为 GCC 7.5 能够自动对其进行矢量化,而不是基于 RowMajor
的循环。您可以看到 in the assembly code(L11
标记的循环)。
自动矢量化为 not performed by GCC in -O2
.
实际上,提到的缓存效果尤其适用于 不适合缓存的大矩阵 ,对于相对较小的矩阵,矢量化可能比缓存效率更重要。问题是 GCC 在简单缩减的向量化方面有一些困难。
您可以帮助他使用 OpenMP 指令,例如 #pragma omp simd reduction(+:accumulatorVar)
。或者,您可以使用 Eigen 提供的按行求和,该求和应该被矢量化(尤其是对于连续数据)。
生成的代码应该是所有先前代码中最快的。
Here 是生成的汇编代码。