• AMD 64架构的CPU提供 atomic 128功能
  • 发布于 2个月前
  • 366 热度
    0 评论
  • 暮涵
  • 0 粉丝 21 篇博客
  •   
我们已经知道,标准库中的 atomic 针对 int32/uint32、int64/uint64 提供了原子操作的方法和函数,但是如果针对 128 bit 的整数呢?当然使用 128 bit 整数的原子操作的场景可能比较少,也不会有太多人有这个需求,但是如果我们需要对几个 32 bit、64 bit 变量进行原子操作吗, atomic128 可能就很有用。

tmthrgd/atomic128 在几年前提供了 atomic 128 的实验性功能,最后放弃了,但是他提供了一个思路,可以使用 CMPXCHG16B 指令为 AMD 64 架构的 CPU 提供 atomic 128 功能。

CAFxX/atomic128 fork 了上面的项目,继续维护,还是使用 CMPXCHG16B 指令,只为 AMD 64 架构提供原子操作。首先我们看看它的功能然后再看一看它的实现,最后我们思路发散一下,看看使用 AVX 为 128 bit 甚至更多 bit 的整数提供原子操作是否可行。

atomic128 的方法
Package atomic128  实现了对 128 bit 值的原子操作。在可能的情况下(例如,在支持 CMPXCHG16B 的 amd64 处理器上),它会自动使用 CPU 的原生特性来实现这些操作;否则,它会回退到基于互斥锁(mutexes)的方法。

Go 的基本整数中不包含 int128/uint128,所以这个库先定义了一个 Int128 的类型:
type Uint128 struct {
 d [3]uint64
 m sync.Mutex
}
然后类似标准库 atomic 中对各种整数的操作,它也提供了类似的方法:
func AddUint128(ptr *Uint128, incr [2]uint64) [2]uint64
func CompareAndSwapUint128(ptr *Uint128, old, new [2]uint64) bool
func LoadUint128(ptr *Uint128) [2]uint64
func StoreUint128(ptr *Uint128, new [2]uint64)
func SwapUint128(ptr *Uint128, new [2]uint64) [2]uint64

func OrUint128(ptr *Uint128, op [2]uint64) [2]uint64
func AndUint128(ptr *Uint128, op [2]uint64) [2]uint64
func XorUint128(ptr *Uint128, op [2]uint64) [2]uint64
可以看到,除了正常的 Add、CAS、Load、Store、Swap 函数,还贴心的提供了 Or、And、Xor 三个位操作的函数。
下面是一个简单的例子:
n := &atomic128.Uint128{}
v := atomic128.LoadUint128(n) // [2]uint64{0, 0}


atomic128.StoreUint128(n, [2]uint64{1, ^uint64(0)})
v = atomic128.LoadUint128(n) // [2]uint64{1, ^uint64(0)}

v = AddUint128(n, [2]uint64{2, 40})
v = atomic128.LoadUint128(n) // [2]uint64{3, 40}
v = atomic128.SwapUint128(n, [2]uint64{4, 50})
v = atomic128.LoadUint128(n) // [2]uint64{4, 50}
v = atomic128.CompareAndSwapUint128(n, [2]uint64{4, 50}, [2]uint64{5, 60})
v = atomic128.LoadUint128(n) // [2]uint64{5, 60}

v = atomic128.OrUint128(n, [2]uint64{0, 0})
v = atomic128.LoadUint128(n) // [2]uint64{5, 60}
atomic128 的实现
聪明的你也许看到Uint128的定义的时候就会感觉有一点不对劲,为啥 128bit 的整数要用 3 个 64bit 的整数来表示呢? 2 个 Uint64 不就够了吗?这是为了保证 128 位对齐,类似的技术在 Go 1.20 之前的 WaitGroup 中也有使用。进一步了解可以查看:
https://go101.org/article/memory-layout.html
https://pkg.go.dev/sync/atomic#pkg-note-BUG
通过包含三个 Uint64 元素的数组,我们总能通过下面的方法得到 128 位对齐的地址:
func addr(ptr *Uint128) *[2]uint64 {
 if (uintptr)((unsafe.Pointer)(&ptr.d[0]))%16 == 0 { // 指针已经128位对齐
  return (*[2]uint64)((unsafe.Pointer)(&ptr.d[0]))
 }
 return (*[2]uint64)((unsafe.Pointer)(&ptr.d[1])) // 必然ptr.d[1]是128位对齐的 (AMD64架构)
}
通过变量useNativeAmd64判断 CPU 是否支持CMPXCHG16B指令:
func init() {
 useNativeAmd64 = cpuid.CPU.Supports(cpuid.CX16)
}
如果不支持,回退到使用 Mutex 实现一个低效的 atomic 128bit 原子操作:
func CompareAndSwapUint128(ptr *Uint128, old, new [2]uint64) bool {
 if runtime.GOARCH == "amd64" && useNativeAmd64 {
  return compareAndSwapUint128amd64(addr(ptr), old, new)
 }
 // 不支持CMPXCHG16B指令,使用Mutex
 ptr.m.Lock()
 v := load(ptr)
 if v != old {
  ptr.m.Unlock()
  return false
 }
 store(ptr, new)
 ptr.m.Unlock()
 return true
}
如果支持CMPXCHG16B指令,直接调用compareAndSwapUint128amd64函数:
TEXT ·compareAndSwapUint128amd64(SB),NOSPLIT,$0
 MOVQ addr+0(FP), BP
 MOVQ old+8(FP), AX
 MOVQ old+16(FP), DX
 MOVQ new+24(FP), BX
 MOVQ new+32(FP), CX
 LOCK
 CMPXCHG16B (BP)
 SETEQ swapped+40(FP)
 RET
主要依赖CMPXCHG16B实现。

CMPXCHG16B是一条 X86 体系结构中的指令,全称为"Compare and Exchange 16 Bytes"。它用于原子地比较和交换 16 个字节(128 位)的内存区域。 这条指令的作用是:
将要比较的 16 个字节的内存值加载到一个寄存器中。
将要写入的 16 个字节的值加载到另一个寄存器中。
比较内存中的值和第一个寄存器中的值是否相等。
如果相等,则用第二个寄存器中的值覆盖内存中的值。

根据比较结果,设置相应的标志位。


思路发散
当前很多号称性能优化的库,可能会使用 SIMD 指令集来提高性能,比如 AVX、SSE 等。那么,我们是否可以使用 AVX 指令集来实现对 128 位整数甚至 256、512 位整数的原子操作呢?

各家处理器手册中并没有为 AVX 指令集提供原子性的担保。The AMD64 Architecture Programmer’s Manual只是保证了内存操作最大 8 个字节,CMPXCHG16B是原子的。The Intel® 64 and IA-32 Architectures Software Developer’s Manual也做了类似的保证。此外,Intel 手册明确指出 AVX 指令没有任何原子性保证。

尽管看起来对齐的 128 位操作室原子的,但是 CPU 提供商没有提供担保,我们还是使用 CMPXCHG16B 指令保险。
用户评论