CPU & 多线程
本文是阅读《大话处理器》的读书摘要
流水线
流水线存在的意义在于尽可能的让所有硬件都不空闲,从而提高整个系统的处理能力
简单的三级流水线:
- 取指,从内存中将指令加载到内核中
- 译码,将指令中有用的数据解析出来,例如跳转指令,需要知道跳转地址,如果是运算指令,则需要知道源操作数和目的操作数的位置
- 执行
MIPS 的五级流水线
- IF,取指;ID,译码;EX,执行;MEM,内存(cache)读写;WB,数据写回到通用寄存器中
流水线上的冒险
导致流水线出现停顿的因素称为冒险(Hazard)
结构冒险
以 MIPS 为例,旧的 MIPS CPU 没有片内 cache,指令和数据放在了一起,这样 IF 和 MEM 流程会发生冲突,两者之一需要等待另一个命令的完成,这样就影响了流水线的效率,最少 IF 模块或者 MEM 模块不能同时执行,违背的流水线的初衷。现在的 CPU 一般一级缓存分为指令和数据两块,不会出现这种情况
数据冒险
以两条相邻的指令为例:
add R1,R2,R3 ; R1 = R2+R3
add R4,R1,R5 ; R4 = R1+R5
按照 MIPS 的五级流水线,如果不做任何处理,流程如下
CMD | Cycle1 | Cycle2 | Cycle3 | Cycle4 | Cycle5 |
---|---|---|---|---|---|
add R1,R2,R3 | IF | ID | EX | MEM | WB |
add R4,R1,R5 | IF | ID | EX | MEM |
第二条指令的 EX 是在第一条指令 WB 之前执行的,结果是错误的。简单的解决方法是在这两条指令之间加 NOP 操作,将第二条命令的流水线后移;高效的方法是直通,系统检测到这种情况后后面的指令直接从寄存器取值而不是寄存器堆
控制冒险
控制冒险指的是指令中的跳转语句造成部分流水线操作的失效,这是 CPU 中引入分支预测与乱序执行模块的原因
分支预测
2 位预测,即在跳转指令处维护一个 2bit 的计数器,每次跳转计数器加 一,否则减一
CPU 在执行到跳转指令处时,如果计数器值为2/3,则解析跳转指令后的命令,否则解析其他跳转后的命令;如果预测失败,则部分流水线会被清空
CPU 核心有专门的分支预测模块,用于保存计数器与跳转地址等信息
乱序执行
除了减少流水线冒险而引入乱序执行,CPU 和内存之间的数据交换一般需要数百个 Cycle,cache 未命中时从内存读数据是非常耗时的,所以一旦出现这种情况,处理器不应该等待内存读写,而是去处理其他任务,数据准备好后再执行未完成的任务,这是 CPU 乱序执行的另一个原因
指令乱序执行的前提是前后指令没有相关性,比如x=a+b;y=x+c
这两条指令是相关的,但x=a+b;y=c+d
这两条指令就不相关,指令是否可以执行依赖于两个条件
- 是否有空闲的功能单元去执行这条指令
- 该执行的源操作数是否已经准备好了
并行
从指令和数据这两个维度,可以将处理器结构分为下面四类
- 单指令,单数据(SISD)
- 单指令,多数据(SIMD),比如 intel 的 SSE,可以一次处理 16 字节的数据;MMX 指令集,多媒体扩展指令
- 多指令,单数据(MISD),意义不大
- 多指令,多数据(MIMD),比如下面的超标量和 VLIW
指令并行
指令并行有两大类,超标量和 VLIW
- 超标量处理器的译码单元可以同时处理多条指令,借用后续微指令的乱序执行,可以提高系统性能;超标量的指令并行化是 CPU 内部自行完成的;使用超标量的 CPU 内部会缓存译码后的微指令,乱序执行核心会扫描这些信息,(并行)执行准备好的微指令
- VLIW,TI 的部分 DSP 处理器有 8 个执行单元,也就是说可以同时执行 8 条指令;每 8 条指令作为一个包(very long instruction word);VLIW 指令并行化信息源自源码,处理器按照源码中的信息并行处理,可控性比超标量要高很多;比如 DSP 中可以通过
||
运算符告知编译器部分指令可以并行执行
这两种实现方式,VLIW 更好一些,编译器可以使用额外的信息进行优化,x86 因为兼容性问题,才选择使用超标量
硬件多线程
软件编程领域的线程切换一般由操作系统实现,系统会保存线程的上下文
硬件多线程的切换由硬件自行完成,速度与效率要远高于软件多线程,这两者有概念上的区别,注意区分
- 处理器发现一个线程的 cache miss,则会主动切换到另一个线程去执行,中间不需要操作系统的干预
- 每个 Cycle ,CPU 会发射与执行不同线程的指令,这是超线程的基础
Cache
CPU Cache 存在的前提是时间局部性(刚访问的数据随后被访问的概率大)与空间局部性(相邻的数据均被访问的概率大于不相邻的数据)
多核 CPU 一般有三级缓存,一二级缓存是 Core 独占的,一级缓存速度最快,一级缓存一般将数据和指令分开,避免上面所说的流水线结构冒险;三级缓存是多个 Core 共享
x86 CPU 中的 Cache 一般对程序员而言是透明的,但 DSP 则不同,DSP 片内 Cache 程序可以直接使用
Cache line
Cache 被分为若干个等大小的块(32Byte、64Byte),这些块被称为 Cache line,Cache line 是 Cache 和内存交换数据的最小单元
Cache 一致性
Cache 与内存的同步有两类
- 写通(Write Through),Cache 的修改会实时同步给内存,这样效率就会比较低
- 写回(Write Back),修改 Cache 不会即时修改对应内存,只有 Cache 被移出 CPU,才会更新内存
为了保证 Cache 一致性,处理器提供了两个保证底层一致性的操作:置无效和写更新
- 置无效,多个核心共享的 Cache,一个核心修改了自己对应的 Cache,则其他核心中的 Cache 会被置无效状态
- 写更新,其他各核心同步 Cache 变动
写更新比置无效更耗时
MESI
MESI 协议使用 2 bit 来描述 Cache 的 4 中状态
- M(modified),Cache line 有效,数据被修改了,但数据只在本 Cache 中
- E(Exclusive),Cache line 有效,数据和内存中的数据一致且只存在与当前 Cache 中
- S(Shared),这行数据有效,数据和内存中的数据一致且存在多个 Cache 中
- I(Ivalid),当前 Cache line 无效
编程 Tips
- 少用数组和指针,多用简单的局部 变量
- 避免使用全局变量
- 避免 false sharing
- 尽量少共享数据
- 尽量少修改数据
- 不要频繁的修改数据
- 可以使用 restrict 关键字,告知编译器变量之间没有重叠,从而进行优化