【问题标题】:Is Eigen slow at multiplying small matrices?Eigen 在乘法小矩阵方面很慢吗?
【发布时间】: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 = 1input.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 输出:

Eigen 的 VTune 输出:

也许对 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 代码很大)。

标签: c++ eigen3


【解决方案1】:
(gdb) bt
#0  0x00005555555679e3 in Eigen::internal::gemm_pack_rhs<double, long, Eigen::internal::const_blas_data_mapper<double, long, 0>, 4, 0, false, false>::operator()(double*, Eigen::internal::const_blas_data_mapper<double, long, 0> const&, long, long, long, long) ()
#1  0x0000555555566654 in Eigen::internal::general_matrix_matrix_product<long, double, 0, false, double, 0, false, 0>::run(long, long, long, double const*, long, double const*, long, double*, long, double, Eigen::internal::level3_blocking<double, double>&, Eigen::internal::GemmParallelInfo<long>*) ()
#2  0x0000555555565822 in BM_PairwiseMultiplyEachMatrixNoAlias(benchmark::State&) ()
#3  0x000055555556d571 in benchmark::internal::(anonymous namespace)::RunInThread(benchmark::internal::Benchmark::Instance const*, unsigned long, int, benchmark::internal::ThreadManager*) ()
#4  0x000055555556b469 in benchmark::RunSpecifiedBenchmarks(benchmark::BenchmarkReporter*, benchmark::BenchmarkReporter*) ()
#5  0x000055555556a450 in main ()

从堆栈跟踪来看,eigen 的矩阵乘法使用通用乘法方法并循环通过动态矩阵大小。对于自定义实现,clang 积极地将其矢量化并展开循环,因此分支要少得多。

也许 eigen 有一些标志/选项可以为这个特定大小生成代码以进行优化。

但是,如果矩阵大小更大,Eigen 版本的性能将比自定义版本好得多。

【讨论】:

    【解决方案2】:

    我已使用适当的基准库(即Google Benchmark)重写了您的代码,并且无法重现您的测量结果。

    -O0 我的结果,其中第二个模板参数是矩阵维度:

    Running ./benchmark
    Run on (12 X 2900 MHz CPU s)
    CPU Caches:
      L1 Data 32K (x6)
      L1 Instruction 32K (x6)
      L2 Unified 262K (x6)
      L3 Unified 12582K (x1)
    ---------------------------------------------------------------------
    Benchmark                              Time           CPU Iterations
    ---------------------------------------------------------------------
    BM_CustomMultiply<double, 3>        5391 ns       5389 ns     105066
    BM_CustomMultiply<double, 4>        9365 ns       9364 ns      73649
    BM_CustomMultiply<double, 5>       15349 ns      15349 ns      44008
    BM_CustomMultiply<double, 6>       20953 ns      20947 ns      32230
    BM_CustomMultiply<double, 7>       33328 ns      33318 ns      21584
    BM_CustomMultiply<double, 8>       44237 ns      44230 ns      15500
    BM_CustomMultiply<double, 9>       57142 ns      57140 ns      11953
    BM_CustomMultiply<double, 10>      69382 ns      69382 ns       9998
    BM_EigenMultiply<double, 3>         2335 ns       2335 ns     295458
    BM_EigenMultiply<double, 4>         1613 ns       1613 ns     457382
    BM_EigenMultiply<double, 5>         4791 ns       4791 ns     142992
    BM_EigenMultiply<double, 6>         3471 ns       3469 ns     206002
    BM_EigenMultiply<double, 7>         9052 ns       9051 ns      78135
    BM_EigenMultiply<double, 8>         8655 ns       8655 ns      81717
    BM_EigenMultiply<double, 9>        11446 ns      11399 ns      67001
    BM_EigenMultiply<double, 10>       15092 ns      15053 ns      46924
    

    如您所见,Google Benchmark 使用的迭代次数比您的基准高几个数量级。微基准测试非常困难,尤其是在处理几百纳秒的执行时间时。

    公平地说,调用您的自定义函数涉及复制并手动内联它会花费几纳秒,但仍然没有超过 Eigen。

    使用手动内联 CustomMultiply-O2 -DNDEBUG -march=native 进行测量:

    Running ./benchmark
    Run on (12 X 2900 MHz CPU s)
    CPU Caches:
      L1 Data 32K (x6)
      L1 Instruction 32K (x6)
      L2 Unified 262K (x6)
      L3 Unified 12582K (x1)
    ---------------------------------------------------------------------
    Benchmark                              Time           CPU Iterations
    ---------------------------------------------------------------------
    BM_CustomMultiply<double, 3>          51 ns         51 ns   11108114
    BM_CustomMultiply<double, 4>          88 ns         88 ns    7683611
    BM_CustomMultiply<double, 5>         147 ns        147 ns    4642341
    BM_CustomMultiply<double, 6>         213 ns        213 ns    3205627
    BM_CustomMultiply<double, 7>         308 ns        308 ns    2246391
    BM_CustomMultiply<double, 8>         365 ns        365 ns    1904860
    BM_CustomMultiply<double, 9>         556 ns        556 ns    1254953
    BM_CustomMultiply<double, 10>        661 ns        661 ns    1027825
    BM_EigenMultiply<double, 3>           39 ns         39 ns   17918807
    BM_EigenMultiply<double, 4>           69 ns         69 ns    9931755
    BM_EigenMultiply<double, 5>          119 ns        119 ns    5801185
    BM_EigenMultiply<double, 6>          178 ns        178 ns    3838772
    BM_EigenMultiply<double, 7>          256 ns        256 ns    2692898
    BM_EigenMultiply<double, 8>          385 ns        385 ns    1826598
    BM_EigenMultiply<double, 9>          546 ns        546 ns    1271687
    BM_EigenMultiply<double, 10>         644 ns        644 ns    1104798
    

    【讨论】:

    • 谢谢@dtell!您可以发布基准代码吗?我想在我的机器上试用它,并了解它如何获得更准确的结果。在我的电脑上,我使用的是std::chrono::steady_clock
    • 在我的代码中,我在调用函数之前和之后调用std::chrono::steady_clock::now(),然后报告结果。
    • 我已使用 Google Benchmark 代码示例更新了我的问题。你能告诉我它是否有问题吗?我发现相同的结果
    • 哦,这很有趣。一个主要区别(我认为不会是主要区别)是我在每次迭代中生成一个随机矩阵,而不是传递一个随机矩阵向量。今晚我在家时会通过代码发布。
    • 谢谢 - 我已经重新运行了我的基准测试,现在即使是单乘自定义版本也更快。你用的是什么CPU?
    猜你喜欢
    • 2017-12-09
    • 2023-03-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-13
    • 1970-01-01
    相关资源
    最近更新 更多