内存屏障是如何工作的?

Posted

技术标签:

【中文标题】内存屏障是如何工作的?【英文标题】:How does memory barrier work? 【发布时间】:2013-01-28 22:03:55 【问题描述】:

在 Windows 下,有 3 个编译器内部函数来实现内存屏障:

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 

【问题讨论】:

在 x86 上,所有加载都是获取,所有存储都是发布,因此您不需要任何显式代码。唯一必要的代码是完整的屏障。读屏障的概念比 x86 架构更通用,并且在内存模型较弱的机器上并不重要。 我猜这些只是改变编译器生成代码方式的编译器障碍,而不是汇编级障碍。 @KerrekSB 这是真的吗,即使对于最新的机器?这样做会大大减慢速度,并消除拥有多个内核的许多效用(至少对于某些类型的应用程序而言)。 @JamesKanze:嗯,至少据我所知。不过,我完全有可能搞错了。 对linux内核smpkernel.org/doc/Documentation/memory-barriers.txt987654321@的一个很好的解释 【参考方案1】:

_ReadBarrier_WriteBarrier_ReadWriteBarrier 是 intrinsics that affect how the compiler can reorder code;它们与 CPU 内存屏障完全无关,仅对特定类型的内存有效(请参阅“受影响的内存”here)。

MemoryBarrier() 是用于强制 CPU 内存屏障的内在函数。但是,Microsoft 的建议是在 VC++ 中使用std::atomic<T>

【讨论】:

【参考方案2】:

现代处理器能够比它实际“完成”指令的位置提前很长的距离执行指令,因此当涉及某些类型的内存操作时,内存屏障用于防止它运行到很远的位置,其中需要严格的排序 - 对于大多数事情,实际上是在变量 b 之前写入变量 a 还是在 a 之前写入 b 实际上并不重要。但有时确实如此。

x86 指令集有lfencesfencefence,它们分别是“围栏”加载、存储和所有内存操作的指令。关于“栅栏”或“屏障”指令的要点是确保屏障指令之前的所有指令在屏障之后的下一条指令可以继续之前完成它们的加载、存储或两者。

如果您正在实现例如信号量、互斥锁或类似指令,这一点很重要,因为在继续读取其他数据之前存储“我已锁定信号量”的值很重要。否则事情可能会出错,让我们说。

请注意,除非您真的知道自己在使用内存屏障做什么,否则最好不要使用它们 - 并依赖解决相同问题的现有代码 - std::atomic 是资助此类代码的一个地方。我写了很多“棘手”的代码,但只有一两次我的代码需要内存屏障。

有好几次,我需要让编译器不散布代码,你可以使用“无操作函数”来做到这一点,而且显然现在甚至有特殊的内在函数可以做到这一点。

【讨论】:

【参考方案3】:

有几个要点需要考虑。也许 首先是障碍只在多线程中起作用 代码,并且大多数编译器需要一个特殊的选项来生成 多线程代码。而像_ReadBarrier 这样的东西几乎 当然是编译器内置的,应该什么都不做,除非 您已经给出了多线程代码的选项。

第二个是硬件要求,即使在一个 多线程上下文,变化。在我用过的大多数机器上 工作(超过四十年),这台​​机器从来不需要 任何事物;仅当机器具有 复杂的读写管道。 (大多数早期的机器 甚至没有栅栏或障碍指令,所以生成的 代码必须为空。)

【讨论】:

内存屏障也与低级硬件访问有关。 @edgar.holleis 是的。当然,这不会影响用户进程,但真正的标准不是线程,而是两个实体访问内存的可能性。线程只是发生这种情况的最常见情况。

以上是关于内存屏障是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章

内存屏障

线程之间的内存栅栏/屏障如何与其他线程中的栅栏/屏障交互?

我如何理解读内存屏障和易失性

Linux 内核 内存管理优化内存屏障 ④ ( 处理器内存屏障 | 八种处理器内存屏障 | 通用内存屏障 | 写内存屏障 | 读内存屏障 | 数据依赖屏障 | 强制性内存屏障 |SMP内存屏障 )

Linux 内核 内存管理优化内存屏障 ④ ( 处理器内存屏障 | 八种处理器内存屏障 | 通用内存屏障 | 写内存屏障 | 读内存屏障 | 数据依赖屏障 | 强制性内存屏障 |SMP内存屏障 )

谈乱序执行和内存屏障