CPU核心具有特殊的内存顺序缓冲区,以帮助它们进行乱序执行。这些缓冲区可以是(通常是)用于加载和存储的独立的:载入顺序缓冲区(LOB)和存储顺序缓冲区(SOB)。
因此,基于此假设,从软件角度来看,您可以向CPU请求以下三种事情之一:
- 清空LOB(loadFence):意味着在LOB中的所有条目被处理之前,该核心上不会启动任何其他指令。在x86中,这是一个LFENCE。
- 清空SOBs(storeFence):意味着在SOB中的所有条目被处理之前,在该核心上不会启动任何其他指令。在x86中,这是一个SFENCE。
- 清空LOB和SOB(fullFence):即上述两种情况。在x86中,这是一个MFENCE。
实际上,每个特定的处理器体系结构都提供不同的内存顺序保证,这些保证可能比上述更严格或更灵活。例如,SPARC体系结构可以重新排序加载存储和存储加载序列,而x86不会这样做。此外,存在一些体系结构,其中LOB和SOB无法单独控制(即只能使用全栅栏)。在这两种情况下:
选择特定API的原因在JEP中解释,正如assylias所提供的答案完全准确。如果您了解内存顺序和缓存一致性,那么assylias的答案就足够了。我认为它们与C++ API中的标准指令匹配是一个重要因素(大大简化了JVM的实现):http:// en.cppreference.com / w / cpp / atomic / memory_order 很可能实际实现将调用相应的C ++ API而不是使用某些特殊指令。
下面有一个基于x86的详细说明,其中提供了理解这些内容所需的所有上下文。实际上,以下部分(以下部分回答了另一个问题:“您能够提供有关内存栅栏如何在x86体系结构中控制缓存一致性的基本示例吗?”)
因为我自己是一名软件开发者而不是硬件设计师,所以我曾经很难理解什么是内存重排序,直到我学习了如何在x86中实际工作的缓存一致性的具体示例。这为讨论内存栅栏在一般情况下(也适用于其他架构)提供了宝贵的上下文。最后,我使用从x86示例中获得的知识稍微讨论了SPARC。
参考文献[1]提供了更详细的解释,并为讨论以下每个方面提供了单独的部分:x86、SPARC、ARM和PowerPC,因此如果您对更多细节感兴趣,它是一个非常好的阅读材料。
x86体系结构示例
x86提供三种类型的栅栏指令:LFENCE(加载栅栏)、SFENCE(存储栅栏)和MFENCE(加载-存储栅栏),因此它与Java API完全对应。
这是因为x86具有单独的加载顺序缓冲区(LOB)和存储顺序缓冲区(SOB),因此LFENCE / SFENCE指令确实适用于相应的缓冲区,而MFENCE适用于两者。
SOB用于存储传出值(从处理器到缓存系统),而缓存一致性协议工作以获取写入缓存行的权限。LOB用于存储无效请求,以便无效可以异步执行(减少接收端的停顿,希望在那里执行的代码实际上不需要该值)。
乱序存储和SFENCE
假设您有一个双处理器系统,其两个CPU 0和1执行以下例程。考虑缓存线最初由CPU 1拥有的持有“failure”的情况,而缓存线最初由CPU 0拥有的持有“shutdown”的情况。
void shutDownWithFailure(void)
{
failure = 1;
shutdown = 1;
}
void workLoop(void)
{
while (shutdown == 0) { ... }
if (failure) { ...}
}
在没有存储栅栏的情况下,CPU 0 可能会因故障发出关闭信号,但 CPU 1 将退出循环并且不会进入故障处理 if 块。这是因为 CPU 0 将向存储排序缓冲区中写入值为 1 的 failure,并发送缓存一致性消息以获取对缓存行的独占访问权限。然后它将继续执行下一条指令(同时等待独占访问),立即更新 shutdown 标志(该缓存行已被 CPU 0 独占所有,因此无需与其他内核进行协商)。最后,当它稍后收到来自 CPU1 的失效确认消息(关于失败)时,它将继续处理 failure 的 SOB 并将该值写入缓存(但此时顺序已经反转)。插入 storeFence() 将修复问题:
void shutDownWithFailure(void)
{
failure = 1;
SFENCE
shutdown = 1;
}
void workLoop(void)
{
while (shutdown == 0) { ... }
if (failure) { ...}
}
值得一提的最后一个方面是,x86具有存储转发功能:当CPU写入的值被卡住(由于缓存一致性)而被暂存在SOB中时,它可能随后尝试在SOB被处理并传递到缓存之前执行相同地址的load指令。因此,CPU在访问缓存之前会首先查看SOB,因此在这种情况下检索到的值是来自SOB中最后写入的值。这意味着来自此核心的存储永远不会与随后的来自此核心的加载重新排序,无论如何都不会。
乱序加载和LFENCE
现在,假设你已经设置了存储栅栏,并且确信shutdown
不能在到达CPU 1之前超过failure
,并关注另一侧。即使存在存储栅栏,仍然存在错误情况。考虑这样的情况:failure
存在于两个缓存中(共享),而shutdown
仅存在于CPU0的缓存中并且由其独占拥有。以下情况可能会发生:
- CPU0向
failure
写入1;它还发送一条消息给CPU1,作为缓存一致性协议的一部分,来使其失效共享缓存行的副本。
- CPU0执行SFENCE并暂停,等待用于
failure
的SOB提交。
- CPU1由于while循环而检查
shutdown
(意识到缺少该值),发送一个缓存一致性消息以读取该值。
- CPU1收到来自CPU0的步骤1中使
failure
无效的消息,并立即对其进行确认。注意:这是使用失效队列实现的,因此实际上它仅输入一个注释(在其LOB中分配一个条目),以稍后执行失效,但在发送确认之前并不会实际执行失效操作。
- CPU0接收到
failure
的确认并通过SFENCE继续执行下一条指令。
- CPU0向
shutdown
写入1,而不使用SOB,因为它已经独占拥有缓存行。不发送额外的失效消息,因为缓存行专属于CPU0。
- CPU1接收到
shutdown
的值并将其提交给其本地缓存,然后继续执行下一行。
- CPU1检查if语句中的
failure
值,但由于失效队列(LOB注释)尚未处理,它使用来自其本地缓存的值0(不进入if块)。
- CPU1处理失效队列并将
failure
更新为1,但现在已经太迟了......
我们所谓的加载顺序缓冲区实际上是无效请求的排队,可以通过以下方式修复:
void shutDownWithFailure(void)
{
failure = 1;
SFENCE
shutdown = 1;
}
void workLoop(void)
{
while (shutdown == 0) { ... }
LFENCE
if (failure) { ...}
}
关于x86的问题
现在你已经知道SOB/LOB是什么了,考虑一下你提到的组合:
loadFence() becomes load_loadstoreFence()
不,装载屏障会等待大型对象(LOBs)被处理,从而清空失效队列。这意味着所有后续的装载将看到最新的数据(没有重新排序),因为它们将从缓存子系统(具有一致性)中获取。存储不能与后续的装载重新排序,因为它们不经过LOB。 (而且存储转发会处理本地修改的高速缓存行)。从此特定核心的视角来看(执行装载屏障的核心),在所有寄存器加载数据之后,跟随装载屏障的存储将执行。这是无法避免的。
load_storeFence() becomes ???
针对这种情况,没有必要使用load_storeFence,因为它没有意义。要存储某些内容,您必须使用输入进行计算。要获取输入,您必须执行加载操作。存储将使用从加载操作中获取的数据发生。如果要确保在加载操作后查看来自所有其他处理器的最新值,请使用loadFence。加载操作在栅栏之后,存储转发会处理一致排序。
所有其他情况类似。
SPARC
SPARC甚至更灵活,并可以重新排序存储和随后的加载(以及加载和随后的存储)。我不太熟悉SPARC,所以我的猜测是没有store-forwarding(当重新加载地址时不会使用SOBs),因此可能存在“脏读”的情况。实际上我错了:在[3]中找到了SPARC体系结构,并且现实是store-forwarding是线程化的。来自第5.3.4节:
所有加载操作都检查存储缓冲区(仅相同线程)是否存在read after write(RAW)危险。当加载操作的dword地址与STB中存储的地址匹配且加载操作的所有字节都在存储缓冲区中时,发生全局RAW。当dword地址匹配但存储缓冲区中未全部字节都有效时,发生部分RAW。(例如,对同一地址的ST(字存储)后跟LDX(dword加载)会导致部分RAW,因为完整的dword不存在于存储缓冲区条目中。)
因此,不同的线程会查找不同的存储顺序缓冲区,因此在存储操作之后可能存在脏读。
参考资料
[1] Memory Barriers: a Hardware View for Software Hackers, Linux Technology Center, IBM Beaverton
http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.07.23a.pdf
[2] Intel® 64 and IA-32 ArchitecturesSoftware Developer’s Manual, Volume 3A
http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf
[3] OpenSPARC T2 Core Microarchitecture Specification http://www.oracle.com/technetwork/systems/opensparc/t2-06-opensparct2-core-microarch-1537749.html