内存屏障是如何工作的?

4
在Windows系统中,有三个编译器内置函数用于实现内存栅栏:
1. _ReadBarrier;

2. _WriteBarrier;

3. _ReadWriteBarrier;

然而,我发现了一个奇怪的问题:_ReadBarrier似乎是一个什么都不做的虚函数!以下是VC++ 2012生成的我的汇编代码。
我的问题是:如何在汇编指令中实现内存屏障函数?
int main()
{   
013EEE10  push        ebp  
013EEE11  mov         ebp,esp  
013EEE13  sub         esp,0CCh  
013EEE19  push        ebx  
013EEE1A  push        esi  
013EEE1B  push        edi  
013EEE1C  lea         edi,[ebp-0CCh]  
013EEE22  mov         ecx,33h  
013EEE27  mov         eax,0CCCCCCCCh  
013EEE2C  rep stos    dword ptr es:[edi]  
    int n = 0;
013EEE2E  mov         dword ptr [n],0  
    n = n + 1;
013EEE35  mov         eax,dword ptr [n]  
013EEE38  add         eax,1  
013EEE3B  mov         dword ptr [n],eax  
    _ReadBarrier();
    n = n + 1;
013EEE3E  mov         eax,dword ptr [n]  
013EEE41  add         eax,1  
013EEE44  mov         dword ptr [n],eax 
}
013EEE56  xor         eax,eax  
013EEE58  pop         edi  
013EEE59  pop         esi  
013EEE5A  pop         ebx  
013EEE5B  add         esp,0CCh  
013EEE61  cmp         ebp,esp  
013EEE63  call        __RTC_CheckEsp (013EC3B0h)  
013EEE68  mov         esp,ebp  
013EEE6A  pop         ebp  
013EEE6B  ret 

2
在x86上,所有的加载都是获取操作,所有的存储都是释放操作,因此您不需要任何显式代码。唯一必要的代码是用于完整屏障。读取屏障的概念比x86架构更通用,在具有弱内存模型的机器上是非平凡的。 - Kerrek SB
1
我猜想那些仅仅是编译器屏障,用于改变编译器生成代码的方式,而不是汇编级别的屏障。 - Pubby
1
@JamesKanze:嗯,至少据我所知是这样的。不过,我可能对此有所误解。 - Kerrek SB
Linux内核SMP的一个很好的解释:http://www.kernel.org/doc/Documentation/memory-barriers.txt - 0x90
出于兴趣,看看在x86上实现Posix自旋锁的过程中是否包含任何“特殊”指令或锁前缀。 - Kerrek SB
显示剩余6条评论
3个回答

9

_ReadBarrier_WriteBarrier_ReadWriteBarrier影响编译器如何重排代码的内置函数;它们与CPU内存屏障没有任何关系,只适用于特定类型的内存(请参见此处中的“受影响的内存”)。

MemoryBarrier()是您用来强制CPU内存屏障的内置函数。然而,Microsoft建议使用std::atomic<T>


5
现代处理器能够在实际“完成”指令之前执行相当长的指令,因此使用内存屏障来防止其在某些类型的内存操作中运行得太远,需要严格排序 - 对于大多数内容,实际上不重要是否先写入变量a还是b,或者先写入b还是a。但有时确实很重要。
x86指令集有lfence、sfence和fence,它们是用于“限制”加载、存储和所有内存操作的指令。 “栅栏”或“屏障”指令的重点在于确保在栅栏指令之前的所有指令都已完成其加载、存储或两者,然后才能继续执行栅栏之后的下一条指令。
如果您正在实现例如信号量、互斥量或类似指令,则这一点非常重要,因为在继续读取其他数据之前,存储值“我已锁定信号量”很重要。否则,事情可能会出错。
请注意,除非您真正了解内存屏障的使用,否则最好不要使用它们 - 并依赖已经存在的解决同样问题的代码 - std :: atomic是一个地方可以找到这样的代码。我编写过相当多的“棘手”代码,但只有一两次需要在我的代码中使用内存屏障。
有几次,我需要让编译器不会将代码分散开来,这可以通过“不操作函数”来完成,显然现在甚至有特殊的内部函数来完成此操作。

0

有几个重要的注意事项需要考虑。也许第一个是屏障只在多线程代码中起作用,大多数编译器需要特殊选项才能生成多线程代码。而像_ReadBarrier这样的东西几乎肯定是编译器内置的,除非您已经给出了多线程代码的选项,否则应该不起作用。

第二点是硬件要求即使在多线程环境下也存在差异。在我工作过的大多数机器上(超过四十年),机器从来没有要求任何东西;只有在机器具有复杂的读写流水线时,屏障才变得相关。(大多数早期机器甚至没有栅栏或屏障指令,因此生成的代码将为空)。


内存屏障对于低级硬件访问也很重要。 - edgar.holleis
@edgar.holleis 是的。当然,这不会影响用户进程,但真正的标准不是线程,而是两个实体访问内存的可能性。线程只是最常见的出现这种情况的情况。 - James Kanze

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