C++ NUMA 优化

5
我正在处理一个最初为多核处理器系统开发的传统应用程序。为了利用多核处理,使用了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节点的数据组合起来以提高性能吗?
如果是这种情况,那么除了标准容器外,是否有其他选择,因为在分配内存时它们不考虑NUMA。

1
我碰巧在浏览器中打开了这篇论文:https://cs.brown.edu/~irina/papers/asplos2017-final.pdf - lockcmpxchg8b
你在询问有关OS NUMA策略的问题,却没有告诉我们你的操作系统(版本),硬件情况以及代码方面的信息非常少。任何回答都必须对你的设置进行猜测。你的测量方式是一个很好的起点,但你需要深入挖掘才能真正找到瓶颈所在。即使是关于最佳NUMA处理实践的精彩详细答案也可能对你毫无帮助... - Zulan
@Zulan 注意,这仅适用于“静态”调度,甚至可能不是默认的调度策略。 - Daniel Langr
@Lukas,我最近也遇到了NUMA问题...与您在此帖子中遇到的问题非常相关(std容器的线程访问)。您是否找到了解决这个问题的方法?下面标记为答案的“首次触摸”政策是否是您最终采用的解决方案? - Tyson
1
@Tyson 很抱歉,我没有想出更好的解决方案。在我的情况下,两个多线程、高负载的应用程序同时运行,因此我选择将每个进程的线程亲和性设置为一个 NUMA 节点。这样,每个进程的线程都会被独占地调度到一个 NUMA 节点上。这样做可以在不修改现有代码库的情况下获得更好的整体性能。 - Quxflux
显示剩余4条评论
1个回答

8
当你分配动态内存(如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]);

使用静态调度,元素到线程的映射在两个循环中应该是完全相同的。
然而,我并不认为访问这两个向量时NUMA效应导致了您的问题。因为您调用了`doExtremeComplexStuff`函数,看起来这个函数在运行时非常耗时。如果是这样,即使访问远程NUMA内存,与函数调用相比也很可能忽略不计地快。整个问题可能隐藏在这个函数内部,但我们不知道它具体做了什么。

这是一个非常好的答案,但你仍然大多数是在猜测答案。这也只能触及表面,例如有透明的NUMA平衡... - Zulan
@Zulan 你可能是指_猜测问题_。我完全同意。 - Daniel Langr
使用变长数组代替std::vector真的是一个好主意吗?std::vector<int*>能够胜任这项工作吗? - Jigao Luo
1
@LuoJigao 这里没有涉及到VLA。VLA是另一回事。你也可以使用vector,但问题在于默认分配器会在调整大小时将元素清零。而这正是我们想要避免的。动态数组之所以被用在这里,是为了简单起见。编写一个自定义分配器来跳过元素的零初始化相对复杂且无关紧要。 - Daniel Langr
@DanielLangr 我很好奇您的回答,因为我正在处理一个类似的问题。一个简单的例子:我有一个包含6个元素的动态数组,而一个内存页面只能容纳四个元素。我使用两个线程在不同的NUMA节点上写入数据。第一个线程是否将3个元素写入节点1的物理内存页面?第二个NUMA节点是否将第4个元素写入节点1的内存中?还是它们分别写入每个页面,每个页面有3个元素?我正在使用Linux。 - user151387
1
@user151387 虚拟地址空间是连续的。如果页面有足够的空间容纳4个元素,我认为系统不可能将3个元素映射到第一页,另外3个元素映射到第二页。 - Daniel Langr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接