【发布时间】:2020-01-24 01:11:02
【问题描述】:
我写了一个函数,将维度为 10x10 的特征矩阵相乘。然后我写了一个简单的乘法函数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
更新: 在 cmets 中经过有益讨论后进行以下修改后结果不变
-
num_repetitions = 1和input.size() = 1000 - 使用
.lazyProduct()和使用.eval()实际上会导致进一步 减速 - clang 8.0.0
- g++ 9.2
- 使用标志
-march=native -DNDEBUG
更新 2:
使用 Google Benchmark 库跟进 @dtell 的发现后,我发现了一个有趣的结果。将 2 个矩阵与 Eigen 相乘比自定义更快,但将多个矩阵与 Eigen 相乘则慢 2 倍,这与之前的发现一致。
这是我的 Google 基准代码。 (注意:下面的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 有更多经验的人可以告诉我我是否正确解释了这个结果。 DSB 是解码后的指令缓存,而 MITE 与指令解码器带宽有关。 Eigen 版本显示,大多数指令都缺少 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
我想知道在我之前的基准测试结果中是否遗漏了优化标志。无论如何,我认为这个问题已经得到证实,即 Eigen 在乘以小矩阵时会产生开销。如果有人有一台不使用 uop 缓存的机器,我很想看看减速是否不那么严重。
【问题讨论】:
-
这里的结果与 g++ 9.2 和
-O2完全相同。 -
为了比较,你也可以试试
-march=native -O2 -DNDEBUG,你可以试试matrix_a.lazyProduct(matrix_b)而不是matrix_a * matrix_b吗?在我看来,切换到缓存优化的 GEMM 的阈值设置得太低了。 -
@chtz 使用您建议的标志并没有什么不同。使用
lazyProduct加快了计算速度,但不如 naive 版本快 - 仅比 naive 慢 1.5 倍。 -
玩弄维度(顺便说一下,你忘记了一个类型)似乎,eigen 的性能开始比从 dim = 8 开始的自定义 GEMM 差。因为它比例如,在 n=4 时的自定义,它认为它切换了内部算法。
-
对我来说,自定义对于单人来说也更快:544ns vs 390ns。一致地,对于 clang 和 gcc。它们之间的差异因编译器而异,但自定义始终更快。顺便说一句,这很难分析。如果没有 VTune 或 perf 写出的琐碎线索,可能需要花费太多精力才能弄清楚发生了什么(因为编译的 asm 代码很大)。