在 Go 的内存模型中,没有任何关于原子及其与内存防护的关系的说明。
尽管许多内部包似乎依赖于原子在它们周围创建内存栅栏时可以提供的内存排序。请参阅this issue了解详情。
在不明白它是如何工作的之后,我查阅了资料来源,特别是 src/runtime/internal/atomic/atomic_amd64.go并发现以下 Load
和 Store
的实现:
//go:nosplit
//go:noinline
func Load(ptr *uint32) uint32 {
return *ptr
}
Store
在同一包中的 asm_amd64.s
中实现。
TEXT runtime∕internal∕atomic·Store(SB), NOSPLIT, $0-12
MOVQ ptr+0(FP), BX
MOVL val+8(FP), AX
XCHGL AX, 0(BX)
RET
两者看起来好像都与并行性无关。
我确实研究过其他架构,但实现似乎是等效的。
但是,如果原子确实很弱并且不提供内存排序保证,那么下面的代码可能会失败,但事实并非如此。
作为补充,我尝试用简单的赋值替换原子调用,但在这两种情况下它仍然产生一致且“成功”的结果。
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()
}
}
接下来的想法是编译器做了一些魔法来提供排序保证。因此,下面是未注释原子 Store
和 Load
变体的列表。完整列表可在 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 $0x1, AX
0x81b 488b4c2408 MOVQ 0x8(SP), CX
0x820 8701 XCHGL AX, 0(CX)
atomic.StoreInt32(&b, 1)
0x822 b801000000 MOVL $0x1, 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 $0x1, CX
0x6ed 75e2 JNE 0x6d1
if a != 1 {
0x6ef 488b442428 MOVQ 0x28(SP), AX
0x6f4 833801 CMPL $0x1, 0(AX)
0x6f7 750a JNE 0x703
0x6f9 488b6c2430 MOVQ 0x30(SP), BP
0x6fe 4883c438 ADDQ $0x38, 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
有一个隐式的 lock
前缀,它是一个完整的屏障;它是 mov
+mfence
的有效替代方案,用于实现 C++ 所称的 memory_order_seq_cst
存储。
它会在以后的加载和存储访问 L1d 缓存之前刷新存储缓冲区。 <强> Why does a std::atomic store with sequential consistency use XCHG?
参见
- https://bartoszmilewski.com/2008/11/05/who-ordered-memory-fences-on-an-x86/
- C/C++11 mappings to processors 描述了在各种 ISA 上实现宽松加载/存储、acq/rel 加载/存储、seq-cst 加载/存储以及各种屏障的指令序列。所以你可以用内存来识别像 xchg 这样的东西。
Does lock xchg have the same behavior as mfence? (TL:DR:是的,除了一些从 WC 内存加载 NT 的极端情况,例如从视频 RAM )。您可能会在某些代码中看到一个虚拟的
lock add $0, (SP)
用作mfence
的替代品。AMD的优化手册IIRC甚至推荐了这一点。它在 Intel 上也很好,特别是在 Skylake 上,其中
mfence
通过微码更新得到了增强 to fully block out-of-order exec甚至 ALU 指令(如 lfence)以及内存重新排序。 (修复 NT 负载的勘误。)https://preshing.com/20120913/acquire-and-release-semantics/
关于multithreading - atomic.Load 和atomic.Store 的意义是什么,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58581820/