CPU Cache:访问存储速度是如何大幅提升的?

news/2024/4/28 4:02:17/文章来源:https://blog.csdn.net/weixin_42136255/article/details/130342118

我们了解到不同的物理器件,它们的访问速度是不一样的:速度快的往往代价高、容量小;代价低且容量大的,速度通常比较慢。为了充分发挥各种器件的优点,计算机存储数据的物理器件不会只选择一种,而是以 CPU 为核心,由内而外地组建了一整套的存储体系结构。它将各种不同的器件组合成一个体系,让各种器件扬长避短,从而形成一种快速、大容量、低成本的内存系统。而我们要想写出高性能的程序,就必须理解这个存储体系结构,并运用好它。

存储体系结构的核心

作为程序员,我们肯定是希望有无限资源的快速存储器,来存放程序的数据。而现实是,快速存储器的制造成本很高,速度慢的存储器相对便宜。所以从成本角度来说,计算机的存储结构被设计成分层的,一般包括寄存器、缓存、内存、磁盘等

其中,缓存又是整个存储体系结构的灵魂,它让内存访问的速度接近于寄存器的访问速度。所以,要想深入理解存储体系结构,我们就要围绕“缓存”这个核心来学习。

在过去的几十年,处理器速度的增长远远超过了内存速度的增长。尤其是在 2001~2005年间,处理器的时钟频率在以 55% 的速度增长,而同期内存速度的增长仅为 7%。为了缩小处理器和内存之间的速度差距,缓存被设计出来。

我们说,距离处理器越近,访问速度就越快,造价也就越高,同时容量也会更小。缓存是处理器和内存之间的一个桥梁,通常分为多层,包括 L1 层、L2 层、L3 层等等。缓存的速度介于处理器和内存之间,访问处理器内部寄存器的速度在 1ns 以内(一个时钟周期),访问内存的速度通常在 50~100ns(上百个时钟周期)之间。那么对于缓存来讲,靠近处理器最近的 L1 层缓存的访问速度在 1ns~2ns(3 个时钟周期)左右,外层 L2 和 L3 层的访问速度在 10ns~20ns(几十个时钟周期)之间。

根据程序的空间局部性和时间局部性原理,一个处理得当的程序,缓存命中率要想达到 70~90% 并非难事。因此,在存储系统中加入缓存,可以让整个存储系统的性能接近寄存器,并且每字节的成本都接近内存,甚至是磁盘。

可见缓存结合了寄存器速度快和内存造价低的优点,是整个存储体系的灵魂之所在。明白了这一点后,接下来我们拆解一下缓存的物理架构。

缓存的物理架构

缓存是由 SRAM(静态随机存储)组成的,它的本质是一种时序逻辑电路,具体的每个单元(比特)由一个个锁存器构成,锁存器的功能就是让电路具有记忆功能。SRAM 的单位造价还是比较高的,而且要远高于内存的组成结构“DRAM(动态随机存储)”的造价。这是因为要实现一个锁存器需要六个晶体管,而实现一个 DRAM 仅需要一个晶体管和一个电容,但是 DRAM 因为结构简单,单位面积可以存放更多数据,所以更适合做内存。为了兼顾这两者的优缺点,于是它们中间需要加入缓存。

在制造方面,DRAM 因为有电容的存在,不再是单纯的逻辑电路,所以不能用 CMOS 工艺制造,而 SRAM 可以。这也是为什么缓存可以集成到芯片内部,而内存是和芯片分开制造的。

在了解了缓存的内部构成之后,我们再来看看缓存是怎样集成到芯片上的。

缓存集成到芯片的方式有多种。在过去的单核时代,处理器和各级缓存都只有一个,因此缓存的集成方式相对单一,就是把处理器和缓存直接相连。2004 年,Intel 取消了 4GHz奔腾处理器的研发计划,这意味着处理器以提升主频榨取性能的时代结束,多核处理器开始成为主流。

在多核芯片上,缓存集成的方式主要有以下三种:

集中式缓存:一个缓存和所有处理器直接相连,多个核共享这一个缓存;

分布式缓存:一个处理器仅和一个缓存相连,一个处理器对应一个缓存;

混合式缓存:在 L3 采用集中式缓存,在 L1 和 L2 采用分布式缓存。

 现代的多核处理器大都采用混合式的方式将缓存集成到芯片上,一般情况下,L3 是所有处理器核共享的,L1 和 L2 是每个处理器核特有的。了解了缓存的物理架构后,我们来看一下缓存的工作原理。

缓存的工作原理

首先,我们来理解一个概念,cache line。cache line 是缓存进行管理的一个最小存储单元,也叫缓存块。从内存向缓存加载数据也是按缓存块进行加载的,一个缓存块和一个内存中相同容量的数据块(下称内存块)对应。这里,我们先从如何管理缓存块的角度,来看下缓存块的组织形式:

 上图中的小方框就代表一个缓存块。从图中,你也可以看到,整个缓存由组(set)构成,
每个组由路(way)构成。所以整个缓存容量等于组数、路数和缓存块大小的乘积:

整个缓存容量 = 组数 × 路数 × 缓存块大小

为了简化寻址方式,内存地址确定的数据块总是会被放在一个固定的组,但可以放在组内的任意路上,也就是说,对于一个特定地址数据的访问,它如果要载入缓存,那么它放在上图中的行数是固定的,但是具体放到哪一列是不固定的。根据缓存中组数和路数的不同,我们将缓存的映射方式分为三类:

直接相连映射:缓存只有一个路,一个内存块只能放置在特定的组上;
全相连映射:缓存只有一个组,所有的内存块都放在这一个组的不同路上;
组组相连映射:缓存同时由多个组和多个路。

对于直接相连映射,当多个内存块映射到同一组时,会产生冲突,因为只有一列,这个时候就需要将旧的缓存块换出,同时将新的缓存块放入,所以直接相连映射会导致缓存块被频繁替换。

全相连映射可以在很大程度上避免冲突,不过,当要查询某个缓存块时,需要逐个遍历每个路,而且电路实现也比较困难。一个折中的办法就是,采用组组相连映射。这种方式与直接相连映射相比,产生冲突的可能性更小,与全相连映射相比,查询效率更高,实现也更简单。

上面的举例比较简单,我们再来看这样一种情况:缓存的组数一直是 2^n。虽然组数为 2^n 利于查询和定位,但是如果一个程序刚好以 2^n 间隔寻址,就会导致地址更多的被映射到同一个组,而另外一些组就会被映射得很少。因此,也有些缓存的组数会设计成一个质数,这样即便程序以 2^n间隔寻址,落到相同组的可能性会大大减小,这样一来,缓存各个组的利用率就会相对均衡。

那一个内存块具体是怎样映射到一个缓存块的呢?我们先来看看缓存块的内部结构:

其中,V(valid)表示这个缓存块是否有效,或者说是否正在被使用;M(modified)表示这个缓存块是否被写,也就是“脏”位;B 表示缓存块的 bit 个数。 

假设要寻址一个 32 位的地址,缓存块的大小是 64 字节,缓存组织方式是 4 路组相连,缓存大小是 8K。经过计算我们得到缓存一共有 32 个组( 8 × 1024 ÷ 64 ÷ 4 = 32 )。那么对于任意一个 32 位的地址 Addr ,它映射到缓存的组号(set index)为 Addr 对组数32 取模,组号同时也等于 Addr 的第 6~10 位( (Addr >> 6) & 0x1F ),Addr 的低 6位很好理解,它是缓存块的内部偏移( 2^6 为 64 字节),那么高 21 位是用来干嘛的呢?我们接着往下看。

确定需要被映射到哪个组之后,我们需要在该组的路中进行查询。查询方式也很简单,直接将每个缓存块 tag 的 bit 位和地址 Addr 的高 21 位逐一进行匹配。如果相等,就说明该内存块已经载入到缓存中;如果没有匹配的 tag,就说明缓存缺失,需要将内存块放到该组的一个空闲缓存块上;如果所有路的缓存块都正在被使用,那么需要选择一个缓存块,将其移出缓存,把新的内存块载入。

上面这个过程涉及到缓存块状态转换,而状态转换又涉及到有效位 V、脏位 M、标签tag。总体来讲,缓存的状态转换有以下几种情况:

这里我们提到了缓存块替换,当同组的缓存块都被用完时,需要选择一个缓存块被换出,那么应该选谁被换出呢?这就和缓存块替换策略有关了。

缓存块替换策略

缓存块替换策略需要达到的一个目标是:被替换出的数据块应该是将来最晚会被访问的块。然而,对将来即将发生的事情是没有办法预测的,因为处理器并不知道程序将来会访问哪个地址。因此,现在的缓存替换策略都采用了最近最少使用算法(Least Recently Used ,LRU)或者是类似 LRU 的算法。

LRU 的原理很简单,比如程序要顺序访问 B1 、B2、B3、B4、B5 这几个地址块,并且这几个缓存块都映射到缓存的同一个组,同时我们假设缓存采用 4 路组组相连映射,那么当访问 B5 时,B1 就需要被替换出来。要实现这一点,有很多种方式,其中最简单也最容易实现的是利用位矩阵来实现。

首先,我们定义一个行、列都与缓存路数相同的矩阵。当访问某个路对应的缓存块时,先将该路对应的所有行置为 1,然后再将该路对应的所有列置为 0。最终结果体现为,缓存块访问时间的先后顺序,由矩阵行中 1 的个数决定,最近最常访问缓存块对应行 1 的个数最多。

假设现在一个四路相连的缓存组包含数据块 B1、B2、B3、B4, 数据块的访问顺序为 B2、B3、B1、B4,那么 LRU 矩阵在每次访问后的变化如下图所示:

 

你会发现,最终 B2 对应行的 1 的个数最少,所以 B2 将会被优先替换。

在理解了缓存结构和它的工作原理以后,我们就可以来讨论核心内容了:如何正确地使用缓存,才可以写出高性能的程序?

缓存对程序性能的影响

通过前面的分析,我们已经知道,CPU 将未来最有可能被用到的内存数据加载进缓存。如果下次访问内存时,数据已经在缓存中了,这就是缓存命中,它获取目标数据的速度非常快。如果数据没在缓存中,这就是缓存缺失,此时要启动内存数据传输,而内存的访问速度相比缓存差很多。所以我们要避免这种情况。下面,我们先来了解一下哪些情况容易造成缓存缺失,以及具体会对程序性能带来怎样的影响。

缓存缺失

缓存性能主要取决于缓存命中率,也就说缓存缺失(cache miss)越少,缓存的性能就越好。一般来说,引起缓存缺失的类型主要有三种:

强制缺失:第一次将数据块读入到缓存所产生的缺失,也被称为冷缺失(cold miss),因为当发生缓存缺失时,缓存是空的(冷的);

冲突缺失:由于缓存的相连度有限导致的缺失;

容量缺失:由于缓存大小有限导致的缺失。

第一类强制缺失最容易理解,因为第一次将数据读入缓存时,缓存中不会有数据,这种缺失无法避免。

第二类冲突缺失是因为相连度有限导致的,这里我用一个例子给你说明一下。在这个例子中,第一步我们可以通过 getconf 命令查看缓存的信息:

wj@wj:~$ getconf -a | grep CACHE
LEVEL1_ICACHE_SIZE                 32768
LEVEL1_ICACHE_ASSOC                8
LEVEL1_ICACHE_LINESIZE             64
LEVEL1_DCACHE_SIZE                 32768
LEVEL1_DCACHE_ASSOC                8
LEVEL1_DCACHE_LINESIZE             64
LEVEL2_CACHE_SIZE                  262144
LEVEL2_CACHE_ASSOC                 4
LEVEL2_CACHE_LINESIZE              64
LEVEL3_CACHE_SIZE                  16777216
LEVEL3_CACHE_ASSOC                 16
LEVEL3_CACHE_LINESIZE              64
LEVEL4_CACHE_SIZE                  0
LEVEL4_CACHE_ASSOC                 0
LEVEL4_CACHE_LINESIZE              0

在这个缓存的信息中,L1Cache(LEVEL1_ICACHE 和 LEVEL1_DCACHE 分别表示指令缓存和数据缓存,这里我们只关注数据缓存)的 cache line 大小为 64 字节,路数为 8 路,大小为 32K,可以计算出缓存的组数为 64 组( 32K÷ 8 ÷ 64 = 64 )。

第二步,我们使用一个程序来测试缓存的影响:

#include <stdio.h>
#include <stdlib.h>#define M 64
#define N 10000000int main(int argc,char* argv[]){printf("%ld\n",sizeof(long long));long long (*a)[N] = (long long(*)[N])calloc(M * N, sizeof(long long));for(int i = 0; i < 100000000; i++) {for(int j = 0; j < 4096; j+=512) {a[5][j]++;}}return 0;
}

上面代码中定义了一个二维数组,数组中元素的类型为 long long ,元素大小为 8 字节。所以一个 cache line 可以存放 64 ÷ 8 = 8 个元素。一组是 8 路,所以一组可以存放 8 × 8= 64 个元素。一路包含 64 个 cache line,因为前面计算出缓存的组数为 64,所以一路可以存放 8 × 64 = 512 个元素。

代码中的第一层循环是执行次数,第二层循环是以 512 为间隔访问元素,即每次访问都会落在同一个组内的不同 cache line ,因为一组有 8 路,所以我们迭代到 512 × 8 = 4096的位置。这样可以使同一组刚好可以容纳二层循环需要的地址空间。运行结果如下:

wj@wj:~/WORK/Learning/DT/C++$ gcc cache.c -o cache.out
wj@wj:~/WORK/Learning/DT/C++$ time ./cache.out 
8real    0m1.213s
user    0m1.212s
sys     0m0.002s

第三步,当我们将第二层循环的迭代次数扩大一倍,也就是 8192 时,运行结果如下:

wj@wj:~/WORK/Learning/DT/C++$ gcc cache.c -o cache.out
wj@wj:~/WORK/Learning/DT/C++$ time ./cache.out 
8real    0m7.938s
user    0m7.935s
sys     0m0.004s

虽然运算量增加了一倍,但运行时间却增加了 6 倍,相当于性能劣化三倍。劣化的根本原因就是当 i > 4096 时,也就是访问 4096 之后的元素,同一组的 cache line 已经全部使用,必须进行替换,并且之后的每次访问都会发生冲突,导致缓存块频繁替换,性能劣化严重。

第三类缓存容量缺失,可以认为是除了强制缺失和冲突缺失之外的缺失,也很好理解,当程序运行的某段时间内,访问地址范围超过缓存大小很多,这样缓存的容量就会成为缓存性能的瓶颈,这里要注意和冲突缺失加以区别,冲突缺失指的是在同一组内的缺失,而容量缺失描述范围是整个缓存。

程序局部性

//未完待续....

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_103121.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java的validation框架(参数校验)

一.bean validation和hibernate validator参数校验常用约束注解&#xff1a; 空值校验类&#xff1a;Null&#xff0c;NotNull&#xff0c;NotEmpty&#xff0c;NotBlank等 范围校验类&#xff1a;Min&#xff0c;Size&#xff0c;Digits&#xff0c;Future&#xff0c;Negati…

微信小程序自定义搜索标题栏

一&#xff1a;需求 把微信小程序标题栏处变成搜索栏。自定义返回上级页面。 二&#xff1a;需求分析 首先要把小程序标题栏设置为可自定义。然后计算原标题栏的高度组成结构。根据计算高度设置搜索框和返回按钮的布局。最后进行代码功能实现。 三&#xff1a;功能实现 1&…

4月19号软件更新资讯合集....

JavaWeb 微服务前后端分离 EurekaEleVue 版 v1.5.0 发布 v1.5.0 更新如下&#xff1a; 1、解决 token 过期无法跳转至登录页的问题&#xff1b; 2、授权服务进行重构与优化&#xff1b; 一款 Java 语言基于 SpringCloud、SpringSecurity、OAuth2、Eureka、Vue、ElementUI、…

Go Fuzzing:发现你未曾发现的漏洞

文章目录 Fuzzing(模糊测试)要求示例模拟crash 总结参考资料 Fuzzing(模糊测试) go fuzz文档 对于软件开发者而言&#xff0c;一项重要的任务就是确保程序的安全性。而其中一种风险就是软件中可能存在的漏洞。传统的测试方法往往需要耗费大量的时间和人力&#xff0c;而使用F…

4月21号软件更新资讯合集.....

PlayEdu v1.0-beta.3 发布&#xff0c;视频培训解决方案 PlayEdu 是基于 SpringBoot3 Java17 React18 开发的企业内部培训系统。它专注于提供私有化部署方案&#xff0c;包括视频&#xff0c;图片等资源的内网部署。目前主要支持有本地视频上传播放、学员邮箱登录、无限级部门…

多数据源 使用 mybatis-plus-generator 3.5.1版本进行代码生成

文章目录 前言多数据源 使用 mybatis-plus-generator 3.5.1版本进行代码生成1. 说明2. 添加依赖2.1. mybatis-plus-generator 自动生成依赖2.2. 多数据源依赖2.3. 建立新项目的完全pom.xml 3. application.yml 多数据源配置 mybatis-plus-generator配置4. 创建一个MybatisPlus…

多通道振弦传感器无线采集仪 数字传感器起始通道分配

多通道振弦传感器无线采集仪 数字传感器起始通道分配 寄存器 DS_CHNUM(299)用于设置读取到的数字传感器数据从哪个通道开始占用&#xff0c;默认为 1。 单个数字传感器占用的通道数量与具体的传感器类型有关&#xff0c;例如&#xff1a;每个激光测距仪会占用 1 个通道&#xf…

Python爬虫之MongoDB

目录 一、Mongo概述 二、安装&下载 1.下载&#xff1a; 2.安装 三、基本命令 插⼊数据 查询数据 修改数据 删除数据 索引 四、Python与MongoDB交互 1.安装pymongo 2.使⽤ 一、Mongo概述 MongoDB是什么&#xff1f; MongoDB是⾮关系型数据库(No sql) 为啥需要…

基于C#asp.net心里咨询服务网站系统

功能模块&#xff1a; 主要分为管理员和注册用户&#xff0c;注册用户可以查看所有人发布的心里文章&#xff0c;情感在线问答&#xff0c;查询相似问题&#xff0c;以及进入论坛进行交流&#xff08;发帖跟帖评论收藏等&#xff09;后台管理主要是针对个人信息修改 管理员对注…

商品价格监控业务场景,API数据分析

商品价格监控指的是对特定商品价格进行实时监控和跟踪&#xff0c;及时更新最新价格并分析价格变化的行为。这种监控可以帮助企业及时了解市场行情&#xff0c;并根据价格变化情况做出相应的调整&#xff0c;以更好地应对市场变化。 一般来说&#xff0c;商品价格监控需要以下…

KVM虚拟机的磁盘无损扩容方法-qcow2格式的

起因&#xff1a;我的KVM主机上安装了基于Debian11的 虚拟机母鸡&#xff0c;其他虚拟机都由此克隆而来。因为最初只配置了8G的虚拟硬盘&#xff0c;因此在需要占用比较大的空间的应用时&#xff0c;就比较麻烦。度娘等中文搜索结果没找到答案&#xff0c;只能google了。 这里…

JavaScript概述四(DOM文档对象模型)

1.DOM(Document Object Model) 会把网页里面的元素当成对象去操作,包含对象的属性,属性值,方便我们去 操作网页。 整个页面最终会形成一个对象 :document ,页面里面的所有的元素(如 标签 ) 最终都会转换成 js 里面的对象。 1.1 获取页面的元素&#xff08;通过选择器&#xff0…

JS-11A/224时间继电器 JOSEF约瑟 板前、板后接线

系列型号&#xff1a; JS-11A/11集成电路时间继电器&#xff1b;JS-11A/12集成电路时间继电器&#xff1b; JS-11A/13集成电路时间继电器&#xff1b;JS-11A/136集成电路时间继电器&#xff1b; JS-11A/137集成电路时间继电器&#xff1b;JS-11A/22集成电路时间继电器&#…

数据结构与算法(三):数论(树形结构、二叉树、二叉搜索树、红黑树、Btree、B+Tree、赫夫曼树、堆树)

数论&#xff08;树形结构、二叉树、二叉搜索树、红黑树、Btree、BTree、赫夫曼树、堆树&#xff09; 树形结构概念 在树形结构里面重要的术语&#xff1a; 结点&#xff1a;树里面的元素。 父子关系&#xff1a;结点之间相连的边 子树&#xff1a;当结点大于1时&#xff0…

华为OD机试真题(Java),数字涂色(100%通过+复盘思路)

一、题目描述 疫情过后&#xff0c;希望小学终于又重新开学了&#xff0c;三年二班开学第一天的任务是将后面的黑板报重新制作。 黑板上已经写上了N个正整数&#xff0c;同学们需要给这每个数分别上一种颜色。 为了让黑板报既美观又有学习意义&#xff0c;老师要求同种颜色的…

LoadRunner参数化最佳实践:让你的性能测试更加出色!

距离上次使用loadrunnr 已经有一年多的时间了。初做测试时在项目中用过&#xff0c;后面项目中用不到&#xff0c;自己把重点放在了工具之外的东西上&#xff0c;认为性能测试不仅仅是会用工具&#xff0c;最近又想有一把好的利器毕竟可以帮助自己更好的完成性能测试工作。这算…

QMS-云质说质量 - 1 张小泉的质量危机

云质QMS原创 转载请注明来源 作者&#xff1a;王洪石 引言 百年老店的拍蒜质量门 最近张小泉拍蒜断刀事件&#xff0c;吸引了全民关注&#xff0c;虽然随后发布了“断刀召集令”&#xff0c;但从事件发生到后续拖沓且不专业的应对&#xff0c;张小泉肯定是“失蒜”了。 张小泉…

Spring Security实战(六)—— 跨域与CORS

跨域是一种浏览器同源安全策略&#xff0c;即浏览器单方面限制脚本的跨域访问。 一、认识跨域 跨域&#xff08;Cross-Origin&#xff09;指的是在Web开发中&#xff0c;当一个网页的内容要从不同源&#xff08;即不同的域名、协议或端口&#xff09;获取时&#xff0c;就会发…

ajax的介绍及使用

ajax的介绍 开发流程 前端 ajax:前后端沟通的桥梁 后端 ajax介绍 ajax叫做异步的Javascript和xml ajax通过浏览器与服务器&#xff08;后端&#xff09;进行少量数据交互&#xff0c;进行页面异步更新&#xff08;网页不会重新加载&#xff09; 优点&#xff1a; 减轻服务器负…

Vue3进阶使用详解(node.js、Vue3路由基础项目、axios的使用详细(实现数据分页---前后端分离)、axios加载失败)

Vue3进阶使用详解(node.js、Vue3路由基础项目、axios的使用详细(实现数据分页—前后端分离)、axios加载失败) Vue cli CLI是Commond-Line Interface&#xff0c;翻译为命令界面&#xff0c;又称脚手架。VueCLI是一个官方发布vue.js项目脚手架。使用VueCLI可以快速搭建vue开发…