为什么系统软件人员要深入了解cache?
在一个系统中,cache无处不在,对于一个系统编程人员来说,你无法躲藏。下图是一个经典的ARM64系统的架构图,由Corte-A72和Cortex-53组成了大小核架构,每个CPU核心都有L1 cache,每个cluster里共享一个L2 cache,另外还有Mali GPU和DMA外设。
对于系统软件人员,下面几个常常疑惑的问题:
-
cache的内部组织架构是怎么样的?能否画出一个cache的layout图?什么是set,way?
-
直接映射,全关联和组相联之间有什么区别?优缺点是啥?
-
重名问题是怎么发生的?
-
同名问题是怎么发生的?
-
VIPT会不会发生重名问题?
-
什么是inner shareability 和outer shareability?怎么区分?
-
什么是PoU?什么是PoC?
-
什么是cache一致性?业界解决cache一致性都有哪些方法?
-
MESI状态转换图,我看不懂。
-
什么cache伪共享?怎么发生的,如何避免?
-
DMA和cache为啥会有cache一致性问题?
-
网卡通过DMA收数据和发数据,应该怎么操作cache?
-
对于self-modifying code,怎么保证data cache和指令cache的一致性问题?
所以,Cache这个玩意,对我们系统编程人员来说,非常重要。Cache没有理解好,或者没有完全搞透了,对系统编程影响很大,有时候我们在编程的时候,一行代码小小的改动可能会影响整个系统的性能,所以,我是建议系统程序员有必要把cache这玩意好好系统的学一学。
笨系列文章主要源自第三季《arm64体系结构与编程》视频课程,大概会有上下两篇:
上篇:介绍cache相关的背景知识,例如什么是cache,cache的layout结构图,cache的层级,VIPT/PIPT/VIVT,cache的重名和同名问题,cache的策略等。
中篇:主要介绍ARM特有的inner share和outer share的概念,以及神马是PoU和PoC,还有cache指令的格式。为什么会有cache一致性问题?arm公司对cache一致性问题的解决方案的演进。cache一致性问题业界常用的解决方案。
下篇:主要介绍MESI协议,怎么去看MESI协议状态图,DMA和cache之间的cache一致性问题,self-modifying code导致的I-cache和D-cache的一致性问题,cache伪共享等问题。
inner share和outer share
Inner 和outer shareability是arm提出来的概念。很重要的一点,大家先要知道,也就是只有normal memory的内存属性的内存才能设置inner 和outer shareability,device memory是不能设置shareability的。
怎么去区分inner share还是outer share呢,arm手册里讲了,不同的SOC设计有不同的区分方法,不过有一个通用的规则:inner share通常是CPU IP集成的caches,包括CPU IP集成的L1 data cache和L2 cache,而outer share是通过总线连接到cache,比如外接的L3 cache等。
下面这个图,比较直观。这个图,虚线分成了两部分,左边都是inner share,右边都是outershare。左边表示是CPU IP集成的cache,上面的cores集成了L1和L2cache,而下面的core集成了L1 cache,那虚线框出来的都是inner share。我们再来看虚线左边,通过总线外接了L2 cache或者L3 cache,这边都是outer share。
在armv8.6手册里,在B2.7.1章里有一段话对inner share和outer share描述。
在这里的example B2-1里,举了一个例子,说在一个在一个2 cluster的系统:
-
每一个cluster里,数据cache和unified cache都是inner shareable
-
两个cluster之间变成了outer shareable
这个系统里,每一个cluster都是一个不同的inner share,但是整个系统是一个outer share。
在Programmer guide第11.3章里,也有一段话描述inner和outer,说的比较接地气。
PoU和PoC的区别
下面来介绍一下arm定义的两个重要的概念,一个是POC,PoC的英文全称为Point of Coherency ,一个是PoU,英文全称为Point of Unification。
神马是PoU?
PoU: 表示一个CPU中的指令cache,数据cache还有MMU,TLB等看到的是同一份的内存拷贝。
-
PoU for a PE,是说保证PE看到的I/D cache和MMU是同一份拷贝。大多数情况下,PoU是站在单核系统的角度来观察的。
-
PoU for inner share,意思是说在inner share里面的所有CPU核心都能看到相同的一份拷贝。
所以,PoU有两个观察点,一个是poc for a PE,PE就是cpu core,另外一个是POU for inner share。关于pou的描述,在armv8.6手册的第D4.4.7章里有详细的描述。
其中它里面举了一个例子,说self-modifing code在pou for inner里,使用下面两条简单的指令就能保证data cache和指令cache的一致性。如果不是pou for inner,要保证data cache和指令cache的一致性,需要额外的memory barrier的指令。
神马是PoC?
PoC:系统中所有的观察者例如DSP, GPU,CPU, DMA等都能看到同一份内存拷贝。
我们来这个图,左边那个图,就是一个单一的一个master,master可以是CPU,gpu,dma等有能力访问内存的设备。那么在单一的master系统里,这些指令cache,数据cache,TLB等都可以看成是POU,因为在这个单一的master眼里,他们都是同一份拷贝。我们来看一下右边这个图,这个图里有两个master。那么在masterA和master B眼里,他们要看到同一份拷贝的话,需要masterA和masterB进行cache一致性,这个观察角度就是POC。
PoC和PoU的区别
我们来看一下pou和poc的区别,最大的一个区别就是poc是系统一个概念,和系统配置相关,它包含了系统所有有能力访问内存的设备,包括cpu,gpu,dma等,这些都称为observer,观察者。
而pou是个局部的概念。还有一点大家需要注意,系统配置的不同可能会影响pou的范围,我们举个例子,在Cortex-A53可以配置L2 cache和没有L2 cache,可能会影响PoU的范围,为什么会这样,因为我们支持pou有一个重要的观察点,就是pou for inner share,而inner share的划分和 这个cache是不是 CPU IP集成有关。
比如下面这个图,左边,没有集成L2 cache,那么这时候POU等于了POC,而且也没有其他的master。右边那个图,CORE里集成了L2 cache,那么core,L1 cache和L2 cache构成了POU for inner,而master a和master b和系统内存构成了poc。
Cache维护指令
接下来,我们来看cache维护。前面我们讲了一大通,关于cache的背景知识,接下来介绍的cache的维护,这里说的维护是指的手工维护,也就是软件干预cache的行为。
Armv8里定义的Cache的管理的操作有三种:
-
无效(Invalidate)整个高速缓存或者某个高速缓存行。高速缓存上的数据会被丢弃。
-
清除(Clean)整个高速缓存或者某个高速缓存行。相应的高速缓存行会被标记为脏,数据会写回到下一级高速缓存中或者主存储器中。
-
清零(Zero)操作。在某些情况下,对高速缓存进行清零操作起到一个预取和加速的功效,比如当程序需要使用一大块临时内存,在初始化阶段对这个内存进行清零操作,这时高速缓存控制器会主动把这些零数据写入高速缓存行中。若程序主动使用高速缓存的清零操作,那么将大大减少系统内部总线的带宽。
对高速缓存的操作可以指定不同的范围。
-
整块高速缓存。
-
某个虚拟地址。
-
特定的高速缓存行或者组和路。
另外在ARMv8架构中最多可以支持7级的高速缓存,L1~L7高速缓存。当对一个高速缓存行进行操作时,我们需要知道高速缓存操作的范围。ARMv8架构中从处理器到所有内存的角度分成如下几个视角。
-
PoC(Point of Coherency,全局缓存一致性角度)
-
PoU(Point of Unification,处理器缓存一致性角度)
Cache指令格式
下面来讲一下cache的指令格式,armv8里提供了两条cache相关的指令,一条是指令cache的管理指令,就叫做IC,其实就是instruction cache的简称。另外一条是data cache的管理指令,叫做DCC,其实就是data cache的简称。
指令的有两部分组成,一个是operation,另外一个是xt,表示参数。Operation可以4个部分,分别是function,就是你要操作的功能是啥,我们前面讲了cache管理的三大功能,一个是invalidate,第二个是clean,第三个zero,第一个和第二个可以合起来,就是clean & invalidate,就是我先把cache flush回内存,然后我再无效它。第二部分是,类型type,VA表示要操作的地址,sw表示路和组,all表示整个cache。第三部分是point,就是我们说的管理cache的观察点,或者说范围,是POU还是POC,POU和POC的范围是不一样的,我们前面讲过。第4部分是IS,表示是否包括inner shareable。这里POU和inner share可以合在一起的。
最后一个参数,可以传递一些参数,比如你需要传递地址给这条指令。
下面这个表就是 armv8里支持的cache管理指令,我这里简单给大家翻译了。
大家可以去看armv8.6手册的D4.4.8章的D4-3这个表,这是最权威的。
为什么要cache一致性
为什么要有cache一致性?或者说cache一致性这个问题是怎么产生的。要了解这个问题,我们需要从单核cpu进化到多核处理器这个过程开始说起。以arm为例,在cortex-a8其实都是单核处理器,到了cortex-a9之后,就有了多核处理器,不过准确来讲在ARM11的时候也有多核,不过那时候的多核还不是很成熟。在多核处理器里,每个核心都有自己L1 cache,多核之间可能共享一个L2的cache等等。以这个图为例,core0有自己的L1 cache,core1有自己的L1 cahe,然后才是内存。那么当core0 先访问一个地址,然后把这个地址的数据加载到它自己的cache里,那么这时候如果core1 也想要这个数据,它应该怎么办呢?它是应该也从内存中去读,还是去问core0 要数据呢?所以,这种情况下,就产生了cache一致性问题。
cache一致性关注的是同一个数据在多个高速缓存和内存中的一致性问题,解决高速缓存一致性的方法主要是总线监听协议,例如MESI协议等。所以我们这节课,很重要一点是和大家去介绍MESI协议。
我们做系统软件的,为啥要去关注cache一致性呢?虽然刚才提到的MESI协议对软件是透明的,也就是说完全是硬件实现的,但是在有些场景下,需要软件手工来干预。下面举几个例子。
-
驱动中使用DMA(数据cache和内存不一致),这个例子很常见,特别是写驱动的同学。当你的驱动力有DMA的时候,你需要特别小心。比如设备内部一般都有FIFO,你需要把设备的FIFO数据写入到内存中的DMA buffer时候,你应该怎么操作你的cache。反过来,当你需要把内存的DMA buffer数据搬移通过DMA来搬移到设备的FIFO的时候,你该怎么处理你的cache?
-
Self-modifying code(数据cache的数据可能比指令cache新)。
-
修改了页表(TLB里保存的数据可能过时)。
ARM的cache一致性的演进路线
我们来看一下ARM的cache一致性的演变。这张图比较有意思。
-
在2006年的时候,cortex-a8处理器横空出世,不过cortex-a8是一个单核的设计,单核处理器只有一个CPU,没有多核之间的cache不一致的问题,不过会有DMA和cache的一致性问题。
-
到了cortex-a9的时候,就有了多核设计(MPcore)了。多核设计的就需要在核与核之间通过硬件来保证cache的一致性问题,通常的做法是实现MESI类似的协议。
-
到了A15的时候,就出现了大小核的架构,大小核架构,相当于有两个cluster,每个cluster里有多个核心。cluster里的多个核心,我们就需要用mesi协议来保证一致性,那cluster与cluster之间呢?那么这时候就需要一个AMBA coherency extensions这样的缓存一致性的控制器来解决这个问题了。这个就是系统级别的cache一致性问题。Arm公司在这方面做了不少工作,有现成的IP可以使用,比如CCI-400,CCI-500等。
在单核CPU里,我们刚才提到,因为系统只有一个CPU,那么他也只有一个L1 cache和L2 cache,不会有第二个CPU来访问cache,所以,单核处理器,没有cache一致性问题,大家注意,这里说的cache一致性问题指的是 多核之间的cache一致性问题,它依然有DMA和cpu之间的cache一致性问题。还有一点,大家需要注意,在单核处理器系统里,cache管理指令,它的作用范围仅仅限于单核。
我们来看一下多核处理器的情况,比如cortex-a9的MP core,这种情况,硬件上就支持了多核的cache一致性,硬件上实现了MESI指令的协议,在arm 的手册里,一般称为Snoop Control Unit的硬件单元,这个SCU的单元实现了MESI的协议。还有一点,在多核处理器系统里,cache的管理指令,它会发广播消息到其他的CPU核心里,这一点和单核处理器不一样。
下面三个图,第一个是单核处理器的情况,它只有一个cpu核心,和单一的cache,没有多核cache一致性问题。第二个图,是一个双核的处理器,每个核心内部有自己的cache,那么就需要一个硬件单元来完成这两个cache的一致性问题,通常就是我们说的SCU的硬件单元了。第三个图,要复杂一些,它由两个cluster组成,每个cluster有两个核心。我们来看一个cluster,它有一个缓冲一致性的硬件单元来保证core与core直接的一致性。那么在最下面有一个缓存一致性总线,或者缓存一致性控制器来保证这两个cluster之间的cache一致性问题。
系统cache一致性问题
我们来看一下系统级别的cache一致性问题。因为现在的arm系统越来越复杂了,从多核发展到多族,例如大小核架构等。比如下面这个图,这是一个典型的大小核架构,小核由A53来担当,大核由A72来担当,两个A53核心构成了一个cluster,这个cluster里,每个a53的cpu都有各自独立的L1 cache,然后一起共享一个L2 cache,然后通过一个ACE的硬件单元连接到缓存一致性总线里,ACE其实就是AXI coherent extensions的简称,这个是AMBA 4协议中定义的。我们再来看大核这边,它同样也是2个核心,每个核心都有一个L1 cache,也是共享一个L2 cache,同样也是通过ACE接口连接到缓存一致性总线上。那么除了CPU之外,系统还有GPU,比如arm公司的mali GPU,还有一些带有DMA的外设等,这些设备他们都有独立访问内存的能力,比如GPU还自己带有cache,那么他们也必须通过ACE接口来连接到这个缓存一致性总线上。这个缓存一致性总线,就是用来实现系统级别的cache一致性的。关于系统级别的cache一致性,我们后面还会讲到。
cache一致性的解决方案
我们来简单看一下cache一致性的解决办法,我们来讲业界常用的三种方法。
cache一致性,需要保证系统中所有的CPU,所有的bus master从,例如GPU,DMA等,他们观察到的内存是一致的。举个例子,外设都用DMA,如果你的软件通过CPU来产生了一些数据,然后想通过DMA来搬移这些数据到外设。如果CPU和DMA看到的数据不一致,比如说CPU产生的数据还在cache里,而DMA却从内存中直接去搬移数据,那么DMA就会看到一个旧的数据,那就产生了数据的不一致性。因为这个时候,最新的数据是在CPU这一侧的cache里,因为这个场景下,CPU是生产者,它来负责产生数据。
一般情况下系统的cache一致性有三种方案。
第一种方案:关闭cache
第一个方案:关闭cache。这是最简单的办法,不过,它会严重影响性能。比刚才那个例子为例,CPU产生数据,然后把数据会先放到一个DMA buffer里,如果采用关闭cache的方式,那么CPU在产生数据的过程中,CPU不能利用cache,这会严重影响性能,因为CPU要频繁的访问DDR,这样导致性能下降和功耗增加。
第二种方案:软件管理cache一致性
第二个方案:软件管理cache一致性。这是最常用的方式,软件需要在合适的时候去clean or flush dirty cache,或者invalidate old data。这种方式需要编写驱动的工程师,特别小心。
优点:硬件RTL实现简单
缺点:
-
软件复杂度增加。软件需要手动clean/flush cache或者invalidate cache
-
增加调试难度。因为the cache cleaning and invalidation的动作必须在合适的timing里完成,如果你处理不正确的话,那么你的DMA可能传输了错误的数据,这个是很难去debug的。因为你只是在某个偶然的时间点传输了错误的数据,又不是系统crash,所以,那个调试难度相对的大,你需要一帧一帧数据抓出来对比,还不一定会想到是cache没有flush或者invalidate的问题,你不一定能想到。造成数据corruption破坏的bug,是最难定位的。
-
降低性能,和增加功耗。可能有的小伙伴不明白,为啥软件去管理cache容易增加减低性能和和增加功耗。因为我们支持flush cache这个动作,你是需要时间的,就是把dirty的cache回写到内存里。在糟糕的情况下,你可能需要把整个cache都flush。也就是你增加了访问内存的次数,这样就降低了性能和增加了功耗。频繁去flush cache,其实不是一个好的习惯,大大影响了性能。
第三种方案:硬件管理cache一致性
第三个方案:硬件管理cache一致性。对软件是透明的。
对于多核之间的cache一致性,通常的做法就是在多核里实现一个MESI协议,实现一种snoop的控制单元。
对于系统级别的cache一致性需要需要实现一种coherent bus protocol。在2011年,arm在AMBA 4协议里,提出了AXI Coherency Extensions(简称ACE)来实现这个。ACE接口用来实现cluster之间的cache一致性,ACE lite接口用来实现IO设备的cache一致性,比如DMA,GPU等。