在 ARM / Raspberry PI 上的多个内核上运行 Eigen 密集矩阵乘法时性能下降

Posted

技术标签:

【中文标题】在 ARM / Raspberry PI 上的多个内核上运行 Eigen 密集矩阵乘法时性能下降【英文标题】:Performance drops when running Eigen dense matrix multiplications over multiple cores on ARM / Raspberry PI 【发布时间】:2021-02-21 15:01:46 【问题描述】:

我发现在 ARM 32 或 64 位 Raspberry PI 4 上并行运行 2 或 3 个线程上的 Eigen 密集矩阵乘法时性能显着下降。

我无法理解这个问题,因为 RPI 4 有 4 个内核,理论上可以处理多达 4 个线程的实际并行性。此外,我无法在我的笔记本电脑(Intel I9 4 核处理器)上重现该问题,其中每个线程都保持相同的性能,无论我并行运行一个、2 个或 3 个线程。

在我的实验中(see this repo 了解详情),我在 4 核 Raspberry Pi 4 buster 上运行不同的线程。该问题出现在 32 位和 64 位版本上。为了说明这个问题,我编写了一个程序,其中每个线程都保存自己的数据,然后使用自己的数据作为一个完全独立的处理单元来处理密集矩阵乘法:

void worker(const std::string & id) 

  const MatrixXd A = 10 * MatrixXd::Random(size, size);
  const MatrixXd B = 10 * MatrixXd::Random(size, size);
  MatrixXd C;
  double test = 0;

  for (int step = 0; step < 30; ++step) 
      test += foo(A, B, C);
  

  std::cout << "test value is:" << test << "\n";


foo 只是一个包含 100 个矩阵乘法调用的循环:

const int size = 512;

float foo(const MatrixXd &A, const MatrixXd &B, MatrixXd &C) 
  float result = 0.0;
  for (int i = 0; i < 100; ++i)
  
      C.noalias() = A * B;

      int x = 0;
      int y = 0;

      result += C(x, y);
  
  return result;

使用chrono包我发现线程循环中的每一步:

test += foo(A, B, C);

如果我只运行一个线程,则需要将近 9.000 毫秒:

int main(int argc, char ** argv)


    Eigen::initParallel();

    std::cout << Eigen::nbThreads() << " eigen threads\n";

    std::thread t0(worker, "t-0");
    t0.join();

    return 0;

当我尝试并行运行 2 个或更多线程时出现问题:

std::thread t0(worker, "t-0");
std::thread t1(worker, "t-1");

t0.join();
t1.join();

根据我的测量(详细结果可以在提到的存储库中找到),当我并行运行两个线程时,100 次乘法的每个周期需要 11.000 毫秒或更多。当我运行 3 个线程时,性能会更差(~23.000 毫秒)。

在我的笔记本电脑(Ubuntu 20.04 64 位,4x Intel I9 9900K 处理器)上进行的同一实验中,即使我只运行一个线程、两个或三个线程,每个线程的性能也几乎相同(约 1600 毫秒)。

我在这个实验中使用的代码 + 编译说明等可以在这个 repo 中找到:https://github.com/doleron/eigen3-multithread-arm-issue

编辑@Surt 答案:

为了验证@Surt 的假设,我做了一些略有不同的实验。

    运行 100 次 100,000 次 16x16 矩阵乘法循环。结果如下图所示:

    运行 100 次 100,000 次 64x64 矩阵乘法循环。结果如下图所示:

据我统计,9 个 64x64 矩阵所需的总内存缓存为:

64 x 64 x sizeof(double) x 9 = 294,912 in bytes

此内存量占 1 MiB 缓存的 28.1%,为运行到处理器内存中的其他对象留出了一些空间。 9 个矩阵是每个线程 3 个矩阵,即矩阵 A、B 和 C。注意我使用 C.noalias() = A * B; 来避免 A * B 的临时矩阵。

    运行 100 次 100,000 次矩阵 128x128 乘法循环。结果如下图所示:

9 个 128x128 矩阵的预期内存量为 1,179,648 字节,超过总可用缓存的 112%。因此,最后一种情况很可能会遇到处理器缓存瓶颈。

我认为前面图表中显示的结果证实了@Surt 假设,我会接受他/她的回答是正确的。仔细检查图表,当矩阵大小为 16 或 64 时,可能会发现 1 个单线程和 2 个或更多线程的场景之间存在细微差别。我认为这是由于一般的 OS 调度程序开销造成的。

【问题讨论】:

但是你的跑步工作量是原来的 2 到 3 倍? 您是否查看过处理器的功率预算和温度?当多个线程忙时,许多现代处理器以降低的时钟速度运行。您可能希望在运行基准测试时检查内核的时钟速度。 【参考方案1】:

基本上,您的 PI 的缓存比您的台式 PC CPU 小得多,这意味着您在 PI 上的程序比您的 PC 更频繁地发生缓存冲突。

512*512*4 (sizeof(float)) or 1 MB per instance. 

在您的 PC 上可能有 12 MB 三级缓存,您将永远无法访问 RAM(可能在分配时),而 PI 上的微小缓存将被吹走。

Raspberry Pi 4 使用带有 1.5 GHz 64 位四核 ARM Cortex-A72 处理器和 1 MiB 共享二级缓存的 Broadcom BCM2711 SoC。

因此 PI 将花费大量时间从 RAM 中提取数据。

另一方面,如果您在不同线程之间拆分相同的工作,那么您可能会看到性能有所提高(或至少没有降低),某些阻塞方案甚至可能利用 L1 缓存(在两台机器上)。

【讨论】:

非常感谢您的回答。这真的很有意义。所以,为了检验这个假设,我们可以做的一件事是减少矩阵的大小,比如说,到 32x32 乘法,然后减少共享处理器缓存中内存故障的可能性,这将使并行线程以相同的速度运行一个人的表现,对吧? @Duloren 将矩阵的总大小减少到 1 MB 以下(其他东西也需要一点缓存)

以上是关于在 ARM / Raspberry PI 上的多个内核上运行 Eigen 密集矩阵乘法时性能下降的主要内容,如果未能解决你的问题,请参考以下文章

为 ARM6 交叉编译 Node.js (Raspberry Pi)

.NET Core Docker Image for Linux-arm (Raspberry pi)

使用单声道时的 Serial.IO.Ports 问题,适用于 dotnet core 3.1 / arm / raspberry pi 4

Ubuntu 22.04 LTS (Jammy Jellyfish) Daily Build安装镜像PC ARM Raspberry Pi

在树莓派2代B型/3代 上安装Fedora23 - Installing Fedora 23 on Raspberry Pi 2 model B or Raspberry Pi 3

为 Raspberry Pi 选择交叉编译器