当在共享内存环境(例如,通过 OpenMP、Pthreads 或 TBB 线程化)中执行内存带宽受限的计算时,如何确保内存在物理内存中正确分布是一个难题,这样每个线程主要访问一个“本地”内存总线。尽管接口不可移植,但大多数操作系统都有设置线程亲和性的方法(例如pthread_setaffinity_np()
,在许多 POSIX 系统sched_setaffinity()
上、LinuxSetThreadAffinityMask()
上、Windows 上)。还有诸如hwloc之类的库用于确定内存层次结构,但不幸的是,大多数操作系统还没有提供设置 NUMA 内存策略的方法。Linux 是一个明显的例外,有libnuma允许应用程序以页面粒度操作内存策略和页面迁移(自 2004 年以来在主线中,因此广泛可用)。其他操作系统希望用户遵守隐含的“第一次接触”策略。
使用“第一次接触”策略意味着调用者应该创建和分配线程,并使用他们计划稍后在首次写入新分配的内存时使用的任何亲和性。(很少有系统被配置为malloc()
实际找到页面,它只是承诺在实际出错时找到它们,可能是由不同的线程。)这意味着使用分配calloc()
或在分配使用后立即初始化内存memset()
是有害的,因为它往往会出错所有内存都在运行分配线程的内核的内存总线上,当从多个线程访问内存时,会导致最坏情况下的内存带宽。这同样适用于new
坚持初始化许多新分配的 C++ 运算符(例如std::complex
)。关于这个环境的一些观察:
- 分配可以进行“线程集体”,但现在分配会混合到线程模型中,这对于可能必须与使用不同线程模型(可能每个都有自己的线程池)的客户端交互的库来说是不可取的。
- RAII 被认为是惯用 C++ 的重要组成部分,但它似乎对 NUMA 环境中的内存性能有害。放置
new
可以与通过分配的内存malloc()
或来自的例程一起使用libnuma
,但这会改变分配过程(我认为这是必要的)。 - 编辑:我之前关于运营商的说法
new
不正确,它可以支持多个参数,请参阅 Chetan 的回复。我相信让库或 STL 容器使用指定的亲和力仍然存在问题。可能会打包多个字段,并且可能不方便确保例如std::vector
在正确上下文管理器处于活动状态的情况下进行重新分配。 - 每个线程都可以分配和故障自己的私有内存,但是索引到相邻区域会更加复杂。(考虑一个稀疏矩阵向量积矩阵和向量的行分区;索引无主的部分需要更复杂的数据结构在虚拟内存中不连续。)
NUMA 分配/初始化的任何解决方案是否被认为是惯用的?我是否遗漏了其他关键问题?
(我的 C++ 示例并不是要强调该语言,但是 C++语言编码了一些关于内存管理的决策,而 C 语言没有,因此当建议 C++ 程序员做这些时往往会有更多的阻力事情不同。)