我正在研究最初为多核处理器系统开发的遗留应用程序。为了利用多核处理,已经使用了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节点的非本地内存访问引起的。所以问题是如何使应用程序更好地运行。
是否以某种方式透明地加速了对非本地存储器的只读访问(例如,OS将数据从一个节点的本地存储器复制到另一个节点的本地存储器)?我是否必须拆分问题大小并将输入数据复制到相应的NUMA节点,处理它,然后再次组合所有NUMA节点的数据以提高性能?
如果是这种情况,是否有std容器的替代品,因为在分配内存时这些不是NUMA感知的?
当您分配动态内存(例如std::vector
)时,您可以有效地从虚拟内存空间获取一些范围的页面。当程序首次访问特定页面时,会触发页面错误并请求物理内存中的某些页面。通常,此页面位于生成页面错误的核心的本地物理内存中,这称为第一个触摸策略。
在您的代码中,如果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内存也可能是可以忽略不计的。整个问题可以隐藏在这个函数中,但我们不知道它的作用。