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

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

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 感知的?

最佳答案

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

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

因此,解决方案是分配“原始内存”,然后首先与所有线程“接触”它,就像在处理阶段这些线程将访问它一样。不幸的是,使用 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 调度,元素到线程的映射在两个循环中应该完全相同。

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

关于C++ NUMA 优化,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/49105427/

10-11 22:59