栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 面试经验 > 面试问答

Java 8不安全:xxxFence()指令

面试问答 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java 8不安全:xxxFence()指令

摘要

CPU内核具有特殊的内存排序缓冲区,以帮助它们无序执行。这些可以(并且通常是)分开进行加载和存储:用于加载顺序缓冲区的LOB和用于存储顺序缓冲区的SOB。

为Unsafe API选择的防护操作是基于以下 假设
选择的:基础处理器将具有单独的加载顺序缓冲区(用于重新排序负载),存储顺序缓冲区(用于重新排序存储)。

因此,基于此假设,从软件角度来看,您可以向CPU请求以下三项之一:

  1. 清空LOB(loadFence):这意味着在处理完LOB的所有条目之前,没有其他指令将在此内核上开始执行。在x86中,这是LFENCE。
  2. 清空SOB(storeFence):意味着在处理完SOB中的所有条目之前,不会再有其他指令在此内核上开始执行。在x86中,这是SFENCE。
  3. 清空LOB和SOB(fullFence):表示以上两者。在x86中,这是MFENCE。

实际上,每个特定的处理器体系结构都提供了不同的内存排序保证,这些保证可能比上述要求更为严格或更灵活。例如,SPARC体系结构可以重新排序加载-存储和存储-
加载序列,而x86则不能。此外,存在无法单独控制LOB和SOB的体系结构(即仅全栅栏是可能的)。但是,在两种情况下:

  • 当体系结构更加灵活时,出于选择的考虑,API根本就不提供对“宽松”排序组合的访问

  • 当体系结构更严格时,API会在所有情况下简单地实现更严格的顺序保证(例如,实际上所有3个调用都将被实现为完全隔离)

根据Asylias提供的答案(即100%当场),在JEP中说明了选择特定API的原因。如果您了解内存排序和缓存一致性,则必须使用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提供了3种类型的防护指令:LFENCE(装入防护),SFENCE(存储防护)和MFENCE(装入防护),因此它将100%映射到Java API。

这是因为x86具有单独的加载顺序缓冲区(LOB)和存储顺序缓冲区(SOB),因此LFENCE /
SFENCE指令确实适用于相应的缓冲区,而MFENCE则适用于两者。

SOB用于存储输出值(从处理器到高速缓存系统),而高速缓存一致性协议用于获取写入高速缓存行的权限。LOB用于存储失效请求,以便失效可以异步执行(减少了接收方的停顿,希望在那里执行的代码实际上不需要该值)。

乱序商店和SFENCE

假设您有一个双处理器系统,其两个CPU 0和1执行下面的例程。考虑以下情况:高速缓存行保留

failure
最初由CPU
1拥有,而高速缓存行保留
shutdown
最初由CPU 0拥有。

// CPU 0:void shutDownWithFailure(void){  failure = 1; // must use SOB as this is owned by CPU 1  shutdown = 1; // can execute immediately as it is owned be CPU 0}// CPU1:void workLoop(void){  while (shutdown == 0) { ... }  if (failure) { ...}}

在没有存储栅栏的情况下,CPU 0可能由于故障而发出关闭信号,但是CPU 1将退出循环,并且如果阻塞则不会进入故障处理。

这是因为CPU0会将值1写入

failure
存储顺序缓冲区,同时还会发出缓存一致性消息以获取对缓存行的独占访问权。然后它将继续执行下一条指令(在等待独占访问时)并
shutdown
立即更新标志(该高速缓存行已由CPU0独占,因此无需与其他内核进行协商)。最后,当它稍后从CPU1接收到一个无效确认消息(关于
failure
)时,它将继续处理SOB
failure
并将其值写入高速缓存(但是现在顺序相反)。

插入storeFence()将解决问题:

// CPU 0:void shutDownWithFailure(void){  failure = 1; // must use SOB as this is owned by CPU 1  SFENCE // next instruction will execute after all SOBs are processed  shutdown = 1; // can execute immediately as it is owned be CPU 0}// CPU1:void workLoop(void){  while (shutdown == 0) { ... }  if (failure) { ...}}

最后值得一提的是x86具有存储转发功能:当CPU写入卡在SOB中的值(由于高速缓存一致性)时,它随后可能会尝试在SOB之前对同一地址执行加载指令。处理并交付给缓存。因此,CPU将在访问缓存之前先咨询SOB,因此在这种情况下检索到的值是从SOB中最后写入的值。
这意味着THIS核心的存储无论如何都无法与THIS核心的后续加载进行重新排序

乱序负载和LFENCE

现在,假设您已经安装了存储栅栏,并且很高兴

shutdown
不会超过
failure
其进入CPU
1的路径,而将注意力集中在另一侧。即使存在商店围栏,在某些情况下也会发生错误的事情。考虑
failure
在两个高速缓存(共享)中
shutdown
都存在,而仅在CPU0的高速缓存中存在且仅由CPU0高速缓存拥有的情况。坏事情可能会发生如下:

  1. CPU0将1写入
    failure
    ; 它还作为高速缓存一致性协议的一部分,向CPU1发送一条消息,以使其共享高速缓存行的副本无效
  2. CPU0执行SFENCE并停顿,等待用于
    failure
    提交的SOB 。
  3. shutdown
    由于while循环,CPU1进行了检查,并(意识到它缺少该值)发送了一个缓存一致性消息来读取该值。
  4. 在步骤1中,CPU1从CPU0接收到使该消息无效的消息,并为其
    failure
    发送立即确认。 注意:这是使用失效队列实现的,因此实际上它只是输入一个注释(在其LOB中分配一个条目)以稍后进行失效,但实际上在发送确认之前并不执行该失效。
  5. CPU0收到确认,
    failure
    并经过SFENCE转到下一条指令
  6. CPU0在不使用SOB的情况下将1写入关闭状态,因为它已专门拥有高速缓存行。 由于缓存行是CPU0专用的,因此不会发送额外的无效消息
  7. CPU1接收该
    shutdown
    值并将其提交到其本地缓存,然后继续进行下一行。
  8. CPU1检查
    failure
    if语句的值,但是由于尚未处理无效队列(LOB注释),因此它使用其本地缓存中的值0(如果阻塞则不输入)。
  9. CPU1处理无效队列并更新
    failure
    为1,但是为时已晚…

我们所谓的加载顺序缓冲区实际上是无效请求的排队,并且可以通过以下方法解决上述问题:

// CPU 0:void shutDownWithFailure(void){  failure = 1; // must use SOB as this is owned by CPU 1  SFENCE // next instruction will execute after all SOBs are processed  shutdown = 1; // can execute immediately as it is owned be CPU 0}// CPU1:void workLoop(void){  while (shutdown == 0) { ... }  LFENCE // next instruction will execute after all LOBs are processed  if (failure) { ...}}

您在x86上的问题

现在您知道SOB / LOB的功能,请考虑您提到的组合:

loadFence() becomes load_loadstoreFence();

不,负载防护栏等待处理LOB,实际上是清空了失效队列。这意味着所有后续加载都将看到最新数据(无需重新排序),因为它们将从缓存子系统中获取(它们是连贯的)。存储CANNNOT会因后续加载而重新排序,因为它们不会通过LOB。(此外,存储转发还负责本地修改的缓存行)从此特定内核(执行负载隔离的内核)的角度来看,在所有寄存器加载完数据后,将在负载隔离之后执行存储。没有其他办法了。

load_storeFence() becomes ???

不需要load_storeFence,因为它没有意义。要存储某些内容,您必须使用输入进行计算。要获取输入,您必须执行加载。将使用从加载中获取的数据进行存储。如果要确保在加载时看到所有其他处理器的最新值,请使用loadFence。对于围栏之后的货物,存储转发要注意保持一致的顺序。

所有其他情况都是相似的。


SPARC

SPARC更加灵活,可以通过后续加载(以及后续存储的加载)对存储进行重新排序。我对SPARC不太熟悉,所以我的 GUESS
是没有存储转发(重新加载地址时未咨询SOB),因此可能出现“脏读”的情况。实际上,我错了:我在[3]中发现了SPARC体系结构,实际上是存储转发是线程化的。从5.3.4节开始:

所有加载都会检查存储缓冲区(仅同一线程)是否存在写入后读取(RAW)的危害。当加载的双字地址与STB中存储的双字地址匹配并且加载的所有字节在存储缓冲区中有效时,就会发生完整RAW。当双字地址匹配但所有字节在存储缓冲区中无效时,将发生部分RAW。(例如,ST(字存储)后跟LDX(双字加载)到相同的地址会导致部分RAW,因为完整的双字不在存储缓冲区条目中。)

因此,不同的线程会查询不同的存储顺序缓冲区,因此有可能在存储后进行脏读。


参考文献

[1]内存壁垒:针对软件黑客的硬件视图,Linux技术中心,IBM Beaverton
http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.07.23a.pdf

[2]英特尔®64和IA-32体系结构软件开发人员手册,第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核心微体系结构规范http://www.oracle.com/technetwork/systems/opensparc/t2-06-opensparct2-core-
microarch-1537749.html



转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/469787.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号