如何故意触摸内存页面?

Kev*_*vin 3 c c++ multithreading memory-mapped-files

首先,在有人抱怨之前,我意识到在理论上完美的 C++ 代码的角度来看,内存模型是一个我不应该依赖的实现细节。然而,我更喜欢表现而不是纪律。

场景是这样的:我有一个地址空间区域,我已经告诉操作系统用我选择的文件来支持它——也就是说,该文件是内存映射的。如果我对 VMM 通常如何工作的理解是正确的,那么操作系统可能会非常懒惰地将页面加载到我的映射中,并且可能仅在页面实际被触摸时才这样做。

通常我可以忽略这个细节,但在这种特殊情况下,我将映射的数据发送到工作线程池中。如果我只是天真地将指向此缓冲区的指针传递给工作线程,那么当第一次触摸页面时,工作线程本身很有可能会遇到页面错误,这将导致工作线程阻塞,直到页面物理上由 VMM 加载。

工作池的设计使得它的线程在 I/O 上阻塞是非常糟糕的,而在作业中发送的线程可以容忍被阻塞。因此,我想让我的发件人线程首先接触映射的页面,以便页面错误会阻止它。

(我知道不能保证首先触摸页面会停止工作线程中的后续页面错误,但该程序仍然会在大多数时间处于最佳状态并始终正确。)

在 x86 汇编语言中,这很简单:

; get the page's address in ebx
mov al, Byte Ptr [ebx]
Run Code Online (Sandbox Code Playgroud)

不幸的是,它在 C 或 C++ 中并不是那么简单。一个简单的实现很简单:

char *pPage = ...;
char Dummy = *pPage;
Run Code Online (Sandbox Code Playgroud)

但是,这可能行不通,因为任何有自尊的优化器都会意识到代码什么都不做,只是简单地省略了它。

我们可以使用内联汇编,但这可能会严重削弱优化器。我们可以调用一个汇编语言函数来做到这一点,但是我们有(无可否认的小)函数调用开销是不必要的。

我们可以改为创建Dummy一个外部可见的变量,这会起作用,因为编译器不能假设赋值是无意义的。但是,这可能会导致 CPU 缓存线保持的争用,从而严重降低多核系统的性能Dummy。(更不用说,我们浪费了缓存行和访问权限。)

我也想过这样做:

char volatile *pPage = ...;
char Dummy = *pPage;
Run Code Online (Sandbox Code Playgroud)

我知道volatile关键字有两个保证:

  • 编译器不会重新排序访问;和

  • 编译器不会假设连续读取之间的值相同。

但是,这似乎并不能保证编译器即使不需要它也会读取该值。

有任何想法吗?

Dav*_*veR 5

volatile 根据定义保证执行内存访问,因此一个简单的解决方案正是您所建议的:

volatile char *prefetch_me = ...;
(void)*prefetch_me;
Run Code Online (Sandbox Code Playgroud)

但是,如果您想以(可能)更有效的方式(并且您在 *ix 系统上运行)触摸多个页面,请查看madvise(),特别是MADV_WILLNEED和/或MADV_SEQUENTIAL。从手册页:

  • MADV_WILLNEED- 预计在不久的将来访问。(因此,提前阅读几页可能是个好主意。)
  • MADV_SEQUENTIAL- 期望按顺序引用页面。(因此,给定范围内的页面可以被积极地提前读取,并且可能会在它们被访问后很快被释放。)

  • 确实“不稳定”起作用了。关于“madvise()”的有趣注释也可能很好。请注意,我不希望它在加载完成之前一定会阻塞,因此它与触摸页面并不完全相同。我想知道是否有一个与“madvise()”等效的 WinAPI。 (2认同)