目录
硬件结构
CPU
存储器
层次结构&&关系
离 CPU 越近越快;每个存储器只与相邻一层存储器打交道;速度越快成本越高。
寄存器:
- 通常在几十 ~ 几百个之间
- 每个寄存器一般存储 4Byte(32 位) 或者 8Byte(64 位) 数据
- 一般半个时钟周期(1 / 主频)内读写完成
CPU Cache:使用静态随机存储器(SARM Static Random-Access Memory)
- L1 高速缓存:2 ~ 4 个时钟周期;几十 KB ~ 几百 KB
- L2 高速缓存:10 ~ 20 个时钟周期;几百 KB ~ 几 MB
- L3 高速缓存:20 ~ 60 个时钟周期;几 MB ~ 几十 MB
内存:使用动态随机存储器(DRAM Dynamic Random Access Memory)
- 200 ~ 300 个时钟周期
SSD/HDD 硬盘:
- SSD(固态硬盘):比内存慢 10 ~ 1000 倍
- HDD(机械硬盘):比内存慢 10W 倍
内存映射到 Cache 的方案
直接映射:
优点:
- 硬件简单,成本低,地址计算速度快
- 不涉及替换算法
缺点:
- 不灵活,每个内存块只有固定位置
- Cache 空间得不到充分利用
- 容易发生冲突,Cache 效率降低
- 只适合大容量 Cache
- 如 0 和 16 都映射到第 0 块,其实其他块空闲也不能占用,这两块会来回替换,降级命中率
全相联映射:
优点:
- 灵活,可以映射到任意块
- Cache 利用率高
- 冲突率低
缺点:
- Cache 比较电路设计和实现比较困难
- 只适合小容量 Cache
组相联映射: 为前两种的折中方法,组间使用直接映射,组内使用全相联映射
- 内存一个组内块数与 Cache 分组数相同;存放那一组固定,组内哪一块灵活
- 内存第 0 块和第 8 块都直接映射到 Cache 第 0 组(直接映射)
- 组内第 0 块和第 1 块都能被映射(全相联映射)
- Cache 组内有多少块称为多少路
CPU Cache 读取过程&&数据结构
读取过程:
- 先从 L1 缓存读取,不存在则往 L2 缓存找;依次往下一级找(远离 CPU)
- 从内存读数据,并非按单个变量读取,每次都连续读一小块(Cache Line,一般为 64 字节);如 int 数组,一次读取 16 个元素,下次访问直接从 Cache Line 读取,大大提高 CPU 读数据性能
数据结构:
- ⭐从内存中读取的这一块数据,称为内存块(Block);读取时,需要拿到数据所在内存块的地址
- ⭐先会判断 Cache 中是否存在需要的数据,并且一般只会读取一个片段(字 word);对于直接映射,内存地址由 组标记 + 索引 + 偏移量 组成
- ⭐索引:用于找到指定 Cache Line,判断有效位,无效则去内存取
- ⭐组标记:用于判断 Cache Line 存储的,是否为所需的内存块
- ⭐偏移量:用于得到指定的数据片段(字 word)
如何优化语句,让 CPU 跑得更快?
CPU 跑得快,其实就是读写快,那就得提高缓存命中率
1. 提高数据缓存命中率:使 CPU 读到的数据在内存中是连续分布的,即按照数据在内存中的布局顺序访问。
- 如:遍历数组,尽量按照内存布局顺序访问
int a[5][5] cache line0: a[0][0] a[0][1] a[0][2] a[0][3] a[0][4] ..... cache line1: a[3][1] a[3][2] a[3][3] a[3][4] a[4][0] a[4][1] ...... // 顺序读取 // 对于前 16 个元素(第一行),只需要读内存一次 // 第二行,需要再读内存一次 // 共 2 次 for(int i = 0; i <= 4; i++){ for(int j = 0; j <= 4; j++){ a[i][j] } } // 跳跃读取 // 第一行,读一次内存 // 当 j == 4 时,数据位于第二行,又需要读取一次内存 // 后续还存在读取内存的情况,并且数组越大,越容易出现这种情况 for(int i = 0; i <= 4; i++){ for(int j = 0; j <= 4; j++){ a[j][i] } }
2. 提高指令缓存命中率:有规律的条件分支语句能够让 CPU 的分支预测器发挥作用(提前将指令放入缓存)
- 如:对数组元素判断之后进行操作,可以先排序,这样可以充分发挥指令缓存的作用
3. 提高多核 CPU 缓存命中率:将线程绑定到某一个 CPU 核心,避免不同核切换可能降低命中率
CPU 缓存一致性
Cache 数据写入
写直达(Writer Through)
- 数据同时写入到 cache(数据在 cache 中)和内存。
- 每次都写入内存,影响性能。
写回(Writer Back): 只有当前行需要被换出时,才写回内存;命中率高时,性能更好。
- 假如缓存命中(数据在该 Cache Line 中),则直接更新到 cache,并将该 Cache Line 标记为脏。
- 假如缓存未命中(该 Cache Line 存放着其他内存地址的数据);若 Cache Line 为脏数据,则将其写回内存;从内存读出该数据到 Cache Line,将数据写入(未标记为脏则直接写入),并标记为脏。
- 减少数据写回内存频率,提高性能。
保证缓存一致性?
写传播(Write Propagation):某个 CPU 核心里的 Cache 数据更新,必须要传播到其他 CPU 核心的 Cache
事务的串行化(Transaction Serialization):一个或多个 CPU 核心对 Cache 统一数据的修改,在其他核心看起来的顺序必须要与实际顺序一致
- CPU 核心对 Cache 数据的操作,同步给其他核心
- 对 Cache 数据的更新,需要拿到锁
总线嗅探
- CPU 每时每刻都监听总线上的广播事件
- 不管其他核心上是否缓存相同数据,更新时,都发送广播
- 只保证更新被其他核心知道,不保证事务的串行化
MESI 协议
特点:
- 基于总线嗅探机制实现了事务的串行化
- 用状态机机制降低了总线带宽压力
- 做到了缓存一致性
Cache Line 四种状态:
- Modified:已修改
- Exclusive:独占
- Shared:共享
- Invalidated:已失效
过程:
- 核心 A 读取数据到 Cache Line,此时其他核心未读取该数据,状态为 E(独占)。
- 核心 B 也读取该数据到 Cache Line,会通知其他核心,由于核心 A 也读取了该数据,所以状态都变为 S(共享)。
- 核心 A 要修改该数据,向其他核心广播一个请求,要求它们将对应的 Cache Line 修改为 I(无效);然后更新自己的 Cache Line,并标记为 M(已修改)。
- 核心 A 若还要继续修改该数据,因为已经是 M(已修改),则无需向其他核心发送信息,直接修改。
- 若核心 A Cache Line 需要被替换 || 其他核心读取或者修改该数据,都会使核心 A 该数据写回内存。
- [独占] 和 [共享],缓存和内存数据一致
- [已修改] 和 [独占],更新不需要通知其他核心,缓解总线压力
伪共享
过程:
- 普通变量 a,b 在内存中相邻。
- 核心 A 需要使用 a,核心 B 需要使用 b
- 由于从内存中读取数据是连续的;核心 A 读取 a 时,会将 b 也读入;核心 B 读取 b 时,会将 a 也读入;两核心读入相同 Cache Line。
- 当核心 A 修改 a 时,会使核心 B Cache Line 失效。
- 核心 B 此时修改 b,需要核心 A 将数据写入内存,然后核心 B 再读取到 Cache Line,最后修改 Cache Line。
- 只要核心 A B 有修改操作,则缓存相当于已经失效。
解决方法:空间换时间
- 大小字节对齐(另外的变量放到新 Cache Line)
- 字节填充(使用不会被读写的变量,填充 Cache Line 空位)
总结:
- 伪共享(False Share):就是多个线程同时读写同一 Cache Line 不同变量,导致缓存失效。
- 多线程热点数据,应该避免在同一个 Cache Line。
中断
什么是中断?
在计算机中,中断是系统用来响应硬件设备请求的一种机制,操作系统收到硬件的中断请求,会打断正在执行的进程,然后调用内核中的中断处理程序来响应请求;中断是异步处理机制,能提高系统并发;但是需要尽快完成,避免影响正常程序运行
- 如:我正在看书,妈妈打电话叫我开门,我需要先停止看书,去开门;我不需要一直询问妈妈是否回来了,只需要等电话通知
什么是软中断?
中断丢失:同一时刻系统中只能响应一个中断,其他中断可能会因此关闭
Linux 为了解决中断处理程序执行过长,将中断分为上下两部分:
- 上半部:对应硬中断;用来快速处理中断,一般暂时关闭中断,主要处理与硬件相关或事件敏感的事情
- 下半部:对应软中断;由内核(每个 CPU 核心对应一个内核线程)触发中断,处理上半部未完成的工作,将耗时的工作都交给软中断处理
Linux 中软中断事件:网络收发、定时、调度、RCU 锁
数字的二进制
为什么负数要用补码表示?
取补码步骤: 除符号位全部取反;最后 + 1
- 若直接将符号位置为 1 来表示负数,则 -2 + 1 结果会变成 -3
- 非补码直接相加不行,需要将加法变成减法,增加运算步骤,影响性能
- 采取补码,可以直接相加,且结果正确
十进制小数如何转二进制?
- 整数部分:除 2 取余
- 小数部分:乘 2 取整
计算机如何存小数?
- 符号位:表示数字是正数还是负数,为 0 表示正数,为 1 表示负数
- 指数位:指定了小数点在数据中的位置,指数可以是负数,也可以是正数,指数位的长度越长则数值 的表达范围就越大;需要 +127(偏移量)再转成二进制
- 尾数位:小数点右侧的数字,也就是小数部分,比如二进制 1.0011 x 2^(-2),尾数部分就是 0011, 而且尾数的长度决定了这个数的精度,因此如果要表示精度更高的小数,则就要提高尾数位的长度
0.1 + 0.2 == 0.3 吗?
不相等;0.1 与 0.2 二进制表示,是无限循环的,无法精确表示,采取近似值表示;两数相加必然不相等。