gpt4 book ai didi

Java 8 不安全 : xxxFence() instructions

转载 作者:行者123 更新时间:2023-12-02 03:23:17 28 4
gpt4 key购买 nike

在 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();
如果我们用以下方式定义内存屏障(我认为或多或少容易理解):

Consider X and Y to be operation types/classes that are subject for reordering,

X_YFence() is a memory barrier instruction that ensures that all operations of type X before the barrier completed before any operation of type Y after the barrier is started.


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

  • 最后, 我的问题是 - 为什么我们没有 load_storeFence()store_loadFence() 、 0x2513431 和 132513432 ?
    我的猜测是 - 它们并不是真正必要的,但我目前不明白为什么。所以,我想知道不添加它们的原因。关于这一点的猜测也是受欢迎的(希望这不会导致这个问题因为基于意见而脱离主题)。
    提前致谢。

    最佳答案

    概括

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

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

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

  • 清空 LOB (loadFence):意味着不会在该内核上开始执行其他指令,直到处理完 LOB 的所有条目。在 x86 中,这是一个 LFENCE。
  • 清空 SOBs (storeFence):意味着在处理完 SOBs 中的所有条目之前,不会在该内核上开始执行其他指令。在 x86 中,这是一个 SFENCE。
  • 清空 LOBs 和 SOBs(fullFence):表示以上两者。在 x86 中,这是一个 MFENCE。

  • 实际上,每个特定的处理器架构都提供了不同的内存排序保证,这可能比上述更严格,或者更灵活。例如,SPARC 体系结构可以重新排序加载-存储和存储-加载序列,而 x86 不会这样做。此外,存在无法单独控制 LOB 和 SOB 的架构(即,只能使用全栅栏)。然而,在这两种情况下:
  • 当架构更灵活时,API 根本不提供对“宽松”排序组合的访问,作为选择
  • 当架构更严格时,API 简单地在所有情况下实现更严格的排序保证(例如,所有 3 个调用实际上都被实现为一个完整的栅栏)

  • 根据 assylias 提供的 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 将退出循环并且不会进入故障处理 if 块。

    这是因为 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 中最后写入的值。这意味着无论如何,从这个核心中存储的存储永远不能与来自这个核心的后续加载一起重新排序。

    无序加载和 LFENCE

    现在,假设您已设置好存储围栏,并且很高兴 shutdown 在通往 CPU 1 的途中无法超过 failure,并专注于另一侧。即使在商店围栏的存在下,也有发生错误的情况。考虑 failure 在两个缓存(共享)中的情况,而 shutdown 仅存在于 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(不进入 if 块)。
  • CPU1处理invalidate queue并将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 被处理,实质上是清空失效队列。这意味着所有后续加载都将看到最新数据(无重新排序),因为它们将从缓存子系统中获取(这是一致的)。存储不能在后续加载中重新排序,因为它们不通过 LOB。 (此外存储转发处理本地修改的缓存行)从这个特定内核(执行加载栅栏的内核)的角度来看,加载栅栏后面的存储将在所有寄存器加载数据后执行。没有其他办法了。
    load_storeFence() becomes ???

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

    所有其他情况都类似。

    空间研究中心

    SPARC 更加灵活,可以对带有后续加载的存储(以及带有后续存储的加载)重新排序。我对 SPARC 不太熟悉,所以我的猜测是没有存储转发(重新加载地址时不咨询 SOB)所以“脏读”是可能的。事实上我错了:我在 [3] 中发现了 SPARC 架构,而实际情况是存储转发是线程化的。来自第 5.3.4 节:

    所有负载都检查存储缓冲区(仅同一线程)是否存在写后读 (RAW) 危险。当加载的双字地址与 STB 中存储的地址匹配并且加载的所有字节在存储缓冲区中都有效时,就会发生完整的 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

    关于Java 8 不安全 : xxxFence() instructions,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23603304/

    28 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com