一、前言
相信很多开发人员都听说过 cache,但有可能对其不甚了解。毕竟在软件开发中很少会接触到这个概念,它的运作完全由 CPU 来完成,一般情况下不需要我们人员去干预。本文就来借助《ARM嵌入式系统开发》一书进行笔记整理,简单地讲述一下 cache 的相关内容,希望能够帮助到各位读者。
二、正文
首先,我们先需要明确 为什么需要cache?
回答: CPU 的运行速度比 内存 的存储速度好快上许多,这样会导致 CPU 需要等待 内存 完成处理后才能继续下一道指令,而 cache 是为了能够解决这一现象,cache 的处理速度跟得上 CPU,但它的存储空间非常小。将 cache 作为中间缓存,CPU 可以不用等待 内存,而 cache 可以在接收 CPU 的数据后,在往后的时间将这些数据放进 内存。
其次是 cache的大致原理是什么?
回答:在程序运行过程中,我们会有一个 局部性原理。这里不展开说明。它的大致意思就是程序会 频繁访问局部内存。 cache 是 CPU硬件 自动通过 内存地址 来找到对应的数据的。如果地址变换频繁,那么 cache 中存放的数据就会频繁改变。如果程序频繁访问 局部数据,那么 cache 中的数据改变就不会很大。因而命中率就会提高,从而 CPU 的运行效率也会提升。
2.1 计算机内存存储层次
如下图所示:
计算机内存存储层次
在 寄存器 和 内存 之间存在一道缓冲,分别是 cache 和 写缓冲。
cache:高速片上存储阵列,用于临时装载慢速存储器中的程序和代码。
写缓冲器:一个容量很小的FIFO缓冲器,主要用于对cache中写入内存的数据提供缓冲
根据 cache 和 MMU(内存管理单元) 的关系,可以将 cache 分为以下类型:
-
逻辑cache:位于 处理器 和 MMU 之间。处理器可以直接通过 逻辑cache 访问数据,不需要通过 MMU。也被称为 虚拟cache
逻辑cache
-
物理cache:位于 MMU 和 内存 之间。当 处理器 访问 内存 时,MMU 必须把 虚拟地址 转换为 物理地址,这样 cache 才能向 CPU 提供数据
物理canche
2.2 cache结构
CPU 在现阶段分为 冯诺依曼结构 和 哈佛结构。cache 也分为 2 种结构分别支持这 2 种 CPU结构:
- 冯诺依曼cache:数据和指令共用 cache,又被称为 混合cache 、统一cache
- 哈佛cache:数据和指令分别有不同的 cache,分为 Icache(指令cache) 和 Dcache(数据cache)。Icache 只存储 指令,Dcache 只存储 数据。
整个 cache 分为 cache控制器 和 cache存储器
- cache控制器:通过使用 处理器 提供的地址,选择 cache存储器 中的内容
- cache存储器:专用的存储器阵列,其访问单元称为 cache行。
- 写缓冲器:容量非常小的 高速FIFO存储缓冲器,用来临时存放处理器将要写入 内存 中的数据。
2.2.1 cache存储器
如下图:
cache存储器结构
cache行 分为以下 3 个部分:
- 目录存储段:记录每个 cache行 在 内存 的位置,因为 cache存储器 必须知道每个 cache行 所对应的 内存位置。对应图中的 cache标签。
-
状态信息段:对应于图中的 有效位 和 脏位。
- 有效位:用来标记当前 cache行 是有效的,即该 cache行 中包含从 内存 中获取的数据,并可以为 CPU 所用。
- 脏位:用来标记当前 cache行 中的数据与 内存 中对应地址的数据是否一致
- 数据项段:用于存储 内存对应地址的数据。一个 数据项大小为8bit
注意:每个 cache 的地址分段长度是不同的,所以图中没有给出对应的位域
2.2.3 cache控制器
cache控制器 可以将 内存 中的数据或代码自动复制到 cache存储器中,也就是能够在软件不为所知的情况下 自动完成搬运工作。
cache控制器 的工作流程如下:
- cache控制器 通过 组索引 在 cache存储器 中确定可能所要求的代码或数据的 cache行 的位置。然后通过 cache标签 和 状态位 来确定数据的实际存储位置。
- cache控制器 检查 有效位,确定该 cache行 当前是否处于 活动状态,并且将 请求地址 上的 标签 和 cache标签 比较。如果 cache行 当前是活动的,并且 标签域 与 cache标签 的值也相同,则 cache命中(hit),否则,cache失效(miss)
- 在 cache失效 的情况下,cache控制器 从 内存 中复制整个 cache行 到 cache储存器中。这个复制过程称为 cache行填充。
- 在 cache命中 时,cache控制器 直接从 cache存储器 为处理器提供数据或代码。cache控制器 使用 数据索引 在 cache行 中选择命中的代码或数据,并将其提供给处理器。
2.3 cache与内存的关系
前面讲过 cache 是通过内存地址进行数据索引的,那么就存在一种 从内存到cache行 的映射。
2.3.1 直接映射cache
这种映射关系比较简单,内存 中的每个地址都对应 cache存储器 中唯一的一行。
举个例子,假设 一个4K大小的cache,前面提到 2 点,即一个数据8bit 和 一个cache行有16个数据数据项,那么可以计算出一共有 256 个cache行。并且该 cache 的索引规则为 0-3bit为数据索引(刚好可以索引16个数据项),4-11bit为组索引。有 0x00010824 和 0x00020824 这 2 个地址,那么可以发现这 2 个地址对应的 cache行 是同一个。
可以通过将 内存地址中 的 标签域 进行比较,就可以确定存储的是 0x00010824 的数据还是 0x00020824 的数据。
当想要从 cache存储器 中读取 0x00010824 的数据,但 cache存储器 中存放的数据是地址 0x00020824 时,会发生 cache行 填充。此时 cache控制器 可以从 内存 中取出数据向 cache 中搬运的同时,也将数据传送给处理器,该过程称为 数据流注(data stream)。数据流注 允许处理器一边执行程序,一般向相应的 cache行 搬运剩余的数据和代码。
如果此时存放 0x00020824 数据的 cache行 的 有效位 为 1(即该cache行有效),同时内存中地址 0x00020824 与 cache行 中的不同。那么 这个过程也称为 替换。
cache替换 会将 cache行 中的数据写入 内存 的对应地址中,并删除 cache行 的原本内容,替换为 新地址的数据。
直接映射cache 的缺点是会导致 cache行 频繁置换,即 cache存储器 中同一位置的软件冲突,这称为 cache颠簸(thrashing)
2.3.2 组相联cache
2.3.2.1 组相联cache结构
前面讲到了 直接映射cache 的缺点,那么 组相联cache 就是用于解决该问题。
组相联cache 将 cache存储器 分成了一些相同容量的小单元,称职为 路(way)。以前面的 4Kcache 为例子,一共过来 256个cache行,现在分成 4路,那么每路有 64个cache行
如下图所示:
在组相联cache 中,每一路都有 相同组索引的cache行,所以一个 组索引 对应多个 cache行 。组索引 相同的 4个cache行 被称为 处于同一组,这就是 组索引 命名的由来。此时这 4个cache行 也被称为 组相联的。这就是 组索引 命名的由来,如下图所示:
内存中的代码或数据在不影响程序执行的情况下会被 分配到任一路中。同时 同一组cache行 的数据具有 排他性,可以防止同样的数据被重复放在 同组内的不同cache行中。
使用 组相联cache,我们内存映射到 cache 的大小被缩小了4倍,而同一个 cache行 被替换的概率减小为原来的 1/4,所以 组相联 以 大小 为代价来解决 cache颠簸。
2.3.2.2 CAM
假设在 64路组相联 的情况下,给出一个地址,那么这个地址的 标签域 就要被比较 64次 才有可能找出正确的 cache行,毕竟谁也不知道该地址的数据被藏在哪个 cache行 中。所以这个时候就有硬件 内容寻址存储器CAM(Content Addressable Memory)。在本例中 cache一共是64路cache,每路cache4个cache行,此时 CAM的数量为4。
CAM 的工作流程如下:
- 地址的 标签部分 作为 4个CAM 的输入,每个 CAM 将输入标签 与 同组内的64个cache行 进行比较。如果发现配,则数据由该 cache存储器 提供,否则将产生 失效信号(miss)
- 使用 组索引域 来选择 4个CAM 中的一个
- 再通过 数据索引部分 找到正确的数据。
2.3.3 cache指标
- 命中率=
- 失效率=
- 命中时间:指 处理器 访问 cache 中数据所需要的时间
- 失效开销:指 处理器 从 内存 中装载一个 cache行数据 到 cache 所需要的时间。
一般情况下有 1=命中率 + 失效率。
2.4 cache策略
cache策略 分一下 3 种,每种策略都是应用于不同的操作:
- 写策略:该策略决定了处理器 执行写操作 时数据存放的位置
- 替换策略:在 cache失效 的情况下,决定选择被替换到 内存 的 cache行
- 分配策略:在 cache失效 的情况下,决定 cache控制器 分配 cache行 的时机
2.4.1 写策略
写策略 分为 2 种:
- 直写法:该策略会使 处理器 写入 cache 时,将同时修改 cache 和 内存 中的内容,保证 cache 和 内存 的 数据一致性。
- 回写法:该策略会使 处理器 写入 cache 时,不会立即同步修改 内存 数据。这样可能会导致 cache 和 内存 的数据不是一致的。
下面说说 回写法 的如何把数据写入内存:
- 当 cache控制器 向 cache存储器 中的某一行写入数据时,会将 脏位 设置为 1。
- 如果 处理器 访问该 cache行,通过 脏位 的状态可以知道该 cache行 的数据是否含有内存中没有的数据。
- 如果 cache控制器 要将一个 脏位为1 的 cache行 替换出 cache存储器,那么该 cache行 的数据会自动写到 内存 中去,以保证数据不会在 内存 中丢失。
当程序频繁使用某些临时的局部变量时,回写法优于直写法
2.4.2 替换策略
在 cache失效 时,cache控制器 必须从当前有效的组中选择一个 cache行 来存储新的数据。被选中的 cacha行 称为 丢弃者(victim)。如果 丢弃者 的 脏位 为 1,则在被写入新数据前会把原来的数据写入 内存。选择和替换的过程称为 淘汰
替换策略 就是如何在一个组内选择 cache行 来写入新的数据,一般有下面几种 替换策略:
- 轮转法(循环替换):该策略简单地将当前分配 cache行 的下一行作为 丢弃者。该策略采用的选择算法是使用 连续加1的丢弃计数器,该计数器在每一次 cache控制器 分配新的 cache行 时加 1。当达到最大值时则复位为设定好的起始值
- 伪随机替换法:该策略随机地选出一个 cache行 替换出去。该算法使用了 非连续增加 的丢弃计数器,控制器 随机产生 一个增加值,使用该增加值加到 丢弃计数器 上。当达到最大值时则复位为设定好的起始值
- 最近最少使用法:该策略记录 cache行 的使用情况,并将 近期内最长时间没被访问过 的 cache行 替换掉。cortex-A15 以上支持该策略。
一般来说,轮转法 有更好的 可预测性,但是当存储器访问发生一些小的变化时有可能造成性能上较大的变化。
书中使用了一个例子来说明 轮转法 的缺点,笔者本来想复现代码,但发现手上的开发板的策略只支持 伪随机法,无法设置策略、
2.4.3 分配策略
按照笔者的理解,分配策略 是指 cache失效 在什么条件下需要找 cache行 来替换。一般情况下有下面 2 种策略:
-
读操作分配(read-allocate):如果 cache失效时,如果此次操作是 对内存进行读操作,此时要找出 cache行 进行替换。如果此次操作是 对内存进行写操作,那么则不找出 cache行 进行替换
-
写/读操作分配(read-allocate):如果 cache失效时,无论此次的操作是 对存储器进行读操作还是写,都要找出 cache行 进行替换。
2.5 cache架构
2.5.1 cache架构基本信息
前面我们讲了 cache 的基本信息,都是基于单个 cache 来说的。但是在 ARM 的 A系列核心 上,cache架构 是具有多个层次的,其架构图如下:
如图所示,每一个 CPU核 上都有 Icache 和 Dcache(我们一般将这 2 种 cache 称为 L1 cache),再往下一级就是 L2 cache。每个 CPU核 都有自己的 L1 cache,往下再 共享L2 cache。一般来说,L1 cache 的大小在 16KB-32KB,而 L2 cache 的大小在 256KB 以上。
按照图中所示,我们可以将其 存储层次 分为 3 层:
- L1 cache
- L2 cahce
- memory
通常来说,越接近 CPU 的存储层次,其访问速度越快。
2.5.2 VIPT
我们前面讲到访问 cache 的地址会被拆分为 标签、组索引、数据索引。但是并没有说明这个地址是 物理地址 还是 虚拟地址。
在现在 嵌入式linux 系统中,大部分都有 MMU 来管理 虚拟地址,以支持 进程特性。如果系统使用 虚拟地址,那么在访问 cache 时会出现以下问题:
- 如果两个进程的虚拟地址正好相同,则会出现虚地址访问cache冲突的情况
- cache访问的时候,需要先将虚拟地址经过TLB抓换为物理地址,会造成性能损失
- 将虚拟地址转换为物理地址时,需要将cache中内容清空
为了解决上面的问题,ARM 使用了另一种方案来访问 cache。即 virtual index physical tag(VIPT),也就是说 使用虚拟地址的组索引和物理地址的标签。
其步骤如下:
- 访问 TLB,将 虚拟地址 转换为 物理地址。与此同时,利用 虚拟地址 中的 组索引 访问cache,获取取出同一组内所有的cache行信息
- 假设 TLB 和 cache 都命中,则利用 步骤1 得到的 物理地址 中的 标签来找出确定 cache行。
2.5.3 PoC和PoU
前面说到 cache 是有多个层次的,最多的 ARMv8 可以支持 L1-L7 cache,再加上 内存。 一共 8 个 存储层次。随着 cache层次 的增加,cache硬件 也会越来越复杂,其管理难度也随机上升。
cache汇编指令操作 如下:
- 清理(clean):清理cache 会将 cache 中的 脏数据 写入内存。
- 清除(Invalidating):清除cache 会直接将 cache 中的数据复位,一般来讲是 有效位置0。
- 锁定:锁定 cache数据 不被修改
当我们需 清理多层次cache 时,这时就有一个问题: 如何保证多个存储层次层次的一致性?
前面讲过 cache 会根据 脏位 来考虑是否将 cache数据 写入 内存。这看起来很简单,但是因为只简到了 1个层次的cache。如果 多存储层次,高存储层次 中包含了部分下一级存储层次 的内容。此时维护数据一致性变得复杂了。例如:当要 清理 某个地址对应的 cache行 时,我们可以假设有下面 3 种操作层次:
- 仅仅操作 L1
- 覆盖L1和L2
- 覆盖L1-L3中对应的cache行
按照笔者的理解,为了明确操作的层次,ARM 定义了 2 个概念来解决歧义:
-
Point of unification(Pou):以一个执行了 cache操作指令的 PE(Process Element,可以理解过逻辑核心)为出发点。该 PE 需要透过 各层次cache 来访问 内存。假设该 操作 到某个 存储层次 为止,其访问的都是同一个数据,那么这个 层次 就是该 PE 的PoU。假设一个 4核cpu,每个 CPU核 都有自己的 L1 cache,所有的 CPU 核 共享 L2 cache。在该系统中,PoU 就是L2 cache。只有在该层次上,PE 的 L1 cache 和 TLB硬件单元 访问 内存 的时候,每个存储层次的数据是相同的。按照笔者理解,在 只有2个存储层次 的系统中,PoU就是L2 cache。
-
Point of coherency(PoC):PoC 和 PoU 的类似。但 PoC 是以系统中所有的 agent(包括CPU、DMA engine等能够访问内存的硬件单元)为视角,这些 agents 在访问 内存 时,直到某个 存储层次上,看到的都是同一个数据,该层级即为 PoC。例如一个 4核cpu,如果系统中的 DMA controller 和 内存 通过 总线 连接起来。在这样的一个系统中,PoC 就是 内存 这个 存储层次,因为 DMA controller 不通过 cache 来访问 内存。因此只能够在 内存 上才能看到同一个数据。DMA 访问内存也需要对 cache 进行清理。因为 DMA 无法访问 cache,所以使用 DMA 时需要让 cache 中的 脏数据 流注到内存中,保证 数据一致性
笔者也将自己在查找资料过程中涉及的其他信息也罗列出来,如下:
- 只有 Dache 有 PoC/PoU 的概念,Icache不涉及。
- 使用 协处理CP15 操作 cache 一般需要 特权模式,在 用户模式 下无法操作。
- 在内核源码 arch/arm/mm/cache-v7.S 可以查看 清除cache 等操作的函数,比如 __clear_cache 函数,其用来清理cache。
- 有关 cache操作 的 汇编指令 可以在 《Cortex-A7 MPCore Technical Reference Manual》 中找到,
参考链接
《ARM嵌入式系统开发》
《Cortex-A7 MPCore Technical Reference Manual》
《ARM Cortex-A Series Programmer’s Guide》
《ARM Architecture Reference Manual》
为打开MMU而进行的CPU初始化
什么是PoU和PoC?
Cache 为什么是物理地址映射? 及与TLB的关系?
ARMv8之Observability
物理CPU CPU核数 逻辑CPU 几核几线程的概念详解
PoU and PoC in cache maintenance operations in arm
网友评论