沉默的性能杀手 - false sharing

用户头像
哈希说
关注
发布于: 2020 年 12 月 06 日
沉默的性能杀手 - false sharing

一般在做性能优化时,我们往往着眼于代码层面,很少关注硬件层面。这篇文章的主题是 false sharing (伪共享),在介绍 false sharing 前,我们首先需要了解下什么是 CPU Cache



CPU Cache





计算机存储器是分层次的,离 CPU 越近的存储器速度越快,每字节的成本越高,同时容量也越小。这里我们要讲的 CPU Cache 指的是 L1 CacheL2 CacheL3 Cache,也是我们常说的高速缓存。CPU Cache 最小缓存单元为 Cache Line,x86_64 CPU 系列为 64Bytes。后面我们要讲到的 false sharing 主要是和 Cache Line 有关





CPU Cache 在处理数据时,是以 Cache Line 为单位进行操作的,那么就意味着,如果处理 1Bytes 数据,其实是会一并把它临近的 63Bytes 数据一并处理。这里是以 x86_64 系列举例



false sharing



false sharing (伪共享) 指的是多个线程操作互相独立的变量时,如果这些变量共享同一 Cache Line,那么就会在无意中影响彼此的性能





那么如何避免 false sharing,答案就是 Cache Line 对齐,也很简单,就是通过空数组把这些独立的变量隔开



type NoPad struct {
a uint64
b uint64
c uint64
}
type Pad struct {
a uint64
_a [7]uint64
b uint64
_b [7]uint64
c uint64
_c [7]uint64
}



由于不同 CPU 架构体系的 Cache Line 大小不同,go 的标准库 golang.org/x/sys/cpu 提供 CacheLinePad 用于做 Cache Line 对齐



type Pad struct {
a uint64
_a cpu.CacheLinePad
b uint64
_b cpu.CacheLinePad
c uint64
_c cpu.CacheLinePad
}



那么有没有做 Cache Line 对性能影响会有多大呢?我们直接来做个 benchmark 测试



type NoPad struct {
a uint64
b uint64
c uint64
}
func (np *NoPad) Increase() {
atomic.AddUint64(&np.a, 1)
atomic.AddUint64(&np.b, 1)
atomic.AddUint64(&np.c, 1)
}
type Pad struct {
a uint64
_a [7]uint64
b uint64
_b [7]uint64
c uint64
_c [7]uint64
}
func (p *Pad) Increase() {
atomic.AddUint64(&p.a, 1)
atomic.AddUint64(&p.b, 1)
atomic.AddUint64(&p.c, 1)
}
func BenchmarkPadIncrement(b *testing.B) {
pad := &Pad{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
pad.Increase()
}
})
}
func BenchmarkNoPadIncrement(b *testing.B) {
noPad := &NoPad{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
noPad.Increase()
}
})
}



benchmark 测试结果如下



$ go test -bench .
BenchmarkPadIncrement-12 42052069 26.6 ns/op
BenchmarkNoPadIncrement-12 20085301 58.9 ns/op



可以看到有做 Cache Line 对齐和没做对齐,两者的性能差异是巨大的,这也是为什么称 false sharing 为沉默的性能杀手





发布于: 2020 年 12 月 06 日阅读数: 18
用户头像

哈希说

关注

还未添加个人签名 2018.03.08 加入

还未添加个人简介

评论

发布
暂无评论
沉默的性能杀手 - false sharing