进程切换之所以比线程切换昂贵了很多,主要原因有两个:
- 需要切换页表;
- 缓存失效导致切换后命中率降低。
1. 切换页表
在 CPU 中有控制寄存器(如 x86 的 CR3)存放当前进程的页表基地址。
- 进程切换时:因为进程之间不共享虚拟地址空间,所以需要修改寄存器值;
- 线程切换时:因为线程之间共享虚拟地址空间,所以这个寄存器不需要修改。
但实际上,修改寄存器值本身非常快,真正带来性能损失的,是切换后带来的连锁反应。
2. 缓存失效
2.1 TLB(页表缓存)失效
TLB 的作用
CPU 访问内存使用的是虚拟地址,这个虚拟地址需要被翻译成物理地址。物理地址映射关系通常被保存在 4 级页表中,这意味着:读取一个数据,可能需要先进行四次额外的内存访问来查找地址,是巨大的性能损耗。
但有 TLB 会把 CPU 最近翻译过的虚拟地址及其物理地址保存下来,如果 CPU 在短期内再次访问同样的内存地址,就可以直接命中。由于 TLB 是高速缓存,这个地址转换过程几乎没有性能损耗。
失效的代价
当进程切换时,CPU 硬件通常会强制清空 TLB(注:虽然现代 CPU 支持 PCID/ASID 等技术来减少完全冲刷,但上下文切换带来的 TLB 失效依然是主要开销来源)。这会导致新进程开始运行的瞬间,TLB 中几乎没有可用的缓存项,CPU 的每次内存访问,都需要去慢速内存中查页表,这时候是一个 CPU 性能的低谷期。
但如果是线程切换,就完全不会有这种烦恼,因为共享虚拟地址空间,TLB 不会被清空。
2.2 三级缓存 (L1/L2/L3 Cache) 失效
三级缓存的作用
三级缓存是为了解决 CPU 和内存之间的速度差而出现的产物,由 SRAM 组成,比内存的 DRAM 快很多,也贵很多。 缓存的大小从 L1 到 L3 逐渐变大,速度也逐渐变慢。打个比方:
如果 CPU 从 L1 Cache 读取数据需要花 1 秒,那么在 L2 Cache 读取可能要花 3 秒,在 L3 读取可能要花 10 秒,到内存 (RAM) 读取要花 2 分钟。
CPU 内有控制电路负责三级缓存的更新,目的是尽量提高缓存命中的概率(通过提前把 CPU 可能需要的数据加载到三级缓存中),让 CPU 可以尽可能地保持高速运行。
Cache Miss 的开销
进程切换后,CPU 的 L1/L2/L3 Cache 里存的数据对新进程而言就失效了(或不再适用),所以切换后的一段时间内,会产生大量的 Cache Miss。和 TLB 一样,也是不得不从慢速内存中读取数据,这进一步加重了 CPU 性能的低谷。
同样地,因为线程共享虚拟地址空间(包括代码段、堆内存和全局变量等),线程切换后的命中率会比进程切换后的命中率高很多。
总结
造成进程切换代价明显大于线程切换的根本原因,是进程切换导致的缓存失效。
CPU 的高速运行非常依赖于缓存的有效性,否则就只能到慢速的内存中读取数据,这会在很大程度上拖累 CPU 的运行速度。从这个角度看,更根本的矛盾可能是:CPU 太快了,而内存太慢了!