admin管理员组

文章数量:1599532

CPU三级缓存技术解析
cpu存取数据
cpu存取数据大致可以认为是下图的流程(此处图比较简单)

cpu拿到需要的内存地址,之后这个地址会被mmu转换成真正的物理地址,接下来会去查接下来查L1 cache,L1 cache不命中查L2 cache,L2 cache不命中查L3 cache,L3 cache不能命中查内存。
其实现在查到内存还算完,现在有了虚拟内存,内存其实也是一层cache,是磁盘的cache,也就是说查内存也有可能不会命中,因为内存中的数据可能被虚拟内存系统放到磁盘中了,如果内存也不能命中就要查磁盘。
为什么需要cache
程序局部性原理
如果访问内存中的一个数据A,那么很有可能接下来再次访问到,同时还很有可能访问与数据A相邻的数据B,这分别叫做时间局部性和空间局部性。
cpu cache 有多快
根据摩尔定律,CPU 的访问速度每 18 个月就会翻 倍,相当于每年增⻓ 60% 左右,内存的速度当然也会不断增⻓,但是增⻓的速度远小于 CPU,平均每年 只增⻓ 7% 左右。于是,CPU 与内存的访问性能的差距不断拉大。
为了弥补 CPU 与内存两者之间的性能差异,就在 CPU 内部引入了 CPU Cache,也称高速缓存。
CPU Cache 通常分为大小不等的三级缓存,分别是 L1 Cache、L2 Cache 和 L3 Cache。其中L3是多个核心共享的。
程序执行时,会先将内存中的数据加载到共享的 L3 Cache 中,再加载到每个核心独有的 L2 Cache,最后 进入到最快的 L1 Cache,之后才会被 CPU 读取。之间的层级关系,如下图。

越靠近 CPU 核心的缓存其访问速度越快

cpu cache 读取过程
CPU Cache 的数据是从内存中读取过来的,以一小块一小块读取数据的,而不是按照单个数组元素来
读取数据的,在 CPU Cache 中的,这样一小块一小块的数据,称为 Cache Line(缓存块)。
可以在linux机器下执行一下命令查询L1cache的大小,单位是字节
#查看cache line 大小
cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
#查看各级缓存大小 inde0-3分别是 L1数据缓存 L1指令缓存 L2数据缓存 L3数据缓存
cat /sys/devices/system/cpu/cpu0/cache/index0/size
比如,有一个 int array[100] 的数组,当载入 array[0] 时,由于这个数组元素的大小在内存只占 4 字 节,不足 64 字节,CPU 就会顺序加载数组元素到 array[15] ,意味着 array[0]~array[15] 数组元素都会 被缓存在 CPU Cache 中了,因此当下次访问这些数组元素时,会直接从 CPU Cache 读取,而不用再从内 存中读取,大大提高了 CPU 读取数据的性能。
如何写出让cpu跑的更快的代码
其实,这个问题定义为如何提高cpu缓存利用率更好
大家可以看下如下代码哪个执行效率更高
func main() {
n := 100
x := 0
arr := createArray(n)
//var arr [][]int
t := time.Now().UnixNano()
for i := 0; i < n; i++ {
for j := 0; j < n; j++ {
x = arr[i][j]
}
}

t1 := time.Now().UnixNano()
for i := 0; i < n; i++ {
for j := 0; j < n; j++ {
x = arr[j][i]
}
}
fmt.Println(x)

}

//创建二维数组
func createArray(n int) [][]int {
var arr [][]int

for i := 0; i < n; i++ {
var tmp []int
for j := 0; j < n; j++ {
tmp = append(tmp, i+j)
}
arr = append(arr, tmp)
}

return arr
}

/**
经过测试,形式一 array[i][j] 执行时间比形式二 array[j][i] 快好几倍。
之所以有这么大的差距,是因为二维数组 array 所占用的内存是连续的,比如⻓度 N 的指是 2 的 话,那么内存中的数组元素的布局顺序是这样的:
array[0][0] array[0][1] array[1][0] array[1][1]
形式一用 array[i][j] 访问数组元素的顺序,正是和内存中数组元素存放的顺序一致。当 CPU 访问 array[0][0] 时,由于该数据不在 Cache 中,
于是会「顺序」把跟随其后的 3 个元素从内存中加载到 CPU Cache,这样当 CPU 访问后面的 3 个数组元素时,就能在 CPU Cache 中成功地找到数据,
这意味着缓存命中率很高,缓存命中的数据不需要访问内存,这便大大提高了代码的性能。
而如果用形式二的 array[j][i] 来访问,则访问的顺序就是:
array[0][0] array[1][0] array[0][1] array[1][1]
可以看到,访问的方式跳跃式的,而不是顺序的,那么如果 N 的数值很大,那么操作 array[j][i] 时,是 没办法把 array[j+1][i] 也读入到
CPU Cache 中的,既然 array[j+1][i] 没有读取到 CPU Cache,那么就 需要从内存读取该数据元素了。很明显,这种不连续性、跳跃式访问数据元素
的方式,可能不能充分利用 到了 CPU Cache 的特性,从而代码的性能不高。那访问 array[0][0] 元素时,CPU 具体会一次从内存中加载多少元素到
CPU Cache 呢?这个问题,在前 面也提到过,这跟 CPU Cache Line 有关,表示 CPU Cache 一次性能加载数据的大小,可以在 Linux 里通过
coherency_line_size 配置查看大小,通常是 64 个字节。
*/
cpu cache的结构
CPU Cache 是由很多个 Cache Line 组成的,CPU Line 是 CPU 从内存读取数据的基本单位,而 CPU Line 是由各种标志(Tag)+ 数据块(Data Block)组成

cpu cache数据的写入
事实上,数据不止有读取还有写入,如果数据写入cache之后,内存和cache的数据就不同了,需要把cache同步到内存中。
问题的关键就在于在什么时机去把数据写到内存?一般来讲有以下两种策略
写直达
保持内存与 Cache 一致性最简单的方式是,把数据同时写入内存和 Cache 中,这种方法称为写直达 (Write Through)。

在这个方法里,写入前会先判断数据是否已经在 CPU Cache 里面了:
如果数据已经在 Cache 里面,先将数据更新到 Cache 里面,再写入到内存里面; 如果数据没有在 Cache 里面,就直接把数据更新到内存里面。
写直达法很直观,也很简单,但是问题明显,无论数据在不在 Cache 里面,每次写操作都会写回到内存, 这样写操作将会花费大量的时间,无疑性能会受到很大的影响。
写回
由于写直达的机制会有性能问题,所以产生了写回(Write Back)的方法
在写回机制中,当发生写操作时,新的数据仅仅被写入 Cache Block 里,只有当修改过的 Cache Block 「被替换」时才需要写到内存中,减少了数据写回内存的频率,这样便可以提高系统的性能。

  1. 如果当发生写操作时,数据已经在 CPU Cache 里的话,则把数据更新到 CPU Cache 里,同时标记 CPU Cache 里的这个 Cache Block 为脏(Dirty)的,这个脏的标记代表这个时候, CPU Cache 里面的这个 Cache Block 的数据和内存是不一致的,这种情况是不用把数据写到内存里的;
  2. 如果当发生写操作时,数据所对应的 Cache Block 里存放的是「别的内存地址的数据」的话,就要检 查这个 Cache Block 里的数据有没有被标记为脏的,如果是脏的话,就要把这个 Cache Block 里的数据写回到内存,然后再把当前要写入的数据,写入到这个 Cache Block 里,同时标记为脏的;如果 Cache Block 里面的数据没有被标记为脏,则就直接将数据写入到这个 Cache Block 里,然后再把这个 Cache Block 标记为脏的就好了。
    可以发现写回这个方法,在把数据写入到 Cache 的时候,只有在缓存不命中,同时数据对应的 Cache 中 的 Cache Block 为脏标记的情况下,才会将数据写到内存中,而在缓存命中的情况下,则在写入后 Cache 后,只需把该数据对应的 Cache Block 标记为脏即可,而不用写到内存里。
    这样的好处是,如果大量的操作都能够命中缓存,那么大部分时间里 CPU 都不需要读写内存,自然性 能相比写直达会高很多。
    缓存一致性问题
    现在的CPU都是多核的,由于L1/L2cache是各个核心独有的,那么会带来多核心的缓存一致性问题,如果不能保证缓存一致性问题就会造成错误的结果
    那缓存一致性的问题具体是怎么发生的呢?以一个含有两个核心的 CP

本文标签: 缓存技术CPU