在Java 8中,向Unsafe类(source)添加了三个内存屏障指令:

/**
 * Ensures lack of reordering of loads before the fence
 * with loads or stores after the fence.
 */
void loadFence();

/**
 * Ensures lack of reordering of stores before the fence
 * with loads or stores after the fence.
 */
void storeFence();

/**
 * Ensures lack of reordering of loads or stores before the fence
 * with loads or stores after the fence.
 */
void fullFence();
如果我们通过以下方式定义内存屏障(我认为或多或少容易理解):

现在,我们可以将屏障名称从Unsafe“映射”到此术语:
  • loadFence()变成load_loadstoreFence();
  • storeFence()变成store_loadStoreFence();
  • fullFence()变成loadstore_loadstoreFence();

  • 最后,我的问题是-为什么我们没有load_storeFence()store_loadFence()store_storeFence()load_loadFence()
    我的猜测是-他们并不是真正的必要,但我目前不明白为什么。因此,我想知道不添加原因的原因。对此的猜想也很受欢迎(但是希望这不会导致该问题成为基于观点的话题)。
    提前致谢。

    最佳答案

    概要

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

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

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

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

  • 实际上,每个特定的处理器体系结构都提供了不同的内存排序保证,这些保证可能比上述要求更为严格或更灵活。例如,SPARC体系结构可以重新排序加载-存储和存储-加载序列,而x86则不能。此外,存在无法单独控制LOB和SOB的体系结构(即,仅全栅栏是可能的)。但是,在两种情况下:
  • ,当体系结构更加灵活时,出于选择的考虑,API根本不提供对“laxer”排序组合的访问
  • 当架构更加严格时,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用于存储传出值(从处理器到高速缓存系统),而高速缓存一致性协议(protocol)用于获取写入高速缓存行的权限。 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会将failure的值1写入存储顺序缓冲区,同时还会发出缓存一致性消息以获取对缓存行的独占访问权。然后它将继续执行下一条指令(在等待互斥访问的同时)并立即更新shutdown标志(该高速缓存行已由CPU0独占,因此无需与其他内核进行协商)。最终,当它稍后从CPU1接收到一个无效确认消息(关于failure)时,它将继续处理failure的SOB并将值写入高速缓存(但是现在顺序相反)。

    插入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在进入CPU 1的过程中不能超过failure,而将注​​意力集中在另一侧。即使存在商店围栏,在某些情况下也会发生错误的事情。考虑到failure在两个缓存中(共享),而shutdown仅存在于CPU0的缓存中且仅由CPU0缓存拥有的情况。坏事情可能会发生如下:
  • CPU0将1写入failure;它还作为高速缓存一致性协议(protocol)的一部分,向CPU1发送一条消息,以使其共享高速缓存行的副本无效。
  • CPU0执行SFENCE并停顿,等待failure使用的SOB提交。
  • CPU1由于while循环而检查shutdown,并且(意识到它缺少该值)发送高速缓存一致性消息以读取该值。
  • CPU1在步骤1中从CPU0接收消息,以使failure无效,并为其立即发送确认。注意:这是使用失效队列实现的,因此实际上它只是输入一个注释(在其LOB中分配一个条目)以稍后进行失效,但实际上在发送确认之前并不执行该失效。
  • CPU0收到对failure的确认,并通过SFENCE转到下一条指令
  • CPU0在不使用SOB的情况下将1写入关闭状态,因为它已专门拥有高速缓存行。由于高速缓存行是CPU0
  • 独有的,因此不会发送额外的无效消息
  • CPU1接收shutdown值并将其提交到其本地缓存,然后继续进行下一行。
  • CPU1检查if语句的failure值,但是由于尚未处理无效队列(LOB注释),因此它将使用其本地缓存中的值0(如果阻塞则不输入)。
  • 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。对于围栏之后的 cargo ,存储转发要注意保持一致的顺序。

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

    SPARC

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

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

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

    引用文献

    [1]内存壁垒:针对软件黑客的硬件 View ,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

    09-30 18:38