atomic LoadInt64 / LoadInt32

在阅读 bytebufferpool 和 workerpool 代码的时候经常看到atomic.LoadUInt64 , atomic.LoadInt32 , atomic.StoreInt32 等函数调用,产生了一个疑问 : 为什么整数操作要用到原子包里的东西

一番搜寻后,得到一个答案是64位的操作在32位上并不是由一个指令完成的,而是分成前32位和后32位两部分操作完成,因此整个操作没有原子性保证

那要如何在32位机器上实现64位的原子操作呢? 先分析一下可能出现的情况

1. 执行完第一部分后,上下文切换到其他线程,其他线程读取该值,导致问题
2. 多cpu,多核,有cpu cache的情况下,单个指令有硬件的缓存一致性协议保证,多个指令的话无法保证 , 也导致问题
3. 编译器对代码进行优化,打乱顺序,导致问题

问题1是可见性的问题,读取同样也要加内存屏障, 这样即使发生了线程切换, 另一个线程也不会看到赋值到一半的int64

问题2是原子性问题, 使用硬件提供的内存屏障指令,被内存屏障指令包围的操作只有在全部完成后才写入内存

问题3是有序性问题, 也由内存屏障提供, 阻止编译器,cpu的执行优化

如何查看 go 汇编代码

  • 样例代码
package main

const x int64 = 1 + 1<<33
func main() {
    var i=x
    _ = i
}
  • gdb
go build -gcflags "-N -l" -o gdb_sandbox main.go
gdb gdb_sandbox

进入 gdb 后
layout split
b main
run
  • go tool compile / objdump
// objdump -gnu 好像没这个选项
//GOARCH=386 go tool compile -N -l main.go
//GOARCH=386 go tool objdump  main.o

//GOARCH=amd64 go tool compile -N -l main.go
//GOARCH=amd64 go tool objdump  main.o
  • 64位和 32 位 int64 汇编代码对比 (macos-darwin)
 32 位:

 main.go:5             0x2ec                   c7042401000000          MOVL $0x1, 0(SP)        
 main.go:5             0x2f3                   c744240402000000        MOVL $0x2, 0x4(SP)

 64 位:
 main.go:5             0x2d4                   48b80100000002000000    MOVQ $0x200000001, AX

  • LoadInt64 源码

runtime/internal/atomic/atomic_amd64.go

func Load64(ptr *uint64) uint64 {
    return *ptr
}

64位下直接取值

runtime/internal/atomic/asm_386.s

// uint64 atomicload64(uint64 volatile* addr);
TEXT runtime∕internal∕atomic·Load64(SB), NOSPLIT, $0-12
    MOVL    ptr+0(FP), AX
    TESTL   $7, AX
    JZ  2(PC)
    MOVL    0, AX // crash with nil ptr deref
    MOVQ    (AX), M0
    MOVQ    M0, ret+4(FP)
    EMMS
    RET

猜测: volatile 告知编译器不做优化 , NOSPLIT 和 EMMS 应该是内存屏障?    

为什么有32位的原子操作

猜测是给 16 位架构使用的, 开始尝试验证

GOARCH可以指定机器架构交叉编译,先看看go提供了哪些平台编译支持 , 并没有看到16位的参数

darwin/386
darwin/amd64
darwin/arm
darwin/arm64
  • 测试代码
package main


const x int32 = 2 + 1 << 30
func main() {
    var i  = x
    _ = i
}
  • 64 位和 32 位 LoadInt32 源码 对比
runtime/internal/atomic/atomic_386.go
func Load(ptr *uint32) uint32 {
    return *ptr
}

runtime/internal/atomic/atomic_amd64.go
func Load(ptr *uint32) uint32 {
    return *ptr
}

和直接取值的编译结果没有区别

  • 再看看arm64 的汇编

arm64 直接取值的编译结果
main.go:6             0x2f5                   d2800040                MOVD $2, R0             
main.go:6             0x2f9                   f2a80000                MOVK $(16384<<16), R0

Loadint32 源码:
// uint32 runtime∕internal∕atomic·Load(uint32 volatile* addr)
TEXT ·Load(SB),NOSPLIT,$0-12
    MOVD    ptr+0(FP), R0
    LDARW   (R0), R0
    MOVW    R0, ret+8(FP)
    RET

结论: arm64 平台的 32 位操作是不是原子操作,先操作前 16 位,再操作后 16 位

资料

后续

  • 既然 32 位执行 64 位操作不是原子的, 为什么不在编译的时候自动加内存屏障呢 , 而是要自己调用 atomic , 猜测是因为内存屏障开销大?

  • 所以这些流行的库为了提供给更多不同平台的机器使用,使用了 int 原子操作 ?

  • atomic 包 ,主要提供了三种接口, RMW (read-modify-write ), 加载 , 存储 , 记住传的是指针