全文内容主要来自对课程《深入浅出计算机组成原理》的学习笔记。
35 | 存储器层次结构全景
理解存储器的层次结构
寄存器
:更像 CPU 本身的部分,空间极其有限,但速度非常快,CPU Cache
:CPU 高速缓存,我们常常简称为“缓存”,使用 SRAM 芯片。
SRAM
SRAM
(Static Random-Access Memory),即静态随机存取存储器。只要处在通电状态,里面的数据就可以保持存在,一断电就丢失。
SRAM 中,1 bit 的数据,需要 6~8 个晶体管。存储密度不高,但电路简单,速度快。
CPU 有 L1、L2、L3 这样三层高速缓存
:L1 Cache
:每个 CPU 有自己的,在CPU内部,分成指令缓存和数据缓存;L2 Cache
:每个 CPU 有自己的,不在CPU内,速度略慢;L3 Cache
:多个 CPU 共享,尺寸大,速度更慢。
DRAM
内存和 Cache 不同,用 DRAM
(Dynamic Random Access Memory,动态随机存取存储器)芯片。密度高,大容量。
DRAM 需要靠不断地“刷新”,才能保持数据被存储。1 bit只需要一个晶体管和一个电容就能存储。存储密度大,速度比 SRAM 慢。且电容会漏电,需要定时刷新充电。
存储器的层级结构
可以看到,从 Cache 到 HDD,容量越来越大,价格越来越便宜,速度越来越慢。
故,一个完整计算机,会通过不等层级的内存组合,来实现性价比:
少量贵的存储保障热数据的速度,大量便宜的存储来提供冷数据存储
并且,各个存储器只和相邻的一层存储器打交道。
36 | 局部性原理
服务端开发时,数据一般存在数据库,访问数据库性能瓶颈是要点,一般就会用缓存来缓解,
理解局部性原理
挑战:既要 CPU Cache 的速度,又要内存、硬盘巨大的容量和低廉的价格。
解法便是局部性原理
(Principle of Locality):时间局部性
(temporal locality)和空间局部性
(spatial locality)
时间局部性
:如果一个数据被访问了,那么它在短时间内还会被再次访问。空间局部性
:如果一个数据被访问了,那么和它相邻的数据也很快会被访问。
使用缓存的时候,例如LRU
(Least Recently Used)缓存算法,需要关注缓存命中率
,越高说明缓存效果越好。
37 | 高速缓存(上)
需要高速缓存
基于摩尔定律,CPU 和 内存的访问速度差异越来越大,为了缓解数据跟不上计算的问题,在CPU中就引入了高速缓存。
在 95% 的情况下,CPU 都只需要访问 L1-L3 Cache,从里面读取指令和数据,而无需访问内存
CPU 从内存中读取数据到 CPU Cache ,是以小块为单位的而不是单个元素,在 CPU Cache 里面,叫作 Cache Line
(缓存块),常是 64 字节(Bytes)。
Cache 的数据结构和读取过程是
CPU 读取数时:
- 先访问 Cache;
- 有,则取出;
- 没有,再访问内存;
- 并将读取的数写入到 Cache。
问题:CPU 如何知道需要访问的内存数据对应的 Cache 位置呢?
答案:直接映射 Cache
(Direct Mapped Cache)。
思路:CPU 拿到的是数据所在的内存块(Block)的地址,其通过 mod 运算,固定映射到一个的 CPU Cache 地址(Cache Line),作为索引
。
mod 运算的技巧:缓存块的数量设置成 2 的 N 次方,直接取地址的低 N 位就是 mod 结果。
这时候,肯定会有多个内存块地址映射到同一个 Cache 地址,需要辨识当前存储的是哪一块。
缓存块中,我们会存储一个组标记
(Tag),记录当前缓存块内存储的数据对应的内存块。
此外,缓存块中还有:
实际存放的数据
,一个 Block;有效位
(valid bit),其0/1代表是否可用;偏移量
(Offset),记录需要取的数据在 Block 中哪个位置。
“索引 + 有效位 + 组标记 + 数据”数据结构,使得 Cache 访问时有4步:
- 取内存地址低位,计算 Cache 对应的索引;
- 根据有效位,判断 Cache 数据是否可用;
- 取内存地址高位,和组标记,确认数据是否符合为目标数据,从 Cache Line 中读取到对应的数据块(Data Block);
- 根据内存地址的 Offset 位,从 Data Block 中,读取希望读取到的字。
如在 2、3 步骤中发现数据不可用,那 CPU 就会访问内存,并把对应的 Block Data 更新到 Cache Line 中,同时更新有效位和组标记的数据。
38 | 高速缓存(下)
volatile 关键词
作者从 java 中“volatile”关键词出发,讨论它的作用和原理。
1 | public class VolatileTest { |
先说结果:
- 直接运行,ChangeListener 能够监听 COUNTER 变化;
- 去掉 volatile 则不行;
- 去掉 volatile,但是又让 ChangeListener 每次 sleep 5ms 则又可以。
volatile 会确保我们对于这个变量的读取和写入,都一定会同步到主内存里,而不是从 Cache 里面读取。
写直达(Write-Through)
每一次数据都要写入到主内存里面。
- 先判断是否在 Cache;
- 在,先写 Cache,再写入主内存;
- 不在,直接写主内存。
写回(Write-Back)
只写到 CPU Cache 里。只有当 CPU Cache 里面的数据要被“替换”的时候,我们才把数据写入到主内存里面去。
结合流程图,逻辑比较清晰。重点解释一下其中的“脏”的概念。
标记“脏”:就是指这个时候,CPU Cache 里面的这个 Block 的数据,和主内存是不一致的。
以上2中写法都需要考虑一个问题,就是在多线程/多CPU时缓存一致性的问题。
39 | MESI协议解决缓存一致性
缓存一致性问题
假设有2核CPU,执行改价格任务,如果核1改了价格,写入到 Cache 中,在 Cache Block 交换出去前不会写入到内存,那么核2在这期间取到的数据就不一致。
为了解决此,需要做到:
- 写传播:Cache 的更新必须同步到其他 CPU 核的 Cache里。
- 事务的串行化:按顺序执行修改,防止不同 CPU 核之间乱序。
总线嗅探机制和 MESI 协议
总线嗅探(Bus Snooping):
把所有的读写请求都通过总线(Bus)
广播
给所有的 CPU 核心,然后让各个核心去嗅探
这些请求,再根据本地的情况进行响应。
写失效(Write Invalidate)协议:只有一个 CPU 核心负责写入数据,其他的核心,只是同步读取到这个写入。
写广播(Write Broadcast)协议:一个写入请求广播到所有的 CPU 核心,同时更新各个核心里的 Cache,大家一起更新。
MESI 协议对 Cache Line 有 4 种标记:
M
:代表已修改(Modified);E
:代表独占(Exclesive);S
:代表共享(Shared);I
:代表已失效(Invalidated)。
M 和 I 都代表 Cache 和主内存数据不一致,即“脏”数据。E 和 S 都是一致的,但他们有区别:
- E 代表仅当前 CPU 的 Cache 里加载了这块数据,则可以自由写入;
- S 代表有其他 CPU 也把同一块 Cache Block 从内存加载到其 Cache中,这时候写入就需要向所有 CPU 核广播请求(RFO,Request For Ownership)。