首先,在任何人抱怨之前,我意识到从理论上完美的 C++ 代码的角度来看,内存模型是一个我不应该依赖的实现细节。然而,我更喜欢表现而不是纪律。
场景如下:我有一个地址空间区域,我告诉操作系统用我选择的文件支持它——也就是说,该文件是内存映射的。如果我对 VMM 通常如何工作的理解是正确的,那么操作系统可能会非常懒惰地将页面加载到我的映射中,并且可能只有在页面实际被触及时才会这样做。
通常我可以忽略这个细节,但在这种特殊情况下,我将映射数据发送到工作线程池中。如果我只是天真地向工作线程传递一个指向该缓冲区的指针,那么工作线程本身很可能会在第一次触摸页面时遇到页面错误,这将导致工作线程阻塞,直到页面物理上由 VMM 加载。
工作池的设计使得它的线程在 I/O 上阻塞是非常糟糕的,而在作业中发送的线程可以容忍被阻塞。因此,我想让我的发件人线程首先触摸映射的页面,以便页面错误将阻止它。
(我知道,不能保证首先触摸页面会停止工作线程中的后续页面错误,但程序在大多数情况下仍然是最佳的,并且一直都是正确的。)
在 x86 汇编语言中,这将是微不足道的:
; get the page's address in ebx
mov al, Byte Ptr [ebx]
不幸的是,在 C 或 C++ 中它并不是那么简单。一个天真的实现很简单:
char *pPage = ...;
char Dummy = *pPage;
但是,这可能行不通,因为任何有自尊的优化器都会意识到代码什么都不做,只是简单地忽略它。
我们可以使用内联汇编,但这可能会严重削弱优化器。我们可以调用一个汇编语言函数来完成它,但是我们有(诚然很小)不必要的函数调用开销。
我们可以改为创建Dummy
一个外部可见的变量,这会起作用,因为编译器不能假定赋值是没有意义的。但是,这可能会导致 CPU 缓存行的争用,从而严重降低多核系统的性能Dummy
。(更不用说,我们浪费了缓存行和访问。)
我也想过这样做:
char volatile *pPage = ...;
char Dummy = *pPage;
我知道volatile
关键字有两个保证:
编译器不会重新排序访问;和
编译器不会假定连续读取之间的值相同。
但是,这似乎并不能保证编译器会读取该值,即使它不需要它。
有任何想法吗?