getOpaque/order_relaxed/read_once 是否会在内存提升期间影响处理器,或者只影响编译器?

Does getOpaque/order_relaxed/read_once have influence on the processor, or just the compiler during memory hoisting?

提问人:Delark 提问时间:11/7/2023 最后编辑:Delark 更新时间:11/7/2023 访问量:60

问:

我与多人就此问题进行了一些讨论,并且有一些观点使得在负载情况下使用内存排序围栏有些令人困惑。

第一个要点似乎是:

  • a) 处理器重新排序的级别受 方法由编译器完成去虚拟化。

    • a.1) 处理器可以在虚拟方法中对指令重新排序, 但不是出于虚拟方法(除非去虚拟化)。

    • a.2) 处理器可以在其他虚拟方法调用之间重新排序虚拟方法调用 方法调用...只要这些方法不相互依赖 (无参考相互依赖性)。

  • b) 并行化等优化是在特定 在 ALU 上完成的数学运算,但从未在 程序员的“宏”逻辑代码...即使可以。

  • c) 处理器永远不会做一些古怪的事情,比如“内存” 提升“,这些事件始终是编译器方面的责任。 (见“D”)

  • d) 注册推广(prosessor的简陋“记忆提升” mechanic)只能出现在基元上,并且需要明确的指令。

    • d.1) 我的假设是,像 Java 的 Math 这样的库会 利用这种优化,但除此之外,编译器将 永远不要让处理器提升寄存器,除非没有其他寄存器是 参与并发数学运算。

在 Java 的编译器上...

  • e) Java 的编译器和 (JIT) 实现永远不会并行化复杂的相互依赖关系,即使它们可以。

    • e.1) 仅在不需要相互依赖的简单循环上。像 Arrays 库 Array/Collection 纯函数构造。
  • f) 内存提升等活动在此级别完成,而不是在处理器级别完成。

      • f的解释:试想一下,如果没有具体的指令来提升或阻止在处理器级别上除原语之外的内存上的寄存器升级,那么延迟预防只能在编译器级别通过提升来实现......虽然像仔细检查这样的操作确实涉及处理器,因为处理器有办法重新排序和简化代码......损坏过程中的双重检查。

现在让我们检查一下这个论点:

“好吧,现在可以工作了,但是......内存排序较弱的处理器呢?

在我看来,AND 加载是用于处理编译器 + 处理器重新排序行为的“杠杆”......volatile() 并获取 DO 对之前和后续的加载和存储有影响。volatilegetAcquireseq_const

但是/只是一个编译器句柄....在处理器级别上什么也没做......根据具体情况,要么是防吊装......或简化预防。getOpaquememory_ordered_relaxed

由于它所阻止的只是吊装,因此处理器甚至不知道使用了......(?),视情况而定。getOpaque

关于我的假设的最大线索是 lynux 内核定义围栏的名称......as ,作为告诉编译器“这不会被多次读取,请不要 HOIST-optimize”read_once()


因此,根据本网站上的一些答案,去虚拟化是一种“几乎从未发生过”的行为,即使它确实发生了,也只会在范围“上下文确认”的情况下发生......又名私有/受保护方法(扩展继承...)

这不包括: 公共方法(相对于外部范围)、lambda 和静态方法(不太确定这个 tbh)。

在下一段代码中,我将尝试演示如何利用方法虚拟化来演示特定语法如何防止负载提升。 注意:在仔细检查时,仍然需要getOpaque/relaxed。

如果。。。对于代码:

   int plain = 0;
   final Executor executor;

   Runnable readRunnable = () -> {
      T localPlain = this.plain; //One important factor of the why this will always work
                                 // properly is HOW the load is the first thing 
                                 // to happen in the stack.
                                 // Even if the load is done within the 'while' of a CAS.
                                 // The load is never hoisted, but...
                                 // "Ok, it works now, but... what about weakly memory ordered processors?"
      print(localPlain);
   };

   long delay = 20; //Millis
   MyExecutor exec = new MyExecutor(executor, delay, readRunnable); //This one's built in the constructor, please pardon my laziness...

   public void read() {
      exec.execute(); // will execute the inner final runnable.
   }
    
   public static void main() {
      Executor ex = Executors...
      PlainClass pc = new PlainClass(ex);
      print(pc.plain); // will read 0;
      pc.read(); // will read 3;
      pc.setPlain(3);
   }
    
   public void setPlain(int value) {
      this.plain = value;
   }

pc.plain 的源代码读取...即使来自同一个寄存器,编译器也无法提升负载(即使在循环中执行),这仅仅是因为负载隐藏在虚拟函数调用的取消引用中。

可运行的实例与执行器分离,因此在编译 MyExecutor 类时不会对其进行去虚拟化/内联。

然后通过 2 级间接读取执行,当主执行 2 次顺序打印时,负载不会被提升。


使用 getOpaque 来防止这种情况...在这种情况下,只会产生内省 + 强制转换的开销(我的意思是甚至 volatile 是首选而不是 tbh)。getOpaque

而且,由于内存提升是一种纯粹的编译器行为,因此“好吧,它现在可以工作了,但是......弱内存排序的处理器呢?“没有影响,因为即使处理器发生变化,编译器也会保持不变......

所以,我认为,真正的问题是,是什么让 // 在处理器眼中如此特别?memory_order_relaxedread_oncegetOpaque

或者,如果 getOpaque 的使用被广泛用于防止简化/提升(在 for 循环内联期间)或在循环加载中,那么该行为更有意义,可以作为跳转循环中的一对重新排序原子围栏来实现,而不是作为在特定寄存器上制定的“有针对性”加载/存储重新排序规则,...除非这是在引擎盖下实际发生的事情,放松......:

//pardon my pseudo code


   T local;

   // ^^^^...Keep reordering above this point...^^^

   atomic_fence(); //Nothing above can go bellow and viceversa.
   local = fieldVal;
   atomic_fence(); //Nothing bellow can go above and viceversa.

   // vvv...Keep reordering bellow this point...vvv

   return local;

所以我相信的是,提升行为与重新排序行为无关。 因此,memory_order_relaxed和同源指令正在做 2 项工作。

  • ONE)与编译器进行通信,防止编译器吊起+重新排序。
  • SECOND) 以防止在处理器重新排序期间重新排序(从而简化)。

我的代码可以执行双重检查吗?绝对不行。

它会作为给定寄存器的并发主动负载吗?绝对。。。但。。。内存排序较弱的处理器呢?

Java C++ 编译器优化 处理器 内存屏障

评论

3赞 Peter Cordes 11/7/2023
a) 处理器重新排序的级别受编译器完成的方法去虚拟化量的限制。-哼?分支预测 + 推测执行让无序执行人员可以查看过去的间接调用。此外,内存重新排序与无序执行是分开的,例如,存储缓冲区自行创建StoreLoad重新排序,而具有弱排序内存模型的ISA允许从存储缓冲区进行无序提交,如果uarch想要这样做。与执行顺序分开(将存储地址+数据写入存储缓冲区。
0赞 Delark 11/7/2023
推测仍然受虚拟函数调用的限制,为什么推测会使方法去虚拟化,而“前端”(非推测)代码序列仍然是虚拟的?推测不会重新排序,重新排序将在任何推测发生之前发生,推测加载可以跨核心缓存(用于验证)或主寄存器(用于验证)发生。同样,到这个时候,任何重新订购/提升都已经发生了......在猜测发生之前......
1赞 Peter Cordes 11/7/2023
Java 是与 C++ 不同的语言。在 C 和 C++ 中,对非变量进行不同步访问是完全未定义的行为,因此编译器可以制作可能撕裂的 asm,在极少数情况下速度更快。我认为 Java 可能禁止这样做,至少对于高达 32 位的值?我不认为 Java 通常可以承诺 64 位原子性,因为这在 32 位系统上会很昂贵(在一些需要锁定的 32 位系统上非常昂贵)。atomiclong
1赞 pveentjer 11/8/2023
@PeterCordes:是的,在 32 位 JVM 上,64 位非易失性长/双精度可能会遇到撕裂。但波动性多头/双倍不能。
1赞 pveentjer 11/16/2023
@Delark不透明需要提供原子性和连贯性。所有现代处理器都是连贯的,因此不需要内存围栏。但是编译器优化可能会导致不连贯,因此需要对其进行控制。

答: 暂无答案