assembly 是否有任何比发布版本弱但仍提供语义同步的操作/防护?

myss37ts  于 2023-01-30  发布在  其他
关注(0)|答案(2)|浏览(153)

std::memory_order_releasestd::memory_order_acquire操作提供同步语义。
除此之外,std::memory_order_release保证所有加载和存储都不能在释放操作之后重新排序。
问题:
1.在C ++20/23中有没有什么东西提供了相同的同步语义,但没有std::memory_order_release那么强,以至于加载可以在发布操作之后重新排序?希望无序代码得到更优化(通过编译器或CPU)。
1.让我们假设在C ++20/23中没有这样的东西,那么对于Linux上的x86,是否没有标准的方法来做到这一点(例如,一些内联asm)?

tpgth1q7

tpgth1q71#

ISO C++只有三种适用于存储的排序:relaxedreleaseseq_cst。Relaxed显然太弱了,而seq_cst严格来说比release强。所以,不。
加载和存储都不能被重新排序超过一个发布存储的属性是提供你想要的synchronize-with语义所必需的,并且不能以任何我能想到的方式被削弱而不破坏它们。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;
}

此程序没有数据争用,并且必须输出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)同步,这应该是不可能的。

x8diyxa7

x8diyxa72#

ISO C++

在ISO C中,不,release是编写器端执行某些操作的最小值(可能是非原子的)存储然后存储data_ready标志。或者对于锁定/互斥,保持释放存储之前的加载和获取加载之后的存储(没有LoadStore重新排序)。或者任何其他 * 发生之前 * 给你。(C的模型是在保证加载能够或必须看到什么方面工作的,而不是从一致缓存中对加载和存储进行本地重新排序。我说的是它们是如何mapped into asm for normal ISAs的。)acq_rel RMW或seq_cst存储或RMW也可以工作,但是比X1 M4 N1 X强。

Asm,具有较弱的保证,可能足以满足某些情况

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

acq/rel同步的一些常见用例只需要在写入器端进行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,那么RISC-V仍然足够强大:其它加载停留在释放存储之前,而其它存储停留在获取加载之后。
CPU自然希望早加载、晚存储以隐藏延迟,因此在阻塞LoadLoad或StoreStore的基础上实际阻塞LoadStore重新排序通常不会造成太大负担。至少对于伊萨来说是这样,只要可以获得所需的排序而不必使用更强大的屏障。(即,当满足最低要求的唯一选项远远超过最低要求时,如32位ARMv7,您需要一个dsb ish全屏障,同时也阻止了StoreLoad。)

release在x86上是空闲的;其他《国际审计准则》更有意义。

memory_order_release在x86上基本上是免费的,只需要阻止编译时重新排序。(请参见 * C++ How is release-and-acquire achieved on x86 only using MOV? * -x86内存模型是程序顺序加上具有存储转发功能的存储缓冲区)。

x86是一个愚蠢的选择问;像PowerPC这样有多个不同的轻量级屏障选择的应用程序会更有趣。事实证明,它只需要一个屏障用于获取和释放,但seq_cst需要多个不同的屏障。
PowerPC asm的加载(获取)和存储(1,释放)如下所示-

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便宜,我认为lwsync也可以在那里工作;我认为拖延前端可能比在负载和存储上强加一些排序更糟糕。
我怀疑使用比较和分支而不是仅仅使用isyncdocumentation)的原因是,一旦加载被认为是无故障的,那么在数据实际到达之前,加载就可以从后端退出(“完成”)。
(x86不这样做,但弱序ISA这样做;这是在ARM等CPU上实现LoadStore重新排序的方式,具有按序或乱序执行。引退按程序顺序进行,但存储在引退 * 之后 * 才能提交到L1 d缓存。x86要求加载在引退之前生成一个值,这是保证LoadStore排序的一种方式。* How is load->store reordering possible with in-order commit? *)
在PowerPC上,条件寄存器0的比较(%cr0)对加载有数据依赖性,在数据到达之前无法执行。因此无法完成。我不知道为什么它上面还有一个always-false分支。我认为$+4分支目标是isync指令,以防万一。我想知道如果你只需要LoadLoad,而不是LoadStore,分支是否可以省略?不太可能。
IDK如果ARMv7可以只阻塞LoadLoad或StoreStore,那么这将是dsb ish的一大胜利,编译器使用dsb ish是因为它们也需要阻塞LoadStore。

加载比获取便宜:memory_order_consume

这是ISO C目前没有公开的有用的硬件特性(因为std::memory_order_consume的定义方式对于编译器来说太难了,无法在任何情况下正确实现,而不引入更多的障碍。因此,它被弃用,编译器处理它的方式与acquire相同)。
依赖排序(在除DEC Alpha之外的所有CPU上)使得加载指针和解除引用指针变得安全,而没有任何障碍或特殊的加载指令,并且如果编写器使用了释放存储,仍然可以看到指向的数据。
如果你想做一些比ISO C
acq/rel更便宜的事情,负载端是像POWER和ARMv7这样的ISA所能节省的。完全获取是免费的)。在armv8上我认为程度要小得多,因为ldapr应该很便宜。
更多信息请参见 * C++11:the difference between memory_order_relaxed and memory_order_consume *,其中包括Paul McKenney的演讲,他讲述了Linux如何使用普通加载(有效地为relaxed)来使RCU的读取端非常非常便宜,没有任何障碍,只要他们小心地编写代码,编译器就不会将数据依赖性优化为控制依赖性或什么都没有。
还涉及:

相关问题