0. 简介
上篇博客我们介绍了如何写出让CPU跑得更快的代码,实际上就是针对CPU Cache的读取操作进行分析,以求写出缓存命中率高的代码。而本篇,我们将介绍一下CPU Cache的写入以及其带来的缓存一致性问题。
1. CPU Cache的写入
正如前文所述,CPU Cache的读操作很简单,从高速缓存中查找所需内容,若命中,则读取;若不命中,则从下一级存储中读取,依此类推。但是写的操作就要复杂一些了。
1.1 直写(Write Through)
直写的逻辑很简单,写入前会判断数据是否已经在CPU Cache中:
- 如果数据在Cache中,那么先将数据更新到Cache,再写入到内存中;
- 如果数据不在Cache中,那么直接写入内存;
注意:以上说法忽略了缓存的层级。
直写的方式很直观,也很简单,但是问题很明显,每次写操作都会写到内存,都会引起总线流量,这无疑会影响到性能。
1.2 写回(Write Back)
在写回机制中,当发生写操作时,新的数据仅仅被写入到Cache Block中,只有当修改过的Cache Block被替换时,才会被写到内存中,减少了数据写回内存的频率,这样可以提高系统的性能。
不过以上逻辑需要高速缓存Cache Line维护一个额外的脏标记位(dirty bit)来标记这个高速缓存块是否被修改过。
注:图片来自于小林coding:2.4 CPU 缓存一致性,侵权必删。
发生写操作时:
- 当数据已经在CPU Cache中,我们只需要将数据更新到CPU Cache,并且标记这个CPU Block为脏即可,不需要立即将此数据写回内存;
- 当数据不在CPU Cache中,说明对应的Cache Block是存储的“别的内存地址的数据”,那么需要检查这个Cache Block是否被标记为脏:
- 如果是脏的,那么将这个Cache Block中的数据写回到内存中,并且要先从内存中将对应需要写入的数据读入到Cache Block中,然后再把当前要写入的数据写入到Cache Block中,最后也将其标记为脏的;
- 如果不是脏的,那么直接将对应需要写入的数据读入到Cache Block中,然后再把当前要写入的数据写入到Cache Block中,最后也将其标记为脏的就好了。
以上写入未命中缓存时将写入数据先加载到缓存,再进行写入操作被称为写分配(Write Allocation),至于需要写分配的原因,我觉得主要原因是根据局部性原理,代码下一次写入的概率极有可能也在此写入地址附近,所以载入内存到缓存能提高下次写的缓存命中率。
2. 缓存一致性
在多核CPU模式下,对缓存数据的写入还可能带来缓存一致性的问题。譬如核心A和B都同时运行两个线程,都操作相同的变量(全局变量),那么必然会带来一致性的问题,为了保证一致性,需要保证做到以下两点:
- 某个CPU核心里的Cache数据更新时,必须要传播到其他核心的Cache,这个称为写传播(Write Propagation);
- 某个CPU核心对数据的操作顺序,必须在其他核心看起来顺序是一致的,这个称为事务串行化(Transaction Serialization)。
要想实现以上两点,需要通过一定的机制去保证。
2.1 总线嗅探
写传播的原则就是当某个CPU核心更新了Cache中的数据,就要把该事件广播到其他核心,最常见的实现方式就是总线嗅探(Bus Snooping)。
总线嗅探实现很简单,CPU需要时刻监听总线上的一切活动,不管别的核心有没有相同的数据,都将发送这个广播事件,无疑会增加总线的负载。另外,它也无法保证事务串行化。
于是,基于总线嗅探机制,使用状态机降低总线带宽压力,并且能够实现事务串行化的MESI协议就应运而生,这一协议做到了CPU的缓存一致性。
2.2 MESI协议
MESI协议的详情大家可以参考MESI 协议,讲的很详细,其最大的优点就是通过一些状态位的设置,避免了每次都需要更新总线,提升性能。
Q:既然有了MESI协议,为什么还会有线程同步问题?
A:因为MESI只能保证缓存间的数据一致性,而CPU存在异步操作以及CPU的流水线操作都可能会导致一些数据一致性问题,这就不是MESI所能保证的了。再深入点,即MESI解决的是数据一致性问题,并不能解决指令顺序一致性的问题。