multithreading - atomic.Load 和 atomic.Store 的意义何在
问题描述
在 Go 的内存模型中,没有任何关于原子及其与内存围栏的关系的说明。
尽管许多内部包似乎依赖于可以提供的内存排序,如果原子在它们周围创建内存栅栏。有关详细信息,请参阅此问题。
在不了解它的真正工作原理之后,我去了源代码,特别是src/runtime/internal/atomic/atomic_amd64.go并找到了以下Load
and的实现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,但它看起来像 x86-64 实现.load()
并且.store()
是顺序一致的。 大概是故意/出于某种原因!
//go:noinline
我认为,加载意味着编译器无法围绕黑盒非内联函数重新排序。在 x86 上,这就是顺序一致性或 acq-rel 的负载方面所需的全部内容。普通的 x86mov
负载是获取负载。
编译器生成的代码可以利用x86 的强排序内存模型,即顺序一致性 + 存储缓冲区(带有存储转发),即 acq/rel。 要恢复顺序一致性,您只需要在释放存储之后清空存储缓冲区。
.store()
用 asm 编写,加载其堆栈参数并xchg
用作 seq-cst 存储。
XCHG
with memory 有一个隐式lock
前缀,它是一个完整的屏障;mov
它是+的有效替代方案,mfence
可以实现 C++ 所称的memory_order_seq_cst
商店。
它在以后的加载和存储允许触及 L1d 缓存之前刷新存储缓冲区。 为什么具有顺序一致性的 std::atomic 存储使用 XCHG?
看
- https://bartoszmilewski.com/2008/11/05/who-ordered-memory-fences-on-an-x86/
- C/C++11 到处理器的映射 描述了在各种 ISA 上实现轻松加载/存储、acq/rel 加载/存储、seq-cst 加载/存储和各种障碍的指令序列。所以你可以用内存识别 xchg 之类的东西。
lock xchg 是否具有与 mfence 相同的行为?(TL:DR:是的,除了一些从 WC 内存加载 NT 的极端情况,例如从视频 RAM 加载)。在某些代码中,您可能会看到一个 dummy
lock add $0, (SP)
用作替代。mfence
IIRC,AMD 的优化手册甚至建议这样做。这对 Intel 也有好处,尤其是在 Skylake 上,
mfence
它通过微码更新加强了甚至完全阻止了 ALU 指令(如 lfence)以及内存重新排序的乱序执行。(修复 NT 负载的错误。)https://preshing.com/20120913/acquire-and-release-semantics/
推荐阅读
- python - 值计数
- python - 在 python3 上发布请求
- multithreading - AXUIElement.h 中的函数可以从主线程以外的线程安全调用吗?
- python - setup.py 安装与 pip 安装
- r - R 闪亮的传单气泡图由 Count
- r - devtool::install_github 是否有一个 lib 参数来控制保存包的位置
- javascript - 如何检查值是否在数据数组中?吴若
- javascript - 正则表达式只接受字符串第一个位置的字母
- python - 临时变量的 Python 类型提示
- sql - SQL listagg 和 concat,删除重复项