【问题标题】:C++ NUMA OptimizationC++ NUMA 优化
【发布时间】:2018-08-12 19:28:06
【问题描述】:

我正在开发一个最初为多核处理器系统开发的遗留应用程序。为了利用多核处理,已使用 OpenMP 和 PPL。 现在一项新要求是在具有多个 NUMA 节点的系统上运行该软件。目标操作系统是 Windows 7 x64。

我进行了几次测量,发现在将应用程序分配给单个 NUMA 节点时执行时间已达到最佳状态,因此浪费了一个完整的处理器。应用程序的许多部分执行数据并行算法,例如并行处理向量的每个元素,并将结果写入另一个向量,如下例所示

std::vector<int> data;
std::vector<int> res;

// init data and res

#pragma omp parallel for
for (int i = 0; i < (int) data.size(); ++i)
{  
  res[i] = doExtremeComplexStuff(data[i]);
}

据我所知,此类算法的性能下降是由第二个 NUMA 节点的非本地内存访问引起的。所以问题是如何让应用程序性能更好。

是否以某种方式透明地加速了对非本地内存的只读访问(例如,通过操作系统将数据从一个节点的本地内存复制到另一个节点的本地内存)? 我是否必须拆分问题大小并将输入数据复制到相应的 NUMA 节点,对其进行处理,然后再次合并所有 NUMA 节点的数据以提高性能?

如果是这种情况,是否有 std 容器的替代品,因为它们在分配内存时不支持 NUMA?

【问题讨论】:

  • 我刚好在浏览器中打开了这篇论文:cs.brown.edu/~irina/papers/asplos2017-final.pdf
  • 您尝试过不同的numactl 策略吗? numactl --interleave=all 有时会有所帮助。
  • 您在询问操作系统 numa 策略时甚至没有告诉我们您的操作系统(版本)、有关您的硬件的任何信息,并且只提供极少的关于您的代码的信息。任何答案都必须对您的设置进行疯狂的猜测。你的测量是一个好的开始,但你必须更深入地挖掘才能真正查明瓶颈。即使是关于 NUMA 处理最佳实践的精彩而详细的答案也可能对您没有丝毫帮助......
  • 请注意,如果您的所有循环都像您描述的那样,那么您将没有 NUMA 问题,因为 OpenMP 保证重复循环,即使具有不同的主体,将在线程之间以相同的方式分配索引,如果大小是一样的。
  • @Tyson 不幸的是我没有想出更好的解决方案。在我的情况下,两个多线程、重负载应用程序同时运行,因此我将每个进程的线程亲和性分别设置为一个 NUMA 节点。因此,每个进程的线程都被排他地调度到一个 NUMA 节点。这在不修改任何现有代码库的情况下提供了更好的整体性能。

标签: c++ parallel-processing openmp numa


【解决方案1】:

当您分配动态内存时(例如std::vector),您可以有效地从虚拟内存空间中获取一定范围的页面。当程序第一次访问特定页面时,会触发页面错误并请求物理内存中的某些页面。通常,该页面位于生成页面错误的内核的本地物理内存中,这称为 first touch 策略。

在您的代码中,如果您的 std::vector 缓冲区的页面首先被单个(例如,主)线程触及,那么这些向量的所有元素可能最终都在单个 NUMA 的本地内存中节点。然后,如果您将程序拆分为在所有 NUMA 节点上运行的线程,则某些线程在使用这些向量时会访问远程内存。

因此,解决方案是分配“原始内存”,然后首先用所有线程“触摸”它,就像在处理阶段这些线程将访问它一样。不幸的是,使用std::vector 实现这一点并不容易,至少使用标准分配器是这样。能不能换成普通的动态数组?我会先试试这个,看看他们对第一次触摸策略的初始化是否有帮助:

int* data = new int[N];
int* res = new int[N];

// initialization with respect to first touch policy
#pragma omp parallel for schedule(static)
for (int i = 0; i < N; i++) {
   data[i] = ...;
   res[i] = ...;
}

#pragma omp parallel for schedule(static)
for (int i = 0; i < N; i++)
   res[i] = doExtremeComplexStuff(data[i]);

使用static 调度,元素到线程的映射在两个循环中应该完全相同。


但是,我不相信您的问题是由访问这两个向量时的 NUMA 效应引起的。当您调用函数doExtremeComplexStuff 时,看起来这个函数对于运行时来说非常昂贵。如果这是真的,与函数调用相比,即使访问远程 NUMA 内存也可能快得可以忽略不计。整个问题可以隐藏在这个函数中,但我们不知道它做了什么。

【讨论】:

  • 这是一个很好的答案,但您仍然主要是在猜测答案。这也只能接触表面,例如有透明的 NUMA 平衡...
  • @Zulan 你的意思可能是猜测问题。我完全同意。
  • 使用可变长度数组代替std::vector真的是个好主意吗? std::vector&lt;int*&gt; 可以完成这项工作吗?
  • @LuoJigao 这里不涉及VLA。 VLA 是不同的。您也可以使用向量,但问题是使用默认分配器时,它会在调整大小时将元素清零。这正是我们想要避免的。为简单起见,此处使用动态数组,编写自定义分配器以跳过元素的零初始化是相对复杂且不相关的主题。
猜你喜欢
  • 2015-07-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-24
  • 1970-01-01
  • 2012-11-13
  • 1970-01-01
相关资源
最近更新 更多