我写了一个函数,将维数为10 x10的特征矩阵相乘。然后我写了一个简单的乘法函数CustomMultiply
,它比Eigen的实现快了2倍。
我尝试了几个不同的编译标志,如-O2和-O3,这并没有什么区别。
#include <Eigen/Core>
constexpr int dimension = 10;
using Matrix = Eigen::Matrix<double, dimension, dimension>;
Matrix CustomMultiply(const Matrix& a, const Matrix& b) {
Matrix result = Matrix::Zero();
for (int bcol_idx = 0; bcol_idx < dimension; ++bcol_idx) {
for (int brow_idx = 0; brow_idx < dimension; ++brow_idx) {
result.col(bcol_idx).noalias() += a.col(brow_idx) * b(brow_idx, bcol_idx);
}
}
return result;
}
Matrix PairwiseMultiplyEachMatrixNoAlias(int num_repetitions, const std::vector<Matrix>& input) {
Matrix acc = Matrix::Zero();
for (int i = 0; i < num_repetitions; ++i) {
for (const auto& matrix_a : input) {
for (const auto& matrix_b : input) {
acc.noalias() += matrix_a * matrix_b;
}
}
}
return acc;
}
Matrix PairwiseMultiplyEachMatrixCustom(int num_repetitions, const std::vector<Matrix>& input) {
Matrix acc = Matrix::Zero();
for (int i = 0; i < num_repetitions; ++i) {
for (const auto& matrix_a : input) {
for (const auto& matrix_b : input) {
acc.noalias() += CustomMultiply(matrix_a, matrix_b);
}
}
}
return acc;
}
在我的机器上,当我传入100个随机矩阵作为input
并使用100作为num_repetitions
时,PairwiseMultiplyEachMatrixNoAlias
比PairwiseMultiplyEachMatrixCustom
慢2倍。我的机器详细信息:Intel Xeon CPU E5-2630 v4,Ubuntu 16.04,Eigen 3
更新:在评论中进行有益讨论后,进行以下修改后,结果不变
num_repetitions = 1
和input.size() = 1000
- 使用
.lazyProduct()
和使用.eval()
实际上会导致进一步的减速 - clang 8.0.0
- g++ 9.2
- 使用标志
-march=native -DNDEBUG
更新2:
在Google Benchmark库中跟踪@dtell的发现后,我发现了一个有趣的结果。将2个矩阵与特征值相乘比自定义快,但将许多矩阵与特征值相乘慢2倍,与之前的发现一致。
这是我的Google Benchmark代码。(注意:下面的GenerateRandomMatrices()
函数中有一个off-by,现在已修复。
#include <Eigen/Core>
#include <Eigen/StdVector>
#include <benchmark/benchmark.h>
constexpr int dimension = 10;
constexpr int num_random_matrices = 10;
using Matrix = Eigen::Matrix<double, dimension, dimension>;
using Eigen_std_vector = std::vector<Matrix,Eigen::aligned_allocator<Matrix>>;
Eigen_std_vector GetRandomMatrices(int num_matrices) {
Eigen_std_vector matrices;
for (int i = 0; i < num_matrices; ++i) {
matrices.push_back(Matrix::Random());
}
return matrices;
}
Matrix CustomMultiply(const Matrix& a, const Matrix& b) {
Matrix result = Matrix::Zero();
for (int bcol_idx = 0; bcol_idx < dimension; ++bcol_idx) {
for (int brow_idx = 0; brow_idx < dimension; ++brow_idx) {
result.col(bcol_idx).noalias() += a.col(brow_idx) * b(brow_idx, bcol_idx);
}
}
return result;
}
Matrix PairwiseMultiplyEachMatrixNoAlias(int num_repetitions, const Eigen_std_vector& input) {
Matrix acc = Matrix::Zero();
for (int i = 0; i < num_repetitions; ++i) {
for (const auto& matrix_a : input) {
for (const auto& matrix_b : input) {
acc.noalias() += matrix_a * matrix_b;
}
}
}
return acc;
}
Matrix PairwiseMultiplyEachMatrixCustom(int num_repetitions, const Eigen_std_vector& input) {
Matrix acc = Matrix::Zero();
for (int i = 0; i < num_repetitions; ++i) {
for (const auto& matrix_a : input) {
for (const auto& matrix_b : input) {
acc.noalias() += CustomMultiply(matrix_a, matrix_b);
}
}
}
return acc;
}
void BM_PairwiseMultiplyEachMatrixNoAlias(benchmark::State& state) {
// Perform setup here
const auto random_matrices = GetRandomMatrices(num_random_matrices);
for (auto _ : state) {
benchmark::DoNotOptimize(PairwiseMultiplyEachMatrixNoAlias(1, random_matrices));
}
}
BENCHMARK(BM_PairwiseMultiplyEachMatrixNoAlias);
void BM_PairwiseMultiplyEachMatrixCustom(benchmark::State& state) {
// Perform setup here
const auto random_matrices = GetRandomMatrices(num_random_matrices);
for (auto _ : state) {
benchmark::DoNotOptimize(PairwiseMultiplyEachMatrixCustom(1, random_matrices));
}
}
BENCHMARK(BM_PairwiseMultiplyEachMatrixCustom);
void BM_MultiplySingle(benchmark::State& state) {
// Perform setup here
const auto random_matrices = GetRandomMatrices(2);
for (auto _ : state) {
benchmark::DoNotOptimize((random_matrices[0] * random_matrices[1]).eval());
}
}
BENCHMARK(BM_MultiplySingle);
void BM_MultiplySingleCustom(benchmark::State& state) {
// Perform setup here
const auto random_matrices = GetRandomMatrices(2);
for (auto _ : state) {
benchmark::DoNotOptimize(CustomMultiply(random_matrices[0], random_matrices[1]));
}
}
BENCHMARK(BM_MultiplySingleCustom);
double TestCustom() {
const Matrix a = Matrix::Random();
const Matrix b = Matrix::Random();
const Matrix c = a * b;
const Matrix custom_c = CustomMultiply(a, b);
const double err = (c - custom_c).squaredNorm();
return err;
}
// Just sanity check the multiplication
void BM_TestCustom(benchmark::State& state) {
if (TestCustom() > 1e-10) {
exit(-1);
}
}
BENCHMARK(BM_TestCustom);
这就产生了以下神秘的报告
Run on (20 X 3100 MHz CPU s)
CPU Caches:
L1 Data 32K (x10)
L1 Instruction 32K (x10)
L2 Unified 256K (x10)
L3 Unified 25600K (x1)
***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead.
----------------------------------------------------------------------------
Benchmark Time CPU Iterations
----------------------------------------------------------------------------
BM_PairwiseMultiplyEachMatrixNoAlias 28283 ns 28285 ns 20250
BM_PairwiseMultiplyEachMatrixCustom 14442 ns 14443 ns 48488
BM_MultiplySingle 791 ns 791 ns 876969
BM_MultiplySingleCustom 874 ns 874 ns 802052
BM_TestCustom 0 ns 0 ns 0
我目前的假设是,减速是由于指令缓存未命中。这可能是Eigen的矩阵乘法函数对指令缓存做了不好的事情。
VTune自定义输出:
本征的VTune输出:
也许对VTune更有经验的人可以告诉我,我是否正确地解释了这个结果。DSB是解码的指令高速缓存,MITE与指令解码器带宽有关。本征版本表明,大多数指令丢失DSB(66%的未命中率),并且由于MITE带宽而导致停顿显著增加。
更新3:在得到自定义的单一版本更快的报告后,我也在我的机器上复制了它。这与@dtell在他们的机器上的最初发现背道而驰。
CPU Caches:
L1 Data 32K (x10)
L1 Instruction 32K (x10)
L2 Unified 256K (x10)
L3 Unified 25600K (x1)
***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead.
----------------------------------------------------------------------------
Benchmark Time CPU Iterations
----------------------------------------------------------------------------
BM_PairwiseMultiplyEachMatrixNoAlias 34787 ns 34789 ns 16477
BM_PairwiseMultiplyEachMatrixCustom 17901 ns 17902 ns 37759
BM_MultiplySingle 349 ns 349 ns 2054295
BM_MultiplySingleCustom 178 ns 178 ns 4624183
BM_TestCustom 0 ns 0 ns 0
我想知道在我以前的基准测试结果中是否遗漏了一个优化标志。在任何情况下,我认为这个问题被证实,本征在乘以小矩阵时会产生开销。如果有人有一台不使用uop缓存的机器,我会有兴趣看看减速是否不那么严重。
3条答案
按热度按时间ttisahbt1#
从堆栈跟踪,特征的矩阵乘法是使用一个通用的乘法方法和循环通过一个动态的矩阵大小。对于自定义实现,clang积极地对其进行向量化并展开循环,因此分支要少得多。
也许有一些标志/选项用于eigen生成此特定大小的代码以进行优化。
但是,如果矩阵大小更大,本征版本的性能将比自定义版本好得多。
zpf6vheq2#
我已经使用适当的基准库(即Google Benchmark)重写了您的代码,无法重现您的测量结果。
我对
-O0
的结果,其中第二个模板参数是矩阵维数:正如您所看到的,Google Benchmark使用的迭代次数比您的基准测试高出一个数量级。微基准测试是非常困难的,特别是当你处理几百纳秒的执行时间时。
公平地说,调用自定义函数涉及到一个副本,手动内联它会产生几纳秒的时间,但仍然不能击败Eigen。
使用手动内联
CustomMultiply
和-O2 -DNDEBUG -march=native
进行测量:q9rjltbz3#
因为特征值将把矩阵压缩到小矩阵上,如果矩阵不回退到gemv。封装矩阵引入其他开销。