1.Cache详解
1.1 什么是Cache
Cache的作用是CPU和主存之间的缓冲区。因为主存的速度还是比不上CPU的速度,因此我们需要更快的存储设备来和CPU直接交互,所以就有了Cache。Cache的主要原理就是SRAM,在上一篇文章中已经详细地讲解过了。其缺点就是体积比DRAM大,而且造价高昂,所以容量不多,但是非破坏性读取使得它的速度很快,因此,我们只是在启动电脑程序以后把很少的一部分
常用数据
存到Cache中,多数的部分其实还是由主存来存,但是这样已经可以极大地提高运算的速度。比如打一个游戏,进入游戏以后如果点击了战斗模式,那么计算机就会自动把战斗相关的代码和数据尽可能地导入到Cache中去,让你的战斗不存在卡顿。Cache几乎比CPU快了60倍,但是造价使之限定了容量,即便是现在的CPU里面的缓存都只是十几二十MB。
1.2 计算机如何知道要放什么数据进Cache
计算机把数据放入Cache遵循的是两个原理:
(1)
空间局部性原理
:用户很有可能要读取和当前数据地址连续的后面几个数据。这是因为一篇文章也好,一部电影甚至是程序的代码多数都是连续存储的,所以用户在读了前面一个以后,大概率会继续读取或是计算后面的数据。所以计算机在读了当前需要的数据进入Cache以后,立马就把和这些数据地址连续的一部分数据也给读到Cache。
(2)
时间局部性原理
:写程序最常用的就循环体,所以计算机当前读入Cache的数据,很可能马上又会用到。所以数据执行结束以后,也会在Cache中停留一段时间。
所以如果写的程序,用到的数据地址多是不连续的,会导致计算机的运行的速度慢,地址不连续不影响从主存里面读取数据的速度,但是却会总是导致计算机不能从Cache中找到需要的数据,需要直接去内存里面找(内存里面找到以后会把数据同时送给CPU和Cache,同时还会根据空间局部性原理把数据给放入Cache,也就是CPU也能直接从主存读数据,不是只能从Cache读)。
1.3 Cache的性能分析
如果运行中,CPU成功从Cache里面找到了所需要的数据,那么我们就称
Cache命中
,否则就称
未命中
。但是计算性能的时候还需要分两种情况:
(1)
CPU不能同时访问Cache和主存
(老电脑有,现在应该已经没了):
那么假设CPU命中Cache的概率是
H
c
H_c
H
c
,CPU从Cache读取1bit数据需要
t
c
t_c
t
c
秒,而从主存读取1bit数据需要
t
m
t_m
t
m
秒,
那么CPU在运行中实际读取1bit数据的平均时间就是
H
c
×
t
c
+
(
1
−
H
c
)
×
(
t
m
+
t
c
)
H_c\times t_c+(1-H_c)\times( t_m+t_c)
H
c
×
t
c
+
(
1
−
H
c
)
×
(
t
m
+
t
c
)
秒
。在上面的公式里面,关键的部分就是如果没有命中,你不能只算
t
m
t_m
t
m
,而是应该计算
t
m
+
t
c
t_m+t_c
t
m
+
t
c
这是因为,
CPU会先去Cache里面寻找,这个寻找的时间必须算进去
。这里插入一个题外话,
t
c
t_c
t
c
和
t
m
t_m
t
m
都是CPU找数据的查找时间,因为找到以后读取就是瞬间的事情。然后有的人会认为
t
c
t_c
t
c
和
t
m
t_m
t
m
是平均值需要计算,但是其实无论是Cache还是主存都是随机存储器,所以CPU查看数据在不在的方法就是查一下映射的地址(下面细讲)在不在,都是直接完成的,所以不需要额外的计算,无论数据在哪个位置
t
c
t_c
t
c
和
t
m
t_m
t
m
都是不会变的。
(2)
CPU可以同时访问Cache和主存
(现在电脑都是这种,所以这也是最易考的情况):
如果CPU可以同时访问两个,那么它在找数据的时候,就会同时去Cache和主存里找,如果命中了则立即停止主存的寻找,这种情况花的时间是
t
c
t_c
t
c
,如果没有找到则继续在主存内找,这种情况花的时间是
t
m
t_m
t
m
于是我们就可以知道,
平均寻找时间是
:
H
c
×
t
c
+
(
1
−
H
c
)
×
t
m
H_c\times t_c+(1-H_c)\times t_m
H
c
×
t
c
+
(
1
−
H
c
)
×
t
m
。不难看到这样的情况更好计算,同时效率也更高。
所以考试一定要审题,408不难,但是坑多。
1.4 Cache的地址
Cache的块和SSD的块其实含义上是一样的,都是一些最小单元的集合。Cache的块一般为1KB,这时候,主存的存储单元也会按照1KB来分块,这样,
当CPU读取了主存中某一块的一部分内容时,主存中这个块里面剩下的内容也会被加入到Cache中,所以空间局部性原理里面连续的空间指的是块
。还需要注意的是,主存的块也被称为页或者页面,Cache的块也被称为行,但是页在SSD里面是最小的存储单元,所以为了不引发歧义,一般就用块来称呼,但是做题遇到也要能分辨。
如图所示,它们的地址就可以被分为
块号加上块内地址。
1.5 Cache和主存的映射方式(超重点,必考考点,大题选择都考)
如何让CPU明白Cache里面的数据来自主存的哪里,避免重复寻找。又或者如何让CPU知道要把主存的数据放入到 Cache时需要放到哪个地址,这必定少不了Cache和主存的映射关系,它就像是一座桥梁,连接了Cache和主存的数据。一般有三种映射方式,但是无论是哪种方式,Cache的每一块都有一个额外的电路存放了一些特殊的信息,包括了
标记位和内存的块号
,标记位标记了这个位置是否为空(既方便CPU查询也能区分内存的0号位在哪里),之后的内存的块号则是代表这里存放的数据来自内存的哪个位置。
1.5.1 全相联映射
就是计算机主存中的每一个位置的块,都可以放到Cache里面的任何位置
。这样听起来有些抽象,你可以想像一下,如果你的卧室里面有一个书架,然后你又没有对书进行分类的话,你想把书放在书架的哪个位置都可以,并不是说某一本书只能放在书架的某一个具体位置。具体可以根据王道PPT的这张图来进行理解:
这里描述Cache块用的是”行”,因为主存和Cache的块大小是一致的,所以即便没有说明主存的块的大小,我们也可以知道,主存的块也是64B也就是
2
6
2^6
2
6
B,记录块内地址需要6位。之后,主存空间是256MB则可以计算出主存有
2
28
÷
2
6
=
2
22
2^{28}\div2^6=2^{22}
2
28
÷
2
6
=
2
22
个块。则主存的块号需要22位二进制来表示,Cache的内存块号标记也需要22位(存个22位的标记其实是由额外的小硬件来实现的,不占用内存和Cache的存储单元,现在的技术造个1MB的小元件很简单)。
因为是全相联映射,所以从图上可以看到,主存中的块在Cache里面的位置是随机的,没有具体位置的规定。
传给CPU的其实是主存里面的地址,那么根据前面的描述,CPU会同时在Cache里面找有没有对应的内存块标记,和主存中查询,如果在Cache里面找到了,还要看看是不是空的,不是空的就取走,如果没有找到或者是空的,那么就从主存拿。
缺点是位置的存放没有规律,不好管理和实现。
1.5.2 直接映射
**在直接映射中,从主存放到Cache的块都是有固定的位置的,具体的计算方法就是用当前的主存块的块号去除以Cache的总块数,获得的余数就是这个主存块能够被存在Cache的位置。**但是主存的块数极多,总是会有两个主存块在用块号去除以Cache的块数以后得到的余数是相同的,从而出现冲突。
如图所示,0和8就发生了冲突,所以后面来的就要覆盖前面一个。
其实计算机在进行直接映射的时候,并不会进行真的除法运算然后还进行余数的取舍,这是因为可以直接看位数来判断:
就拿这个图来举例,因为Cahce有8个块,所以它的块号可以用3个二进制位来表示,所以只需要看主存块号的后3位即可,比如0和8的末尾3位都是0,这也是计算机为什么会选择用二进制来作为计算机的语言的原因。同时,为了节约空间,
标记位的最后3位也可以被省去
,因为末尾3位就是Cache的块号,没有必要重复存储。所以,
如果Cache的块数是
2
n
2^n
2
n
的时候,主存中块的末尾n位就是这个块在Cache中被存放的位置,同时将主存块的其余位作为标记即可。
那么CPU读取数据的过程就是这样的,先根据地址的末尾n位找到对应的Cache块,之后看看前面的标记位和地址上的末尾n位之前的位数是不是一样的,如果是一样的,并且Cache不是空的,则命中,否则还是去到主存中取数据。
1.5.3 组相连映射
组相连映射是前面两种映射的集合,
其思想是把Cache中的块分组,比如2块为一组,或者4块为一组,几块为一组就称为几路组相联映射,之后求一下有多少组,获得组的个数。在把主存放入Cache的时候,也就是看主存块号和组数除法的余数确定它是在哪个组(其实可以用上面的方法直接看末尾n位),在组内则是全相连映射(想放在哪里就放在哪里)
。一样的,因为组号是已经知道的,所以存标记位就可以少存n位。下面这个图就不需要再描述了:
1.5.4 三种映射关系的对比
(1)全相连映射的优点是
不容易起冲突
,除非Cache存满了,所以全相连映射的命中率最高,但是缺点是
CPU需要数据的时候,要一个个标记位对比过来费时间,并且标记位必须完整的写成主存的块号
。
(2)直接映射的优点是
CPU找起来方便
,并且可以节省几位的标记位,缺点就是
容易发生冲突,Cache可能需要频繁地更换数据,不符合时间局部性原理,导致命中率可能不高
。
(3)
组相连映射兼具了前两种的优点,查起来不那么费时间,并且节省了位数,这也是当前用的最多的方法。
1.6 Cache的替换算法
全相连映射和组相连映射,当整个Cache或者整个组已经满了以后,需要换掉其中的某一块,那么关键之处是,对于全相连映射而言,换哪一块都行,对于组相连映射而言,换组内的哪个都行,所以换哪块是一个大问题
(但是直接映射不需要替换算法,因为都是一对一的关系,所以在换哪块上面没有什么需要疑问的)。无论是是全相连映射还是组相连映射,他们的替换算法都是一样的,主流的一共有五种。
1.6.1 随机替换算法(RAND)
随机替换算法RAND的思想很简单
,当Cache的块满了需要被替换的时候,
随机地选一块进行替换
。这个算法其实没有什么好说的,几乎所有的程序语言也已经预设好了随机数的函数,所以直接调用即可实现这个算法。它的优点显而易见,就是简单,对程序员来说简单,但是缺点就是使用的时候很不稳定,所以基本没有人用。
1.6.2 先进先出算法(FIFO)
先进先出算法(FIFO)的思想和人类基本的思想是一样的,谁先进来的,就最先出去
,每次替换最先被调入Cache的。这个算法不符合局部时间性原理(因为最先进来的也可能一直在用),优点也是实现起来十分简单,只需要一组专门的简单电路就可以实现,还比RAND稳定些。也是基本没有啥人用。
FIFO算法和RAND算法可会导致
抖动
现象,抖动就是刚刚被换出的块又需要被换入,命中率很低。
1.6.3 最不经常使用算法(LFU)
最不经常使用算法
其实是为了弥补前面两种算法的缺点而产生的。因为时间局部性原则,我们自然会想到给每一个Cache块增加一个计数器,每命中一次就加1。如果替换的时候计数器有重复的,则随机选一个或者就先进先出。但是这个算法有时候命中率也不高,因为它不符合
局部
这两个字,所以会出现有的块明明已经不再使用了,但是因为它的计数器很高,所以一直停留被占用,并且它的计数器的位数无法确定,所以位数必须设置得很长。所以这种方法也不常用。
1.6.4 近期最少使用算法(LRU)(重点)
近期最少使用算法,简称LRU。
听名字就可以知道,它的核心思想是换出最近很少使用的块,这是LFU的进一布改进,让计数器计算这个块有多久不被使用了。这其实才是目前最常使用的替换算法,因为它充分地考虑了时间局部性原理。但是LRU算法要复杂一点,因为它要给每一个块一个计数器来记录其命中次数(虽然软件实现很简单,但是硬件的话还是有些小占空间)。
所以使用LRU算法,则在有效位,标志位之前还需要额外的加一个计数位
。具体的计数方法是这样的:
(1)刚刚装入Cache的时候,计数位是0。
(2)CPU每运行一次,没有被命中的非空Cache块的计数器都加1,如果命中了则把计数器置为0。
(3)如果有可以替换的空Cache当然还是会优先把块放入空的里面。
这里还可以进一步优化计数器,就是在加1的时候,仅仅把非空未命中且当前的计数器比
命中的那个要小的
几个块加1,因为其他的数本身就比这些数大,即便是加1之后也顶多一样大,所以不需要大费周章,最后要注意的是,
替换了新块进来的时候是全部都加1
。这个时候,相信聪明一些的读者会有疑惑,再有Cache块被命中且需要加1的时候,
是不是需要小于等于它的数据都加1
,其实如果说成小于等于它的数据都加1也是对的,这是因为,
按照我们上面说的计数规则,可以保证每一次命中或者未命中,每一个块的计数器的数字都是不一样的,并且如果有
2
n
2^n
2
n
块,则只需要n位的计数器就可以表示全部的计数情况
(其原理就是我们是一次次的变换的,所以先加入的一个肯定比后加入的一个,如果都没有命中则大了1,也不会出现两个块计数器同时为0的情况,同时最大的情况也不会出现,因为出现最大值则说明满了,那么如果要调入新的,这个最大的就会被换了,如果命中其他的,这个最大的也不会增加,同时,比被命中的块的计数器要更小的计数器的下一位正好会替补这个计数器的位数)。关说是很难理解的,所以可以自己动手写一下,或者把上面黑体加粗的字背下来,好好体会。
强烈建议照着上面的写一遍计数器,加深记忆。
这个世界上没有完美无缺的东西,就像歌里面唱的,谁的青春不遗憾。所以,
LUR算法也有缺点
,
如果主存里面需要频繁访问的块数大于了Cache里面的块数,则也会频繁地抖动
。举个例子,比如现在程序的循环语句涉及到了主存的8个主存块,但是Cahche只有4块,则满了以后就会一直处于替换之中,根本无法命中(其实这时候退化成了FIFO算法)。那有没有算法可以弥补这种缺点呢,真的有,替换算法不仅仅这四种,但是考试只考这四种,所以其他的就感兴趣自己百度一下,或者可以自行创造一下,或许你就发明了更好的算法呢。
1.7 Cache的写策略
如果CPU把主存中某一个块中的数据改变了,同时Cache中有这个块的副本,因为Cache和主存中位置映射相同的块所存的数据应该是相同的,所以怎么保证在改变主存数据(也就是往主存写入数据)的时候,保证Cache和主存中对应的数据是相同的是一个问题。写策略就是来解决这个问题的。可以分成两种情况,要修改的主存的块在Cache中,这种情况我们称之为
写命中
和要修改的主存的块不在Cache中,这种情况叫
写不命中
。每一种情况都有两种策略。
1.7.1 写命中
(1)
写回法
:当需要修改主存内容的时候,因为这部分内容已经在Cache中,所以
CPU其实只需要先在Cache中修改,在这个Cache块要被换出的时候,再一次修改主存的内容
。这么做的优点是,在要修改的块需要多次连续的修改的时候,就不需要不停地访问主存了,一次改好一次放进去,省时省力。如果采用的是写回法,那么必须在有效位旁边再加一个
脏位
的信息,来标记这块是否被写过。
写回法的缺点是会直接导致主存和Cache在一段时间内数据是不一致的,这个时候如果Cache坏了,那么数据就丢失了。
(2)
全写法
:全写法也叫做写直通法,和写回法不同的是,它
同时把数据写入到Cache和主存
,优点是保证了两边数据的一致性,不会出错,但是缺点就是慢,如果写的数据非常重要,那就用全写法。
如果数据都很重要,那么为了加快全写法的速度,需要在主板上额外加一个
写缓冲
。写缓冲是由SRAM制作的,所以写数据到写缓冲里面和写道Cache里面是一样快的,CPU先快速地把要写的数据写到Cache和写缓冲里面,之后由特殊的硬件再慢慢地从写缓冲写入到主存中(说慢其实也不是很慢)。
可以看到,写缓冲的容量并不大,那么假设CPU一直写命中,则数据就一直被丢入到写缓冲里面,最终写缓冲会被填满。**写缓冲满了以后,CPU只能先阻塞,等待写缓冲。**不用写缓冲,直接写到主存,CPU会更慢。所以全写法总的来讲,只适合数据很重要,并且要写的数据不多的情况。
1.7.2 写不命中
(1)
写分配法
:因为写不命中,所以按照正常的CPU思维,
会先把不命中的数据块从主存复制到Cache中
,
然后在Cache里面修改
。
写分配法往往是和写回法一起用的
,就是在Cache中修改好的数据块,在被换出Cache时才会写回到主存。
(2)
非写分配法
:
如果写不命中,那么直接就在主存里面修改
。如果采用这样的策略,那么其实对CPU而言,
只有读操作未命中才需要导入到Cache
。
(3)通常来说,如果一个电脑的命中策略是写回法,那么它的写不命中策略是写分配法。同样的,全写法和非写分配是一对的。前者适合硬件不那么好的电脑,后者适合硬件条件好的电脑(超大的写缓冲加上快速的CPU和主存交互技术)。
1.7.3 多级Cache
然而,现代的计算机主板已经几乎都使用了
多级Cache的技术
,如图所示:
越是靠近CPU的Cache速度就越快,但是容量越小,越是靠近主存的Cache速度越慢,但是容量也越大。下图是一个真实的CPU中,各个级Cache的交互速度。
现在为了最大化的开发电脑性能,所以
在Cache和Cache之间用的是全写法和非写分配法。在最后一级Cache和主存之间则使用写分配法加写回法
。因为Cache之间的速度也很快,更高级的Cache会把下一级的Cache看作是主存,当高级Cache不命中时,会同时在所有Cache和主存里面寻找,之后把它一层层地加到最高级的Cache中。这样就最大程度地综合了全部情况,搭配之前的近期最少使用算法,CPU可以在最高级的Cache里面找到最常用的数据,快速交互,即便常用数据较多,那么和下一级Cache频繁交互也不是那么慢。在电脑上同时按下Ctrl+Alt+del,可以打开任务管理器,之后在性能的位置可以看CPU的状态:
可以看到我的电脑有三级缓存,并且每一级的容量都是递减的。
\quad
到现在,实体存储器我们都已经学完了,接下来我们开始学习虚拟存储器。
2.虚拟存储器(常考)
因为操作系统也会教虚拟存储器,所以这里其实是学科的交叉点,那么考试的出题人出一题就可以涵盖两个科目,何乐而不为呢,所以经常变成大题考察。先声明,这里的知识和操作系统的其实大差不差,而且操作系统的更详细,如果赶时间,可以不看这里,直接去看操作系统第三章,这里只是虚拟存储器的粗略解读。
2.1 页式存储
首先要注意这里的页和SSD的页是不一样的,SSD的页是最小存储单元,
这里的页是指应用程序的数据被按照主存块的大小分成一块一块的,这样的每一块都称为一页
。比如说一个程序(准确地说是进程)的数据有10KB,主存的一块是1KB,那么这个程序就可以操作系统被分成10页,
每一页都可以在主存中离散地存储
。
那么现在的一个问题就是,进程的代码应该是连续执行的,但是实际上在内存里面它却是离散存放的,那么要如何使这些离散的数据变得连续呢?这里引出两个概念:
(1)实地址(物理地址):实际在主存里面存在的地址。
(2)虚地址(逻辑地址):程序员看到的地址。
虚地址是连续的,实地址是离散的,如何映射就是一个重点。
2.1.1 如何分页,获得逻辑地址
先简单的说一下如何把一个程序分页,分页其实很简单,比如内存块是1KB的,然后我们要16KB的进程进行分页。那么16KB显然是可以分成16页也就是
2
4
2^4
2
4
,所以只需要根据它地址的前4位划分就可以了,计算机会自动的把地址是0000的划为一组,0001的划为一组,以此类推。
对于一个进程的地址,我们就可以分为两部分:逻辑页号(表示这是第几页)+页内地址
。这里学的不扎实的人会有一个误区,就是逻辑页号不是进程的地址,但是其实逻辑页号和页内地址都是从一个进程的完整地址里面拆分出来的,我们把这个地址丢回辅存,它的地址也是逻辑页号加页内地址,但是这时候就总称其为程序地址,不再拆开。这里题目会告诉你主存的块的大小和进程数据的总大小,让你写出其有多少页,以及页内地址有多大。
2.1.2 如何把获得物理地址
需要注意的是,分好页以后放入主存的时,是不需要加上页号的,也就是说,
物理地址=主存的块号+页内地址
。这样其实寻址的时候就不需要再看页号,只需要看主存块号就行。现在剩下的问题就是,如何把主存的块号和进程的页号联系起来。
2.1.3 如何联系页号和块号(页表)
这个联系的桥梁是由操作系统来搭建的,称之为
页表
。
**页表可以看作是一个字典,键是逻辑页号,值是主存块号。每一个键值对,我们称之为一个页表项。**它将于操作系统中细讲。页表被放在主存中一个额外的空间(被操作系统占用,不会被换出)中。
2.1.4 页表基址寄存器
页表基址寄存器是CPU内一个额外的部件,
它用于存放当前的页表在主存的哪个位置
(注意,它不是存页表的,它存的只是页表在主存的位置)。
计算机在运行一段进程时,首先读取到的是程序员写的虚拟地址,然后计算机的操作系统先前已经根据主存的块的大小,把这个虚拟地址分成逻辑页号和页内地址两部分,并且放在了主存里面,同时还在主存里面生成了页表,把页表的地址放入了页表基址寄存器中,计算机先是把虚拟地址的页号和页内地址分开,然后从页表基址寄存器里面找对应的主存里面存放着的页表,然后根据页表把主存块号和页内地址相加就形成了真实的物理地址,之后的操作就是访问Cache,看看Cache里面有没有,没有的话去内存导入。
这样的缺点是,无论目标地址是否已经存入Cache,CPU都要访问一次主存去找页表。
2.1.5 快表(TLB)
为了减少程序因为寻访页表所需要的时间,这里提出了和Cache类似的解决方法,我们使用一个同样
是SRAM的元件,放在Cache旁边,称为快表(TLB),根据时间局部性原理和空间局部性原理来存放
一部分
的页表
。这里解释一下为什么不把页表放在Cache,首先如果 只是存放部分的页表,那么这个元件的容量不需要很大,新造一个单独的放在旁边,比放在Cache里面要方便和快捷。肯定也会有人疑惑慢表去哪里了,
原本主存里面存的完整的页表就是慢表
。
在引入了TLB之后,每次CPU读到一个虚拟地址,都会先去TLB里面看看能不能在到对应的页号(也就是图上的标记位),找不到则访问页表基址寄存器,然后访问主存里的慢表,同时把慢表里面被访问的这对页表项给加入到快表里面。
最后需要注意的是,
TLB是一种相联存储器
,也就是它是可以根据内容去快速查找的,不需要给他具体地址,给他内容就行。所以它设计起来甚至比Cache复杂,容量也小得多,所以极其容易存满,替换的方法也可以参考上面说过的四种方法。
2.2 虚拟存储
虚拟存储其实是模仿主存和Cache的关系,进一步的套用到了辅存和主存上。也就是根据空间和时间局部性原理,每一次仅把一个进程的一部分数据放入主存,这样一个68GB的进程(比如原神)就可以先拆分一部分放入到8GB的内存中,有时候游戏场景转换的比较慢,其实就是在更换数据到内存。但是这些操作其实是由操作系统决定的,不属于硬件的内容,现在只需要知道它和主存与Cache的数据交互很相似就行。因为主存和Cache的交互是硬件实现的(为了快),所以这里详细学习。引入了虚拟存储技术以后,其实页表是需要改动的,页表不能只用两个位,而是增加了辅存的信息,主存的信息,脏位(和前面Cache中数据的脏位是一样的,代表是否修改)等信息,具体内容会在操作系统中细谈。敬请期待后面的操作系统课程,到时候就会发现,其实当前计算机用的也不是简单的页式存储,实际上还存在段式存储,段页式存储等的内容。
本章的重点是Cache部分,必须非常了解,对于虚拟存储器的知识,如果不考操作系统,简单了解即可,下面是虚拟存储的操作系统链接:
下一章我们将开启另一个难点问题,计算机的指令系统。