啥是 CPU 缓存?又如何提高缓存命中率呢?

发布于: 2020 年 06 月 20 日

1 什么是CPU缓存

1.1 CPU缓存的来历

CPU缓存的容量比内存小的多但是交换速度却比内存要快得多。缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快很多,这样会使CPU花费很长时间等待数据到来或把数据写入内存。所以,为了解决CPU运算速度与内存读写速度不匹配的矛盾,就出现了CPU缓存。

1.2 CPU缓存的概念

CPU缓存是位于CPU与内存之间的临时数据交换器,它的容量比内存小的多但是交换速度却比内存要快得多。为了简化与内存之间的通信,高速缓存控制器是针对数据块,而不是字节进行操作的。高速缓存其实就是一组称之为缓存行(Cache Line)的固定大小的数据块组成的,典型的一行是64字节。

1.3 CPU缓存的意义

CPU往往需要重复处理相同的数据、重复执行相同的指令,如果这部分数据、指令CPU能在CPU缓存中找到,CPU就不需要从内存或硬盘中再读取数据、指令,从而减少了整机的响应时间。所以,缓存的意义满足以下两种局部性原理

  • 时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。

  • 空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。

任何代码的执行都依赖 CPU,通常,使用好 CPU 是操作系统内核的工作。然而,当我们编写计算密集型的程序时,CPU 的执行效率就开始变得至关重要。如果运算时需要的输入数据是从 CPU 缓存,而不是内存中读取时,运算速度就会快很多。所以,了解 CPU 缓存对性能的影响,便能够更有效地编写我们的代码,优化程序性能。

2 CPU的三级缓存

CPU 缓存离 CPU 核心更近,由于电子信号传输是需要时间的,所以离 CPU 核心越近,缓存的读写速度就越快。但 CPU 的空间很狭小,离 CPU 越近缓存大小受到的限制也越大。所以,综合硬件布局、性能等因素,CPU 缓存通常分为大小不等的三级缓存:L1L2L3。级别越小越接近CPU,所以速度也更快,同时也代表着容量越小。

L1 是最接近CPU的, 它容量最小(例如:32K),速度最快,每个核上都有一个 L1 缓存,L1 缓存每个核上其实有两个 L1 缓存, 一个用于存数据的 L1d Cache(Data Cache),一个用于存指令的 L1i Cache(Instruction Cache)。

L2 缓存 更大一些(例如:256K),速度要慢一些, 一般情况下每个核上都有一个独立的L2 缓存;

L3 缓存是三级缓存中最大的一级,同时也是最慢的一级, 在同一个CPU插槽之间的核共享一个 L3 缓存。如下这是我的开发机的CPU缓存大小情况。

程序执行时,会先将内存中的数据载入到共享的三级缓存中,再进入每颗核心独有的二级缓存,最后进入最快的一级缓存,之后才会被 CPU 使用,就像下面这张图。

如果 CPU 所要操作的数据在缓存中,则直接读取,这称为缓存命中。命中缓存会带来很大的性能提升,因此,我们的代码优化目标是提升 CPU 缓存的命中率。

2 提升代码缓存命中率

2.1 GO基准测试

package array
import "testing"
const LEN = 2048
func BenchmarkArrayHead(t *testing.B) {
//连续访问
var arr [LEN][LEN]int
t.ResetTimer()
for i := 0; i < LEN; i++ {
for j := 0; j < LEN; j++ {
arr[i][j] = 0
}
}
}
func BenchmarkEnd(t *testing.B) {
//非连续访问
var arr [LEN][LEN]int
t.ResetTimer()
for i := 0; i < LEN; i++ {
for j := 0; j < LEN; j++ {
arr[j][i] = 0
}
}
}

基准测试性能结果

goos: darwin
goarch: amd64
pkg: demo/array
BenchmarkArrayHead-8 1000000000 0.00372 ns/op
BenchmarkEnd-8 1000000000 0.0240 ns/op
PASS
ok demo/array 0.279s

通过上边的GO语言基准测试代码,前者顺序遍历与后者非连续遍历,性能差距20倍之多。

2.2 PHP基准测试

<?php
ini_set('memory_limit', '256M');
//初始化Packed Array
$max = 2048;
$arr = [];
for ($i = 0; $i < $max; $i++) {
for ($j = 0; $j < $max; $j++) {
$arr[$i][$j] = 0;
}
}
//非连续访问
$stime = microtime(true);
for ($i = 0; $i < $max; $i++) {
for ($j = 0; $j < $max; $j++) {
$arr[$j][$i] = 0;
}
}
$etime = microtime(true);
echo "非连续访问 :\t" . ($etime - $stime) . "\n";
//连续访问
$stime = microtime(true);
for ($i = 0; $i < $max; $i++) {
for ($j = 0; $j < $max; $j++) {
$arr[$i][$j] = 0;
}
}
$etime = microtime(true);
echo "连续访问 :\t" . ($etime - $stime) . "\n";

php test.php
非连续访问 : 1.3713099956512
连续访问 : 0.97811102867126

由于PHP数组容器与GO等其他语言存在差异性,性能差距不会很大。

如果用顺序访问数组元素,因此访问arr[0][0]时,缓存已经把紧随其后的 3 个元素也载入了,CPU 通过快速的缓存来读取后续 3 个元素。如果用非连续来访问,此时内存是跳跃访问的,如果 N 的数值很大,那么操作arr[j][i]时,是没有办法把 arr[j+1][i]也读入缓存的。

3 结论

在做密集计算场景时,CPU缓存对程序有着不小的影响,无论我们使用的是何种语言,这一结论都有效。CPU 缓存分为数据缓存与指令缓存,对于数据缓存,我们应在循环体中尽量操作同一块内存上的数据,所以顺序地操作连续内存数据时也有性能提升。

在日常业务迭代中,应该有意识的注重这些更底层、细节的优化,能为我们节约出客观的硬件成本。在高性能场景中,我们所写的代码就是在极限压榨硬件性能、最大化利用系统本身的缓存资源,如如何实现百万主机的心跳服务,需要我们去仔细思考,充分压榨硬件资源优势。当然我们也可以通过阅读Swoole的代码,来了解Swoole是如何实现心跳包管理的,因为是更基础的底层服务,所以Swoole在设计之初就已充分的考虑到了各种场景、以及所面临的性能问题。

用户头像

八两

关注

还未添加个人签名 2018.08.06 加入

还未添加个人简介

评论

发布
暂无评论
啥是CPU缓存?又如何提高缓存命中率呢?