代码之家  ›  专栏  ›  技术社区  ›  xmh0511

“std::memory_ordering”的值是否会影响编译器重新排序和原子对象上的硬件指令?

  •  0
  • xmh0511  · 技术社区  · 1 年前

    我想知道,类型的参数的值是多少 std::memory_ordering 只需提示编译器如何重新排序代码,或者该值是否也会影响操作原子对象的指令的选择?

    如中所述 https://en.cppreference.com/w/cpp/atomic/memory_order 例如

    memory_order_acquire :在加载之前,不能对当前线程中的任何读取或写入进行重新排序。

    这就要求编译器如何重新排序代码。假设目标平台有两个原子指令: W0 addr, eax , W1 addr, eax .IIUC,的值 memory_order 也会影响对原子对象使用哪条指令的选择,对吧?


    另一个问题是,如果类型的参数的值 内存顺序 只能在运行时确定,编译器如何知道如何根据值对代码进行重新排序?

    0 回复  |  直到 1 年前
        1
  •  2
  •   Nate Eldredge    1 年前

    是的,两者都有。

    C++内存模型要求原子操作遵循某些语义,这些语义取决于指定的内存排序参数。因此,编译器必须发出代码,这些代码在执行时会根据这些语义进行操作。

    例如,采用如下代码:

    std::atomic<int> x;
    int y, tmp;
    if (x.load(std::memory_order_acquire) == 5) {
        tmp = y;
    }
    

    在典型的机器上,编译器需要:

    1. 不重新排序的负载 x y 在编译时。换句话说,它应该发出的加载指令 x 和的加载指令 y ,使得第一个按程序顺序在第二个之前执行。

    2. 确保的负载 x y 成为 看得见的 按照这个顺序。如果机器能够执行无序、推测性加载或任何其他可能导致两个加载按程序顺序可见的功能,则编译器必须发出代码以防止这种情况发生。

      该代码的外观取决于所讨论的机器。可能性包括:

      • 不需要什么特别的东西,因为机器不会进行这种特殊的重新排序。所以 x y 将仅由普通加载指令加载,不需要任何额外的内容。例如,x86上就是这样,“获取所有负载”。

      • 使用一种特殊形式的加载指令来禁止重新排序。例如,在AArch64上,的加载 x 会用 ldapr ldar 指令而非普通 ldr .

      • 在两个负载之间插入一条特殊的内存屏障指令,如ARM dmb .


    在绝大多数代码中,内存排序参数被指定为编译时间常数,因为程序员静态地知道需要什么排序,因此编译器可以发出适合该特定排序的指令。

    在排序参数不是常量的特殊情况下,无论指定了什么值,编译器都必须发出行为正常的代码。通常情况下,编译器只将排序参数视为 memory_order_seq_cst ,因为这比其他所有东西都强:a seq_cst 操作满足较弱排序所需的所有语义(除此之外还有更多)。这节省了在运行时实际测试排序参数的值并相应地进行分支的成本,这可能超过了使用较弱排序进行操作的潜在节约。

    但是,如果编译器确实选择了测试和分支,为了优化周围的代码,它通常必须假设“最坏情况”。例如,在AArch64上,对于 x.load(order) 它可能会发出如下代码块:

    int t;
    if (order == std::memory_order_relaxed)
        LDR t, [x]
    else if (order == std::memory_order_acquire)
        LDAPR t, [x]
    else if (order == std::memory_order_seq_cst)
        LDAR t, [x]
    else
        abort();
    if (t == 5)
        LDR tmp, [y]
    

    然而,它需要确保 y 保留在这段代码的末尾(按程序顺序)。如果 order 等于 std::memory_order_relaxed ,则可以执行的加载 y 加载之前 x ,但如果是 std::memory_order_acquire 或更强。

    另一方面,它可以想象地发射

    int t, t2;
    if (order == std::memory_order_relaxed) {
        LDR t2, [y]
        LDR t, [x]
    } else if (order == std::memory_order_acquire) {
        LDAPR t, [x]
        LDR t2, [y]
    } else if (order == std::memory_order_seq_cst) {
        LDAR t, [x]
        LDR t2, [y]
    else
        abort();
    if (t == 5)
        tmp = t2;
    

    但我们现在已经远远超出了实际编译器实际执行的转换范围。