atomic.Load 和 atomic.Store 有什么意义

What is the point of atomic.Load and atomic.Store

在 Go 的内存模型中,没有任何关于原子及其与内存栅栏的关系的说明。

尽管许多内部包似乎依赖于原子在它们周围创建内存栅栏时可以提供的内存排序。有关详细信息,请参阅 this issue

在不了解它的真正工作原理之后,我找到了来源,特别是 src/runtime/internal/atomic/atomic_amd64.go 并发现了 LoadStore 的以下实现:

//go:nosplit
//go:noinline
func Load(ptr *uint32) uint32 {
    return *ptr
}

Storeasm_amd64.s同一个包中实现。

TEXT runtime∕internal∕atomic·Store(SB), NOSPLIT, [=13=]-12
    MOVQ    ptr+0(FP), BX
    MOVL    val+8(FP), AX
    XCHGL   AX, 0(BX)
    RET

两者看起来都与并行无关。

我确实研究了其他架构,但实现似乎是等效的。

但是,如果原子确实很弱并且不提供内存排序保证,那么下面的代码可能会失败,但事实并非如此。

作为补充,我尝试用简单的赋值替换原子调用,但在这两种情况下它仍然产生一致的 "successful" 结果。


func try() {
    var a, b int32

    go func() {
        // atomic.StoreInt32(&a, 1)
        // atomic.StoreInt32(&b, 1)
        a = 1
        b = 1
    }()

    for {
        // if n := atomic.LoadInt32(&b); n == 1 {
        if n := b; n == 1 {
            if a != 1 {
                panic("fail")
            }
            break
        }
        runtime.Gosched()
    }
}

func main() {
    n := 1000000000
    for i := 0; i < n ; i++ {
        try()
    }
}

接下来的想法是编译器做了一些魔术来提供排序保证。因此,下面是未注释原子 StoreLoad 的变体列表。 pastebin.

上提供完整列表
// Anonymous function implementation with atomic calls inlined

TEXT %22%22.try.func1(SB) gofile../path/atomic.go
        atomic.StoreInt32(&a, 1)
  0x816         b801000000      MOVL [=15=]x1, AX
  0x81b         488b4c2408      MOVQ 0x8(SP), CX
  0x820         8701            XCHGL AX, 0(CX)
        atomic.StoreInt32(&b, 1)
  0x822         b801000000      MOVL [=15=]x1, AX
  0x827         488b4c2410      MOVQ 0x10(SP), CX
  0x82c         8701            XCHGL AX, 0(CX)
    }()
  0x82e         c3          RET
// Important "cycle" part of try() function

 0x6ca          e800000000      CALL 0x6cf      [1:5]R_CALL:runtime.newproc
    for {
  0x6cf         eb12            JMP 0x6e3
        runtime.Gosched()
  0x6d1         90          NOPL
    checkTimeouts()
  0x6d2         90          NOPL
    mcall(gosched_m)
  0x6d3         488d0500000000      LEAQ 0(IP), AX      [3:7]R_PCREL:runtime.gosched_m·f
  0x6da         48890424        MOVQ AX, 0(SP)
  0x6de         e800000000      CALL 0x6e3      [1:5]R_CALL:runtime.mcall
        if n := atomic.LoadInt32(&b); n == 1 {
  0x6e3         488b442420      MOVQ 0x20(SP), AX
  0x6e8         8b08            MOVL 0(AX), CX
  0x6ea         83f901          CMPL [=16=]x1, CX
  0x6ed         75e2            JNE 0x6d1
            if a != 1 {
  0x6ef         488b442428      MOVQ 0x28(SP), AX
  0x6f4         833801          CMPL [=16=]x1, 0(AX)
  0x6f7         750a            JNE 0x703
  0x6f9         488b6c2430      MOVQ 0x30(SP), BP
  0x6fe         4883c438        ADDQ [=16=]x38, SP
  0x702         c3          RET

如您所见,没有再次设置围栏或锁。

注意:所有测试均在 x86_64 和 i5-8259U

上完成

问题:

那么,在函数调用中包装简单的指针取消引用是否有任何意义,或者它是否有一些隐藏的含义,为什么这些原子仍然作为内存屏障工作? (如果他们这样做)

我完全不懂 Go,但看起来 .load().store() 的 x86-64 实现是 顺序一致。 大概是故意的/出于某种原因!

//go:noinline 加载意味着编译器无法围绕黑盒非内联函数重新排序,我假设。在 x86 上,这就是顺序一致性或 acq-rel 的负载端所需的全部。普通 x86 mov 加载是获取加载。

编译器生成的代码可以利用 x86 的强序内存模型,即顺序一致性 + 存储缓冲区(带存储转发),即 acq/rel. 要恢复顺序一致性,您只需要在释放存储后清空存储缓冲区。

.store() 用 asm 编写,加载其堆栈参数并使用 xchg 作为 seq-cst 存储。


XCHG with memory 有一个隐含的 lock 前缀,这是一个完整的障碍;它是 mov+mfence 的有效替代方案,可实现 C++ 所谓的 memory_order_seq_cst 存储。

它会在以后的加载和存储被允许接触 L1d 缓存之前刷新存储缓冲区。

  • https://bartoszmilewski.com/2008/11/05/who-ordered-memory-fences-on-an-x86/
  • C/C++11 映射到处理器 描述了在各种 ISA 上实现宽松的 load/store、acq/rel load/store、seq-cst load/store 和各种障碍的指令序列。所以你可以用记忆识别xchg之类的东西。
  • (TL:DR:是的,除了一些从 WC 内存加载 NT 的极端情况,例如从视频 RAM)。在某些代码中,您可能会看到一个虚拟 lock add [=21=], (SP) 作为 mfence 的替代。

    IIRC,AMD的优化手册竟然这么推荐。它在 Intel 上也很好,尤其是在 Skylake 上,其中 mfence 通过微代码更新 to fully block out-of-order exec 甚至 ALU 指令(如 lfence)以及内存重新排序得到了加强。 (修复 NT 负载的错误。)

  • https://preshing.com/20120913/acquire-and-release-semantics/