在Go的内存模型中,没有任何关于原子及其与内存围栏的关系的说明。
尽管许多内部包似乎依赖于内存排序,但如果atomics在其周围创建内存围栏,则可以提供内存排序。详情请参见本期。
在不了解它的真正工作原理之后,我访问了源代码,特别是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两人似乎都与并行无关。
我确实研究过其他架构,但实现似乎是等价的。
但是,如果atomics确实很弱并且没有提供内存排序的保证,那么下面的代码可能会失败,但它不会。
另外,我尝试用简单的赋值替换原子调用,但在这两种情况下,它仍然产生一致和“成功”的结果。
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未注释的变量的清单。完整的清单可以在巴斯丁上找到。
// 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-8259 u上进行的。
问题:
那么,在函数调用中包装简单指针取消引用有什么意义吗?或者它是否有隐藏的意义?为什么这些atomics仍然作为内存屏障工作?(如果是的话)
发布于 2019-10-27 18:34:00
我一点也不知道Go,但是看起来.load() 和 .store() 的x86-64实现可能是故意的/有原因的!
加载的//go:noinline意味着编译器不能围绕一个非内联的黑匣子重新排序,我想。在x86上,这就是顺序一致性( acq-rel )加载端所需的全部内容。普通的x86 mov负载是获取负载。
编译器生成的代码可以利用x86的强有序内存模型,该模型是顺序一致性+存储缓冲区(具有存储转发),即acq/rel.为了恢复顺序一致性,只需要在发布存储后耗尽存储缓冲区。
.store()是用asm编写的,加载它的堆栈args,并使用xchg作为seq存储。
带内存的XCHG有一个隐式lock前缀,这是一个完全障碍;它是mov+mfence的一个有效替代方案,可以实现C++称为memory_order_seq_cst存储的内容。
它在以后加载之前刷新存储缓冲区,并且允许存储区访问L1d缓存。为什么具有顺序一致性的std::原子存储使用XCHG?
看见
lock add $0, (SP)作为mfence的替代方案。
IIRC,AMD的优化手册甚至建议这样做。它对英特尔也有好处,尤其是在Skylake,在那里,mfence被微码更新完全阻止无序的主管增强,甚至包括ALU指令(如lfence)以及内存重新排序。(用NT负载修正错误。)https://stackoverflow.com/questions/58581820
复制相似问题