CPU
文章目录
一:认识 CPU
中央处理单元(Central Processing Unit,CPU)也叫中央处理器或主处理器,是整个计算机的核心,也是整台计算机中造价最昂贵的部件之一。
从硬件的角度: CPU 由超大规模的晶体管组成;
从功能的角度: CPU 内部由时钟、寄存器、控制器和运算器 4 大部分组成。
- 时钟(Clock): 负责发出时钟信号,也可以位于 CPU 外部;
- 寄存器(Register): 负责暂存指令或数据,位于存储器系统金字塔的顶端。使用寄存器能够弥补 CPU 和内存的速度差,减少 CPU 的访存次数,提高 CPU 的吞吐量;
- 控制器(Control Unit): 负责控制程序指令执行,包括从主内存读取指令和数据发送到寄存器,再将运算器计算后的结果写回主内存;
- 运算器(Arithmetic Logic Unit,ALU): 负责执行控制器取出的指令,包括算术运算和逻辑运算。
1:通用处理器和专用处理器
在早期的计算机系统中,只有 1 个通用处理器,使用 1 个处理器就能够完成所有计算任务。
后来人们发现可以把一些计算任务分离出来,单独设计专门的芯片微架构,在执行效率上会远远高于通用处理器,最典型的专用处理器就是 GPU 图形处理器
。
这种用来专门处理某种计算任务的处理器就是专用处理器,那为什么专用处理器在处理某些特定问题时更快呢:
- 最优架构: 专用处理器只处理少量类型的工作,可以为特定工作设计最优芯片架构,而通用处理器只能设计全局最优架构,但不一定是执行特定工作的最优机构;
- 硬件加速: 可以把多条指令的计算工作直接用硬件实现,相比于 CPU 一条条地执行指令,能够节省大量指令周期;
- 成本更低: 专用处理器执行的计算流程是固定的,不需要 CPU 的流水线控制、乱序执行等功能,实现相同计算性能的造价更低。
现代计算机架构都是 1 个通用处理器加上多个专用处理器
这种将不同类型的计算任务采用不同的计算单元完成的设计,也叫 异构计算(Heterogeneous Computing)。
二:指令集架构 ISA
1:什么是指令集架构
CPU 所能理解的机器语言就是 指令(Instruction Code), 一个 CPU 所能理解的所有指令就是指令集
(Instruction Set)。
为了保证芯片间的兼容性,芯片厂商并不为每款新芯片设计一个新的指令集,而是将指令集推广为标准规范,这个规范就是指令集架构ISA
相对于指令集架构,CPU 在实现具体指令集功能的硬件电路设计就是微架构(Micro Architecture)。
如果用软件的思考方式,ISA 就是 CPU 的功能接口,定义了 CPU 的标准规范,而微架构就是 CPU 的功能实现,定义了 CPU 的具体电路设计,一种指令集可以兼容不同的微架构。
三:两种主流的指令集架构
因为 CPU 位于整个计算机系统最底层且最核心的部件,如果 CPU 的兼容性都出问题了,那么以前开发的应用软件甚至操作系统将无法在新的 CPU 上运行,这对芯片厂商的生态破坏是致命的。
因此,指令集架构是相对稳定的,芯片厂商在 ISA 中添加或删除指令时会非常谨慎。
目前,能够有效占领市场份额的只有 2 个 ISA ,它们也分别代表了复杂与精简 2 个发展方向:
- x86 架构: Intel 公司在 1970 年代推出的复杂指令集架构;
- ARM 架构: ARM 公司在 1980 年代推出的精简指令集架构,我们熟悉的 Apple M1 芯片、华为麒麟芯片和高通骁龙芯片都是 ARM 架构(其实,ARM 公司并不生产芯片,而是以技术授权的模式运行)。
1:复杂指令集和精简指令集
在 CPU 指令集的发展过程中,形成了 2 种指令集类型:
- 复杂指令集: 强调单个指令可以同时执行多个基本操作,用少量指令就可以完成大量工作,执行效率更高;
- 精简指令集: 强调单个指令只能执行一个或少数基础操作,指令之间没有重复或冗余的功能,完成相同工作需要使用更多指令。
在早期的计算机系统中,指令集普遍很简单,也没有复杂和精简之分。
随着应用软件的功能越来越丰富,应用层也在反向推动芯片架构师推出更强大的指令集,以简化程序编写和提高性能。
例如,一些面向音视频的指令可以在一条指令内同时完成多个数据进行编解码。
这在当时的确是不错的选择。 原因是 CPU 和主存的速度差实在太大了,用更少的指令实现程序功能(指令密度更高)可以减少访存次数。
凭借这一点,复杂指令集对精简指令集的优势是几乎全面性的:
- 优势 1: 可以减少程序占用的内存和磁盘空间大小;
- 优势 2: 可以减少从内存或磁盘获取指令所需要的带宽,能够提高总线系统的传输效率;
- 优势 3: CPU L1 Cache 可以容纳更多指令,可以提高缓存命中率。且现代计算机中多个线程会共享 L1 Cache,指令越少对缓存命中率越有利;
- 优势 4: CPU L2 Cache 可以容纳更多数据,对操作大量数据的程序也有利于提高缓存命中率。
然而,这些优势都是有代价的:
- 缺点 1 - 处理器设计复杂化
- 缺点 2 - 指令功能重叠
- 缺点 3 - 指令长度不统一
因此,到 1980 年代,精简指令集 RISC 逐渐浮出水面。
目前,大多数低端和移动系统都采用 RISC 架构,例如 Android 系统、Mac 系统和微软 Surface 系列。
相比于复杂指令集,精简指令集更加强调 “正交性” ,单个指令只能执行一个或少数基础操作,指令之间没有重复或冗余的功能。
而且精简指令集的每条 指令长度相同 ,非常便于实现流水线式结构。
总结一下:
- 复杂指令集凭借更高的指令密度,在性能方面整体优于精简指令集(内存 / 磁盘占用、CPU Cache 命中率、TLB 未命中率)
- 精简指令集牺牲了指令密度换取更简单的处理器架构,以性能换取功耗的平衡。
指令集类型 | CISC | RISC |
---|---|---|
指令数量 | 指令数量庞大 | 指令数量相对较少 |
指令长度 | 长度不同 | 长度相同 |
指令功能 | 有重叠 | 正交 |
举例 | x86 | ARM、MIPS |
四:CPU 的性能指标
五:影响 CPU 性能的因素
CPU 作为计算机的核心部件,未来一定是朝着更强大的性能出发。在看待 CPU 的视角上,我们也要具备一定的全局观:
- 提升 CPU 性能不止是 CPU 的任务: 计算机系统是多个部件组成的复杂系统,脱离整体谈局部没有意义;
- 平衡性能与功耗: 一般来说,CPU 的计算性能越高,功耗也越大。我们需要综合考虑性能和功耗的关系,脱离功耗谈性能没有意义。
1:提升CPU主频
提升主频对 CPU 性能的影响是最直接的,过去几十年 CPU 的主要发展方向也是在怎么提升 CPU 主频的问题上。
不过,最近几年 CPU 主频的速度似乎遇到瓶颈了。因为想要主频越快,要么让 CPU 满频或超频运行
,要么升级芯片制程
,在单位体积里塞进去更多晶体管。
这两种方式都会提升 CPU 功耗,带来续航和散热问题。如果不解决这两个问题,就无法突破主频瓶颈
2:多核并行执行
既然单核 CPU 的性能遇到瓶颈,那么在 CPU 芯片里同时塞进去 2 核、4 核甚至更多,那么整个 CPU 芯片的性能不就直接翻倍提升吗?
理想很美好,现实是性能并不总是随着核心数线性增加。
在核心数较小时,增加并行度得到的加速效果近似于线性提升,但增加到一定程度后会趋于一个极限,说明增加并行度的提升效果也是有瓶颈的。
因为不管程序并行度有多高,最终都会有一个结果汇总的任务,而汇总任务无法并行执行,只能串行执行。
例如,我们用 Java 的 Fork/Join 框架将一个大任务分解为多个子任务并行执行,最终还是需要串行地合并子任务的结果。
这个结论也有一个经验定律 —— 阿姆达尔定律(Amdahl’s Law) ,它解释了处理器并行计算后效率提升情况
以绿色的曲线为例,程序可以的并行分量是 95%,串行分量是 5%,最终得出的提升极限就会 20 倍
- 并行后的执行时间是
W p p + W s \frac{W_p}{p} + W_s pWp+Ws
- 并行后的加速倍数是
W s + W p W s + W p p \frac{W_s+W_p}{W_s+\frac{W_p}{p}} Ws+pWpWs+Wp
- 当并行度 p 趋向于无穷大时,提升极限就是
W s + W p W s \frac{W_s+W_p}{W_s} WsWs+Wp
3:指令重排序
增加核心数是提升并行度最直接的方法,但并不是唯一的方法。
现代 CPU 为了提高并行度,会在遵守单线程数据依赖性原则的前提下,对程序指令做一定的重排序。事实上不止是 CPU,从源码到指令执行一共有 3 种级别重排序:
- 编译器重排序: 例如将循环内重复调用的操作提前到循环外执行;
- 处理器系统重排序: 例如指令并行技术将多条指令重叠执行,或者
使用分支预测技术提前执行分支的指令,并把计算结果放到重排列缓冲区(Reorder Buffer)的硬件缓存中,当程序真的进入分支后直接使用缓存中的结算结果
; - 存储器系统重排序: 例如写缓冲区和失效队列机制,即是可见性问题,从内存的角度也是指令重排问题。
六:认识 CPU 高速缓存
1:存储器的金字塔结构
现代计算机系统为了寻求容量、速度和价格最大的性价比会采用分层架构,从 “CPU 寄存器 - CPU 高速缓存 - 内存 - 硬盘”自上而下容量逐渐增大,速度逐渐减慢,单位价格也逐渐降低。
- 1、CPU 寄存器: 存储 CPU 正在使用的数据或指令;
- 2、CPU 高速缓存: 存储 CPU 近期要用到的数据和指令;
- 3、内存: 存储正在运行或者将要运行的程序和数据;
- 4、硬盘: 存储暂时不使用或者不能直接使用的程序和数据。
2:为什么在 CPU 和内存之间增加高速缓存
- 原因 1 -
弥补 CPU 和内存的速度差(主要)
:- 由于 CPU 和内存的速度差距太大,为了拉平两者的速度差,现代计算机会在两者之间插入一块速度比内存更快的高速缓存。
- 只要将近期 CPU 要用的信息调入缓存,CPU 便可以直接从缓存中获取信息,从而提高访问速度;
- 原因 2 -
减少 CPU 与 I/O 设备争抢访存
:- 由于 CPU 和 I/O 设备会竞争同一条内存总线,有可能出现 CPU 等待 I/O 设备访存的情况。
- 而如果 CPU 能直接从缓存中获取数据,就可以减少竞争,提高 CPU 的效率。
3:CPU 的三级缓存结构
在 CPU Cache 的概念刚出现时,CPU 和内存之间只有一个缓存
随着芯片集成密度的提高,现代的 CPU Cache 已经普遍采用 L1/L2/L3 多级缓存的结构来改善性能。
自顶向下容量逐渐增大,访问速度也逐渐降低。当缓存未命中时,缓存系统会向更底层的层次搜索。
- L1 Cache: 在 CPU 核心内部,分为指令缓存和数据缓存,分开存放 CPU 使用的指令和数据;
- L2 Cache: 在 CPU 核心内部,尺寸比 L1 更大;
- L3 Cache: 在 CPU 核心外部,所有 CPU 核心共享同一个 L3 缓存。
七:理解 CPU 三级缓存的设计思想
1:为什么L1要将指令缓存和数据缓存分开
这个策略叫分离缓存,与之相对应的叫统一缓存:
- 分离缓存:指令和数据分别存放在不同缓存中:
- 指令缓存(Instruction Cache,I-Cache)
- 数据缓存(Data Cache,D-Cache)
- 统一缓存: 指令和数据统一存放在一个缓存中。
那么,为什么 L1 缓存要把指令和数据分开呢?
- 原因 1 -
避免取指令单元和取数据单元争夺访缓存
(主要): 在 CPU 内核中,取指令和取数据指令是由两个不同的单元完成的。如果使用统一缓存,当 CPU 使用超前控制或流水线控制(并行执行)的控制方式时,会存在取指令操作和取数据操作同时争用同一个缓存的情况,降低 CPU 运行效率;
- 原因 2 -
内存中数据和指令是相对聚簇的,分离缓存能提高命中率
: 在现代计算机系统中,内存中的指令和数据并不是随机分布的,而是相对聚集地分开存储的。因此,CPU Cache 中也采用分离缓存的策略更符合内存数据的现状;
2:为什么 L1 采用分离缓存而 L2 采用统一缓存
- 原因 1:
L1 采用分离缓存后已经解决了取指令单元和取数据单元的争夺访缓存问题
,所以 L2 是否使用分离缓存没有影响; - 原因 2:
当缓存容量较大时,分离缓存无法动态调节分离比例,不能最大化发挥缓存容量的利用率
。- 例如数据缓存满了,但是指令缓存还有空闲,而 L2 使用统一缓存则能够保证最大化利用缓存空间。
3:L3 缓存是多核心共享的,放在芯片外有区别吗
集成在芯片内部的缓存称为片内缓存,集成在芯片外部(主板)的缓存称为片外缓存。
最初,由于受到芯片集成工艺的限制,片内缓存不可能很大,因此 L2 / L3 缓存都是设计在主板上,而不是在芯片内的。
后来,L2 / L3 才逐渐集成到 CPU 芯片内部后的。片内缓冲和片外缓存是有区别的,主要体现在 2 个方面:
- 区别 1 - 片内缓存物理距离更短: 片内缓存与取指令单元和取数据单元的物理距离更短,速度更快;
- 区别 2 - 片内缓存不占用系统总线: 片内缓存使用独立的 CPU 片内总线,可以减轻系统总线的负担。
八:Cache Line
CPU Cache 在读取内存数据时,每次不会只读一个字或一个字节,而是一块块地读取,这每一小块数据也叫 CPU 缓存行(CPU Cache Line)。
这也是对局部性原理的应用,当一个指令或数据被访问过之后,与它相邻地址的数据有很大概率也会被访问,将更多可能被访问的数据存入缓存,可以提高缓存命中率。
当然,块长也不是越大越好(一般是取 4 到 8 个字长,如 64 字节):
- 前期:当块长由小到大增长时,随着局部性原理的影响使得命中率逐渐提高;
- 后期:但随着块长继续增大,导致缓存中承载的块个数减少,很可能内存块刚刚装入缓存就被新的内存块覆盖,命中率反而下降。
- 而且,块长越长,追加的部分距离被访问的字越远,近期被访问的可能性也更低,无济于事。
区分几种容量单位:
- 字节(Byte): 字节是计算机数据存储的基本单位,即使存储 1 个位也需要按 1 个字节存储;
- 字(Word): 字长是 CPU 在单位时间内能够同时处理的二进制数据位数。多少位 CPU 就是指 CPU 的字长是多少位(比如 64 位 CPU 的字长就是 64 位);
- 块(Block): 块是 CPU Cache 管理数据的基本单位,也叫 CPU 缓存行;
- 段(Segmentation)/ 页(Page): 段 / 页是操作系统管理虚拟内存的基本单位。
事实上,CPU 在访问内存数据的时候,与计算机中对于 “缓存设计” 的一般性规律是相同的:
- 对于基于 Cache 的系统,对数据的读取和写入总会先访问 Cache,检查要访问的数据是否在 Cache 中。
- 如果命中则直接使用 Cache 上的数据,否则先将底层的数据源加载到 Cache 中,再从 Cache 读取数据。
九:内存地址与 Cache 地址的映射
CPU 怎么知道要访问的内存数据是否在 CPU Cache 中,在 CPU 中的哪个位置,以及是不是有效的呢?
无论对 Cache 数据检查、读取还是写入,CPU 都需要知道访问的内存数据对应于 Cache 上的哪个位置,这就是内存地址与 Cache 地址的映射问题。
事实上,因为内存块和缓存块的大小是相同的,所以在映射的过程中,我们只需要考虑 “内存块索引 <-> 缓存块索引” 之间的映射关系,而具体访问的是块内的哪一个字,则使用相同的偏移在块中寻找。
举个例子:假设内存有 32 个内存块,CPU Cache 有 8 个缓存块,我们只需要考虑 紫色 部分标识的索引如何匹配即可。
目前,主要有 3 种映射方案:
- 直接映射(Direct Mapped Cache): 固定的映射关系;
- 全相联映射(Fully Associative Cache): 灵活的映射关系;
- 组相联映射(N-way Set Associative Cache): 前两种方案的折中方法。
1:直接映射
直接映射是三种方式中最简单的映射方式,直接映射的策略是: 在内存块和缓存块之间建立起固定的映射关系,一个内存块总是映射到同一个缓存块上。
具体方式:
- 将内存块索引对 Cache 块个数取模,得到固定的映射位置。例如 13 号内存块映射的位置就是 13 % 8 = 5,对应 5 号 Cache 块;
- 由于取模后多个内存块会映射到同一个缓存块上,产生块冲突,所以需要在 Cache 块上增加一个 组标记(TAG),标记当前缓存块存储的是哪一个内存块的数据。其实,组标记就是内存块索引的高位,而 Cache 块索引就是内存块索引的低 4 位(8 个字块需要 4 位);
- 由于初始状态 Cache 块中的数据是空的,也是无效的。为了标识 Cache 块中的数据是否已经从内存中读取,需要在 Cache 块上增加一个 有效位(Valid bit) 。如果有效位为 0,则 CPU 可以直接读取 Cache 块上的内容,否则需要先从内存读取内存块填入 Cache 块,再将有效位改为 1。
2:全相联映射
理解了直接映射的方式后,我们发现直接映射存在 2 个问题:
缓存利用不充分
: 每个内存块只能映射到固定的位置上,即使 Cache 上有空闲位置也不会使用;块冲突率高
: 直接映射会频繁出现块冲突,影响缓存命中率。
基于直接映射的缺点,全相联映射的策略是: 允许内存块映射到任何一个 Cache 块上。
这种方式能够充分利用 Cache 的空间,块冲突率也更低,但是所需要的电路结构物更复杂,成本更高
。
具体方式:
- 当 Cache 块上有空闲位置时,使用空闲位置;
- 当 Cache 被占满时则替换出一个旧的块腾出空闲位置;
- 由于一个 Cache 块会映射所有内存块,因此组标记 TAG 需要扩大到与主内存块索引相同的位数,而且映射的过程需要沿着 Cache 从头到尾匹配 Cache 块的 TAG 标记。
3:组相联映射
组相联映射是直接映射和全相联映射的折中方案
组相联映射的策略是:
- 将 Cache 分为多组,每个内存块固定映射到一个分组中,又允许映射到组内的任意 Cache 块。
- 显然,组相联的分组为 1 时就等于全相联映射,而分组等于 Cache 块个数时就等于直接映射。
十:Cache 块的替换策略
在使用直接映射的 Cache 中,由于每个主内存块都与某个 Cache 块有直接映射关系,因此不存在替换策略。
而使用全相联映射或组相联映射的 Cache 中,主内存块与 Cache 块没有固定的映射关系,当新的内存块需要加载到 Cache 中时(且 Cache 块没有空闲位置),则需要替换到 Cache 块上的数据。
此时就存在替换策略的问题。
常见替换策略:
- 随机法: 使用一个随机数生成器随机地选择要被替换的 Cache 块,实现简单,缺点是没有利用 “局部性原理”,无法提高缓存命中率;
- FIFO 先进先出法: 记录各个 Cache 块的加载事件,最早调入的块最先被替换,缺点同样是没有利用 “局部性原理”,无法提高缓存命中率;
- LRU 最近最少使用法: 记录各个 Cache 块的使用情况,最近最少使用的块最先被替换。
- 这种方法相对比较复杂,也有类似的简化方法,即
记录各个块最近一次使用时间,最久未访问的最先被替换
。 - 与前 2 种策略相比,
LRU 策略利用了 “局部性原理”
,平均缓存命中率更高。
- 这种方法相对比较复杂,也有类似的简化方法,即