繁体   English   中英

任何可用的操作/围栏比发布更弱但仍提供同步语义?

[英]Any operation/fence available weaker than release but still offering synchronize-with semantic?

std::memory_order_releasestd::memory_order_acquire操作提供同步语义。

除此之外, std::memory_order_release保证所有加载和存储不能在释放操作之后重新排序。

问题:

  1. C++20/23 中是否有任何东西提供相同的同步语义但不如std::memory_order_release强,以便可以在释放操作之后重新排序加载? 希望乱序代码得到更多优化(通过编译器或 CPU)。
  2. 假设在 C++20/23 中没有这样的东西,对于 linux 上的 x86 是否有任何标准的方法(例如一些内联汇编)?

ISO C++ 只有三个适用于商店的顺序: relaxedreleaseseq_cst Relaxed 显然太弱了,而seq_cst严格来说比release强。 所以不行。

加载和存储都不能通过发布存储重新排序的属性是提供您想要的同步语义所必需的,并且不能以我能想到的任何方式削弱而不破坏它们。 synchronize-with 的要点是发布存储可以用作关键部分的结尾。 该关键部分内的操作,包括加载和存储,都必须留在那里。

考虑以下代码:

std::atomic<bool> go{false};
int crit = 17;

void thr1() {
    int tmp = crit;
    go.store(true, std::memory_order_release);
    std::cout << tmp << std::endl;
}

void thr2() {
    while (!go.load(std::memory_order_acquire)) {
        // delay
    }
    crit = 42;
}

该程序没有数据竞争,必须 output 17 这是因为 thr1 中的发布存储与 thr2 中的最终获取负载同步,后者返回true (因此从存储中获取其值)。 这意味着 thr1 中的crit负载发生在thr2中的存储之前,因此它们不会竞争,并且负载不会观察到存储。

如果我们将 thr1 中的发布存储替换为您假设的半发布存储,这样crit的负载可以在go.store(true, half_release)之后重新排序,那么该负载可能会在任何时间后发生。 它特别可能与 thr2 中的crit存储同时发生,甚至之后发生。 所以它可以读取42或垃圾,或任何其他可能发生的事情。 如果go.store(true, half_release)确实与go.load(acquire)同步,这应该是不可能的。

国际标准化组织 C++

在 ISO C++ 中,不, release是编写器端进行一些(可能是非原子的)存储然后存储data_ready标志的最小值。 或者为了锁定/互斥,在发布存储之前保持加载,在获取加载之后保持存储(没有 LoadStore 重新排序)。 或者其他任何事情发生之前给你。 (C++ 的 model 在保证加载可以或必须看到的内容方面起作用,而不是在从连贯缓存中对加载和存储进行本地重新排序方面起作用。我说的是它们如何映射到普通 ISA 的 asm 中。) acq_rel RMWs 或seq_cst商店或 RMWs 也可以工作,但比release更强。


具有较弱保证的 Asm 在某些情况下可能就足够了

在某些平台的 asm 中,也许你可以做一些更弱的事情,但它不会完全发生在之前。 我不认为对 release 有任何要求是多余的,而不是 happens-before 和正常的 acq/rel 同步。 ( https://preshing.com/20120913/acquire-and-release-semantics/ )。

acq/rel sync 的一些常见用例只需要写入端的 StoreStore 排序,读取端的 LoadLoad (例如,具有单向通信、非原子存储和data_ready标志的生产者/消费者。)如果没有 LoadStore 排序要求,我可以想象在某些平台上写入器或读取器会更便宜。

也许是 PowerPC 或 RISC-V? 我检查了编译器在 Godbolt 上a.load(acquire)a.store(1, release)做了什么。

# clang(trunk) for RISC-V -O3
load(std::atomic<int>&):     # acquire
        lw      a0, 0(a0)    # apparently RISC-V just has barriers, not acquire *operations*
        fence   r, rw        # but the barriers do let you block only what is necessary
        ret
store(std::atomic<int>&):    # release
        fence   rw, w
        li      a1, 1
        sw      a1, 0(a0)
        ret

如果fence r和/或fence w存在并且比fence r,rwfence rw, w便宜,那么是的,RISC-V 可以做一些比 acq/rel 稍微便宜的事情。 除非我遗漏了一些东西,否则如果您只想在获取负载后加载,请查看发布存储之前的存储,但不关心 LoadStore:其他负载停留在发布存储之前,而其他存储停留获取负载后。

CPU 自然希望提前加载和延迟存储以隐藏延迟,因此在阻塞 LoadLoad 或 StoreStore 之上实际阻塞 LoadStore 重新排序通常不是什么负担。 至少对于 ISA 来说是这样,只要它可以在不必使用更强大的屏障的情况下获得所需的顺序。 (即当满足最低要求的唯一选项远远超出它时,例如 32 位 ARMv7,您需要一个dmb ish完整屏障,它也阻止了 StoreLoad。)


release免费发布; 其他 ISA 更有趣。

memory_order_release在 x86 上基本免费,只需要阻止编译时重新排序。 (参见C++ How is release-and-acquire on x86 only using MOV? - The x86 memory model is program order plus a store-buffer with store forwarding)。

x86 是一个愚蠢的选择; 像 PowerPC 这样有多种不同的轻量级屏障选择的东西会更有趣。 事实证明它只需要一个屏障来获取和释放,但 seq_cst 在前后需要多个不同的屏障。

对于加载(获取)和存储(1,发布),PowerPC asm 看起来像这样 -

load(std::atomic<int>&):
        lwz %r3,0(%r3)
        cmpw %cr0,%r3,%r3     #; I think for a data dependency on the load
        bne- %cr0,$+4         #; never-taken, if I'm reading this right?
        isync                 #; instruction sync, blocking the front-end until older instructions retire?
        blr
store(std::atomic<int>&):
        li %r9,1
        lwsync               # light-weight sync = LoadLoad + StoreStore + LoadStore.  (But not blocking StoreLoad)
        stw %r9,0(%r3)
        blr

我不知道isync是否总是比lwsync便宜,我认为它也可以在那里工作; 我原以为拖延前端可能比对加载和存储强加一些排序更糟糕。

我怀疑比较和分支而不仅仅是isync文档)的原因是一旦负载被认为是无故障的,在数据实际到达之前,负载可以从后端退出(“完成”)。

(x86 不会这样做,但弱序 ISA 会这样做;这就是您如何在 ARM 等 CPU 上使用有序或乱序执行程序对 LoadStore 进行重新排序。退役按程序顺序进行,但存储无法提交到 L1d 缓存直到它们退出。x86 要求加载在退出之前产生一个值是保证 LoadStore 排序的一种方法。加载- > 存储重新排序如何通过有序提交实现?

所以在 PowerPC 上,条件寄存器 0 ( %cr0 ) 的比较对负载有数据依赖性,在数据到达之前无法执行。 从而无法完成。 我不知道为什么上面还有一个 always-false 分支。 我认为$+4分支目的地是isync指令,以防万一。 如果只需要LoadLoad,不需要LoadStore,是否可以省略分支? 不太可能。


IDK 如果 ARMv7 可能只阻止 LoadLoad 或 StoreStore。 如果是这样,那将是对dmb ish的巨大胜利,编译器使用它是因为它们还需要阻止 LoadStore。


加载比获取便宜: memory_order_consume

这是 ISO C++ 当前未公开的有用硬件功能(因为std::memory_order_consume的定义方式对于编译器来说太难了,无法在不引入更多障碍的情况下正确实现每个极端情况。因此它已被弃用,编译器处理它与acquire相同)。

依赖排序(在除 DEC Alpha 之外的所有 CPU 上)使得加载指针和取消引用它变得安全,没有任何障碍或特殊加载指令,并且如果编写者使用发布存储,仍然可以看到指向的数据。

如果您想做一些比 ISO C++ acq / rel更便宜的事情,那么负载端就是在 POWER 和 ARMv7 等 ISA 上节省的地方。 (不是 x86;完全获取是免费的)。 我认为在 ARMv8 上的程度要小得多,因为ldapr应该很便宜。

有关更多信息,请参见C++11:memory_order_relaxed 和 memory_order_consume 之间的区别,包括 Paul McKenney 关于 Linux 如何使用普通加载(有效地relaxed )使 RCU 的读取端非常便宜,没有障碍,只要他们小心不要编写编译器可以将数据依赖性优化为仅控制依赖性或什么都没有的代码。

还相关:

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM