[译] 什么是缓存 false sharing 以及如何解决(Golang 示例)

栏目: Go · 发布时间: 6年前

内容简介:在解释缓存 false sharing 之前,有必要简要介绍一下缓存在 CPU 架构中的工作原理。CPU 中缓存的最小化单位是缓存行(现在来说,CPU 中常见的缓存行大小为 64 字节)。因此,当 CPU 从内存中读取变量时,它将读取该变量附近的所有变量。图 1 是一个简单的例子:当 core1 从内存中读取变量 a 时,它会同时将变量 b 读入缓存。(顺便说一下,我认为 CPU 从内存中批量读取变量的主要原因是基于空间局部性理论:当 CPU 访问一个变量时,它可能很快就会读取它旁边的变量。)(译者注:关于

在解释缓存 false sharing 之前,有必要简要介绍一下缓存在 CPU 架构中的工作原理。

CPU 中缓存的最小化单位是缓存行(现在来说,CPU 中常见的缓存行大小为 64 字节)。因此,当 CPU 从内存中读取变量时,它将读取该变量附近的所有变量。图 1 是一个简单的例子:

[译] 什么是缓存 false sharing 以及如何解决(Golang 示例)

当 core1 从内存中读取变量 a 时,它会同时将变量 b 读入缓存。(顺便说一下,我认为 CPU 从内存中批量读取变量的主要原因是基于空间局部性理论:当 CPU 访问一个变量时,它可能很快就会读取它旁边的变量。)(译者注:关于空间局部性理论可以参考 这篇文章

该缓存架构存在一个问题:如果一个变量存在于不同 CPU 核心中的两个缓存行中,如图 2 所示:

[译] 什么是缓存 false sharing 以及如何解决(Golang 示例)

当 core1 更新变量 a 时:

[译] 什么是缓存 false sharing 以及如何解决(Golang 示例)

当 core2 读取变量 b 时,即使变量 b 未被修改,它也会使 core2 的缓存未命中。所以 core2 会从内存中重新加载缓存行中的所有变量,如图 4 所示:

[译] 什么是缓存 false sharing 以及如何解决(Golang 示例)

这就是缓存 false sharing:一个 CPU 核更新变量会强制其他 CPU 核更新缓存。而我们都知道从缓存中读取 CPU 的变量比从内存中读取变量要快得多。因此,虽然该变量一直存在于多核中,但这会显著影响性能。

解决该问题的常用方法是缓存填充:在变量之间填充一些无意义的变量。使一个变量单独占用 CPU 核的缓存行,因此当其他核更新时,其他变量不会使该核从内存中重新加载变量。

我们使用如下的 Go 代码来简要介绍缓存 false sharing 的概念。

这是一个带有三个 uint64 变量的结构体,

type NoPad struct {
	a uint64
	b uint64
	c uint64
}

func (myatomic *NoPad) IncreaseAllEles() {
	atomic.AddUint64(&myatomic.a, 1)
	atomic.AddUint64(&myatomic.b, 1)
	atomic.AddUint64(&myatomic.c, 1)
}
复制代码

这是另一个结构,我使用 [8]uint64 来做缓存填充:

type Pad struct {
	a   uint64
	_p1 [8]uint64
	b   uint64
	_p2 [8]uint64
	c   uint64
	_p3 [8]uint64
}

func (myatomic *Pad) IncreaseAllEles() {
	atomic.AddUint64(&myatomic.a, 1)
	atomic.AddUint64(&myatomic.b, 1)
	atomic.AddUint64(&myatomic.c, 1)
}
复制代码

然后写一个简单的代码来运行基准测试:

func testAtomicIncrease(myatomic MyAtomic) {
	paraNum := 1000
	addTimes := 1000
	var wg sync.WaitGroup
	wg.Add(paraNum)
	for i := 0; i < paraNum; i++ {
		go func() {
			for j := 0; j < addTimes; j++ {
				myatomic.IncreaseAllEles()
			}
			wg.Done()
		}()
	}
	wg.Wait()

}
func BenchmarkNoPad(b *testing.B) {
	myatomic := &NoPad{}
	b.ResetTimer()
	testAtomicIncrease(myatomic)
}

func BenchmarkPad(b *testing.B) {
	myatomic := &Pad{}
	b.ResetTimer()
	testAtomicIncrease(myatomic)
}
复制代码

使用 2014 年的 MacBook Air 做的基准测试结果如下:

$> go test -bench=.
BenchmarkNoPad-4 2000000000 0.07 ns/op
BenchmarkPad-4 2000000000 0.02 ns/op
PASS
ok 1.777s
复制代码

基准测试的结果表明它将性能从 0.07 ns/op 提高到了 0.02 ns/op,这是一个很大的提高。

你也可以用其他语言测试这个,比如 Java,我相信你会得到相同的结果。

在将其应用于你的代码之前,应该了解两个要点:

  1. 确保系统中 CPU 的缓存行大小:这与你使用的缓存填充大小有关。
  2. 填充更多变量意味着消耗更多内存资源。在你的方案中运行基准测试以确保这些内存消耗是值得的。

我的所有示例代码都在 GitHub 上。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

C++ Primer 中文版(第 5 版)

C++ Primer 中文版(第 5 版)

[美] Stanley B. Lippman、[美] Josée Lajoie、[美] Barbara E. Moo / 王刚、杨巨峰 / 电子工业出版社 / 2013-9-1 / CNY 128.00

这本久负盛名的 C++经典教程,时隔八年之久,终迎来史无前例的重大升级。除令全球无数程序员从中受益,甚至为之迷醉的——C++ 大师 Stanley B. Lippman 的丰富实践经验,C++标准委员会原负责人 Josée Lajoie 对C++标准的深入理解,以及C++ 先驱 Barbara E. Moo 在 C++教学方面的真知灼见外,更是基于全新的 C++11标准进行了全面而彻底的内容更新。......一起来看看 《C++ Primer 中文版(第 5 版)》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

MD5 加密
MD5 加密

MD5 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具