概述
当ARM架构首次开发时,处理器的时钟速度和内存的访问速度大致相似。如今的处理器内核要复杂得多,而且可以更快地实现数个数量级的时钟。然而,外部总线和内存设备的频率并没有达到相同的程度。
可以实现小的片上SRAM块,这些块可以以与内核相同的速度运行,但与标准DRAM块相比,这种RAM非常昂贵,标准DRAM块的容量可以高出数千倍。在许多基于ARM处理器的系统中,对外部存储器的访问需要数十甚至数百个核心周期。
缓存是位于核心和主内存之间的一个小而快速的内存块。它在主内存中保存项目的副本。对缓存的访问速度明显快于对主存的访问速度。每当内核读取或写入特定地址时,它首先在缓存中查找它。如果它在缓存中找到地址,它将使用缓存中的数据,而不是执行对主内存的访问。通过减少缓慢的外部内存访问时间的影响,这将显著提高系统的潜在性能。通过避免驱动外部信号,它还降低了系统的功耗。
ARMv8-A体系结构的处理器通常采用两级或两级以上的缓存。这通常意味着处理器对每个核心都有小的L1指令和数据缓存。Cortex-A53和Cortex-A57处理器通常使用两个或两个以上级别的缓存来实现,即一级指令和数据缓存,以及更大的二级缓存,二级缓存在集群中的多个核心之间共享。此外,还可以有一个外部三级缓存作为外部硬件块,在集群之间共享。
向缓存提供数据的初始访问速度并不比正常速度快。对缓存值的后续访问会更快,而性能的提高正是源于此。核心硬件检查缓存中的所有指令获取和数据读取或写入,但必须将内存的某些部分(例如包含外围设备的部分)标记为不可缓存。因为缓存只保存主内存的一个子集,所以需要一种快速确定要查找的地址是否在缓存中的方法。
有时,缓存中的数据和指令与外部内存中的数据可能不一样;这是因为处理器可以更新尚未写回主存的缓存内容。或者,一个代理可以在一个核心获取自己的副本后更新主内存。这是第14章描述的一致性问题。当有多个内核或内存代理(如外部DMA控制器)时,这可能是一个特殊的问题。
1、缓存术语
在冯·诺依曼体系结构中,指令和数据使用单个缓存(统一缓存)。改进后的哈佛体系结构有单独的指令和数据总线,因此有两个缓存,一个指令缓存(I-cache)和一个数据缓存(D-cache)。在ARMv8处理器中,有不同的指令和数据一级缓存,由统一的二级缓存支持。
缓存需要保存地址、一些数据和一些状态信息。
以下是对使用的一些术语的简要总结,以及说明缓存基本结构的图表:
- Tag是存储在缓存中的内存地址的一部分,用于标识与一行数据关联的主内存地址。64位地址的顶部位告诉缓存信息来自主存的位置,称为标记。虽然计算中不包括用于保存标记值的RAM,但总缓存大小是它可以保存的数据量的度量。但是,标记确实会占用缓存中的物理空间。
- 为每个标记地址保存一个字的数据效率很低,因此通常会将多个位置分组在同一个标记下。这个逻辑块通常被称为Cache Line,指的是缓存中最小的可加载单元,即来自主存的连续字块。缓存线包含缓存数据或指令时称为有效,不包含缓存数据或指令时称为无效。
与每行数据关联的是一个或多个状态位。通常,您有一个有效位,将该行标记为包含可使用的数据。这意味着地址标签代表一些实际值。在数据缓存中,还可能有一个或多个脏位,用于标记缓存线(或缓存线的一部分)是否包含与主内存内容不同(更新)的数据。 - 索引是内存地址的一部分,用于确定可以在缓存的哪些行中找到地址。地址或索引的中间位标识行。索引用作缓存RAM的地址,不需要存储作为标记的一部分。本章后面将详细介绍这一点。
- 路径是缓存的细分,每条路径大小相同,索引方式相同。一个集合由共享特定索引的所有方式的缓存线组成。
- 这意味着地址底部的几个位(称为偏移量)不需要存储在标记中。需要一整行的地址,而不是该行中的每个字节的地址,因此五或六个最低有效位始终为0。
1.1 设置关联缓存和方式
ARM内核的主缓存总是使用一组关联缓存来实现。这显著降低了直接映射缓存中出现的缓存抖动的可能性,提高了程序执行速度,并提供了更具确定性的执行。它的代价是硬件复杂度增加,功耗略有增加,因为每个周期都会比较多个标签。
通过这种缓存构型,缓存被划分为许多大小相同的块,称为“方式”。然后,内存位置可以映射到一条路径,而不是一条直线。地址的索引字段继续用于选择特定的行,但现在它以各种方式指向单独的行。通常,一级数据缓存有两种或四种方式。Cortex-A57有一个三种一级指令缓存。二级缓存通常有16种方式。
外部三级缓存实现,例如ARM CCN-504缓存一致性网络(请参阅第14-18页的计算子系统和移动应用程序),由于其规模大得多,可以有更多的方式,即更高的关联性。具有相同索引值的缓存线被称为属于一个集合。要查看命中率,必须查看集合中的每个标记。
在图11-3中,显示了一个双向缓存。地址0x00、0x40或0x80中的数据可能位于其中一种缓存方式的第0行,但不能同时位于这两种缓存方式中。
增加缓存的关联性可以降低抖动的概率。理想的情况是完全关联缓存,其中任何主内存位置都可以映射到缓存中的任何位置。然而,除了非常小的缓存之外,构建这样的缓存是不切实际的,例如,与MMU TLB关联的缓存。在实践中,对于8路以上的缓存,性能改进最小,16路关联性对于更大的二级缓存更有用。
1.2 缓存标记和物理地址
每一行都有一个与之相关联的标记,该标记在与该行相关联的外部内存中记录物理地址。缓存线的大小由实现定义。然而,由于互连的原因,所有的内核应该具有相同的缓存线大小。
访问的物理地址用于确定数据在缓存中的位置。最低有效位用于选择缓存线中的相关项。中间位用作索引,以选择缓存集中的特定行。最高有效位标识地址的剩余部分,并用于与该行存储的标记进行比较。在ARMv8中,数据缓存通常是物理索引、物理标记(PIPT)的,但也可以是非锯齿的虚拟索引、物理标记(VIPT)。
缓存中的每一行包括:
- 来自关联物理地址的标记值
- 指示缓存中是否存在该行的有效位,即标记是否有效。如果缓存在多个核之间是一致的,则有效位也可以是MESI状态的状态位
- 脏数据位,用于指示缓存线中的数据是否与外部内存不一致
ARM缓存设置为关联。这意味着对于任何给定的地址,都有多个可能的缓存位置或方式。集合关联缓存显著降低了缓存抖动的可能性,从而提高了程序执行速度,但代价是硬件复杂度增加,功耗略有增加。
图11-4显示了一个简化的四路集合关联32KB一级缓存(如Cortex-A57处理器的数据缓存),缓存线长度为16字(64字节):
1.3 包容性和排他性缓存
考虑一个简单的内存读取,例如,在单核处理器中的LDR X0,[X1]
-
如果X1指向内存中标记为可缓存的位置,则一级数据缓存中存在缓存查找
-
如果在一级缓存中找到地址,则从一级缓存读取数据并返回到核心
-
如果在一级缓存中找不到地址,但在二级缓存中,则缓存线将从二级缓存加载到一级缓存中,数据将返回到核心。这可能会导致一行从一级缓存中移出以腾出空间,但它可能仍存在于较大的二级缓存中
-
如果地址不在L1或L2缓存中,数据将从外部内存加载到L1和L2缓存中,并提供给内核。这可能会导致线路被逐出。
这是一个相当简单的观点。对于多核和多集群系统,在从外部内存执行加载之前,还可以检查集群内或其他集群的核心的L2或L1缓存的缓存。此外,此时不考虑L3或系统缓存。
这是一种包容性缓存模型,其中一级缓存和二级缓存中都可以存在相同的数据。在独占缓存中,数据只能存在于一个缓存中,并且不能同时在一级缓存和二级缓存中找到地址。
2、缓存控制器
高速缓存控制器是一个硬件块,负责管理高速缓存内存,其方式对程序来说基本上是不可见的。它会自动将代码或数据从主存写入缓存。它接收来自内核的读写内存请求,并对缓存或外部内存执行必要的操作。
当它收到来自内核的请求时,它必须检查请求的地址是否在缓存中找到。这就是所谓的缓存查找。将此标记的值与缓存线中与其关联的地址行的位进行比较。如果存在匹配项(称为命中),并且该行被标记为有效,则使用高速缓存进行读取或写入。
当内核从特定地址请求指令或数据,但与缓存标记不匹配,或标记无效时,会导致缓存未命中,请求必须传递到内存层次结构的下一级、二级缓存或外部内存。它还可能导致缓存线填充。缓存线填充会将一段主内存的内容复制到缓存中。同时,请求的数据或指令被传输到内核。这个过程是透明的,软件开发人员无法直接看到。在使用数据之前,内核无需等待填充完成。缓存控制器通常首先访问缓存线中的关键字。例如,如果执行的加载指令未命中缓存并触发缓存线填充,则内核首先检索缓存线中包含请求数据的部分。这些关键数据被提供给内核管道,而缓存硬件和外部总线接口随后在后台读取缓存线的其余部分。
3、缓存策略
缓存策略使我们能够描述何时应该将一行分配给数据缓存,以及当执行命中数据缓存的存储指令时应该发生什么。
缓存分配策略包括:
- 写入分配(WA):缓存线是在写未命中时分配的。这意味着在处理器上执行store指令可能会导致突发读取。在执行写操作之前,有一个linefill来获取缓存线的数据。缓存包含整行,这是它最小的可加载单元,即使您只写入行中的一个字节。
- 读取分配(RA):缓存线是在读取时分配的。
缓存更新策略包括:
- 写回(WB):写操作只更新缓存,并将缓存线标记为脏缓存。只有当线路被逐出或显式清理时,外部内存才会更新。
- 写通(WT):写操作会同时更新缓存和外部内存系统。这不会将缓存线标记为脏。
在WT和WB缓存模式下,命中缓存的数据读取行为相同。
普通内存的可缓存属性分别指定为内部和外部属性。内部和外部实现之间的界限已定义,并在第13章中有更详细的介绍。通常,内部属性由集成缓存使用,外部属性在处理器内存总线上可供外部缓存使用。
处理器可以有预测地访问普通内存,这意味着它可以潜在地自动将数据加载到缓存中,而无需程序员明确请求特定地址。这将在第13章内存排序中详细介绍。然而,程序员也可以向内核指示将来使用哪些数据。ARMv8-A提供预加载提示说明。缓存是否支持推测和预加载由实现定义。以下说明可用:
- AArch64: PRFM PLDL1KEEP, [Xm, #imm] ; 这表示从Xm+imm 预期指到一级缓存,作为预期指缓存区,这意味着数据可能会被多次使用。
- AArch32: PLD Rm ;将数据从Rm中的地址预加载到缓存
更一般地说,A64指令预取内存的形式如下:
4、Point of coherency and unification
对于使用虚拟地址的操作,体系结构定义了两点:
- 相干点(PoC)。对于一个特定地址,PoC是所有可以访问内存的观察者(例如,内核、DSP或DMA引擎)都能看到内存位置的相同副本的点。通常,这是主要的外部系统内存。
- 统一点(PoU)。内核的PoU是指保证核心的指令和数据缓存以及转换表能够看到内存位置的相同副本的点。例如,在一个系统中,统一的二级缓存将是统一点,该系统具有哈佛一级缓存和用于缓存转换表条目的TLB。如果没有外部缓存,主内存将是统一点。
对PoU的了解使代码能够自我修改,以确保将来正确地从修改后的代码版本获取指令。他们可以通过使用两个阶段的过程来实现这一点:
- 按地址清除相关的数据缓存项
- 按地址使指令缓存项无效
ARM体系结构不需要硬件来确保指令缓存和内存之间的一致性,即使对于共享内存的位置也是如此。
5、缓存维护
软件有时需要清除缓存或使其失效。当外部内存的内容已更改,并且需要从缓存中删除过时数据时,可能需要执行此操作。在与MMU相关的活动(如更改访问权限、缓存策略或虚拟地址到物理地址映射)之后,或者必须为动态生成的代码(如JIT编译器和动态库加载器)同步I缓存和D缓存时,也可能需要使用它。
- 缓存或缓存线的失效意味着通过清除一条或多条缓存线的有效位来清除数据。重置后,缓存必须始终无效,因为其内容未定义。这也可以视为一种在缓存之外的内存域中更改更改缓存的一种用户可见的方法。
- 清除缓存或缓存线意味着将标记为脏的缓存线的内容写入下一级缓存或主内存,并清除缓存线中的脏位。这使得缓存线的内容与缓存或内存系统的下一级保持一致。这仅适用于使用回写策略的数据缓存。这也是一种使缓存中的更改对外部内存域的用户可见的方法,但仅适用于数据缓存。
- 零。这会将缓存中的内存块归零,而无需首先从外域读取其内容。这仅适用于数据缓存。
对于这些操作中的每一项,您可以选择操作应应用于下列条目:
- All,是指整个缓存,不可用于数据或统一缓存
- Modified Virtual Address(MVA),VA的另一个名称,是包含特定虚拟地址的缓存线
- Set或Way是根据缓存线在缓存结构中的位置选择的特定缓存线
AArch64缓存维护操作使用具有以下一般形式的指令执行:
< cache > < operation >{, < Xt >}
有许多操作可用,如下
接受地址参数的指令采用64位寄存器,该寄存器保存要维护的虚拟地址。此地址不受对齐限制。采用Set/Way/Level参数的指令采用64位寄存器,其低32位遵循ARMv7体系结构中描述的格式。AARC64数据缓存按地址失效指令DC IVAC需要写入权限,否则会生成权限错误。
所有指令缓存维护指令可以按照相对于其他指令缓存维护指令、数据缓存维护指令以及加载和存储的任何顺序执行,除非在指令之间执行DSB。
指定地址的数据缓存操作(DC ZVA除外)只有在指定相同地址时才能保证按程序顺序执行。指定地址的操作按照与所有未指定地址的维护操作相关的程序顺序执行。
考虑下面的代码序列:
前两条指令按顺序执行,因为它们指向同一地址。但是,最终的指令可能会相对于之前的操作重新排序,因为它引用了不同的地址。
这只适用于发出指令。只有在收到DSB指令后才能保证完成。
ARMv8-A中新增了使用DC ZVA指令以零值预加载数据缓存的功能。处理器的运行速度比外部内存系统快得多,从内存加载缓存线有时需要很长时间。
缓存线归零的行为方式与预取类似,因为它是一种向处理器暗示未来可能会使用某些地址的方式。然而,由于不需要等待外部内存访问完成,所以归零操作可以快得多。
不是将实际数据从内存读取到缓存中,而是将缓存线填充为零。它可以向处理器暗示代码完全覆盖缓存线内容,因此无需进行初始读取。
考虑需要一个大的临时存储缓冲区或正在初始化一个新结构的情况。您可以让代码简单地开始使用内存,也可以编写在使用内存之前预取内存的代码。在将初始内容读取到缓存时,两者都会占用大量的周期和内存带宽。通过使用cache zero选项,您可能会节省浪费的带宽,并更快地执行代码。
缓存维护指令发生的点可以根据指令是通过VA操作还是通过Set/Way操作来定义。
您可以选择范围,可以是PoC或PoU,对于可以广播的操作,请参阅第14章多核处理器,您可以选择可共享性。
下面的示例代码演示了一种通用机制,用于将整个数据或统一缓存清理到PoC。
需要注意以下几点:
- 在正常情况下,作为内核通电或断电顺序的一部分,清理或使整个缓存失效是只有固件才应该做的事情。这也可能需要相当长的时间,二级缓存中的行数可能相当大,需要逐个循环。因此,这种清洁绝对只适用于特殊场合
- 缓存维护操作,如DC CSW,见第11-13页的缓存维护。
- 必须在序列开始时禁用缓存,以防止在序列中间分配新行。如果缓存是独占的,那么一行可以在不同级别之间迁移。
- 在SMP系统中,另一个内核可能能够从缓存中间序列获取脏缓存线,从而阻止它们到达PoC。Cortex-A53和Cortex-A7处理器都可以做到这一点。
- 如果存在EL3,则缓存必须从安全世界中失效,因为某些条目可能是“安全脏”数据,而不能从正常世界中失效。如果保持不动,“安全脏”数据在安全或正常世界中由于正常缓存使用而被逐出时,可能会损坏内存系统。
如果软件需要指令执行和内存之间的一致性,它必须使用ISB和DSB内存屏障以及缓存维护指令来管理这种一致性。示例11-4中所示的代码序列可用于此目的。
此代码序列仅对适合单个I或D缓存线的指令序列有效。
该代码通过虚拟地址清除数据和指令缓存并使其失效,虚拟地址的起始区域为x0中给定的基址和x1中给定的长度。
6、Cache discovery
缓存维护操作可以通过缓存集、缓存方式或虚拟地址来执行。独立于平台的代码可能需要知道缓存的大小、缓存线的大小、集合和方式的数量,以及系统中的缓存级别。重置后缓存失效和零操作最有可能出现这种要求。架构缓存上的所有其他操作可能都是基于PoC或PoU进行的。
有许多系统控制寄存器包含以下信息:
- 通过让软件读取缓存级别ID寄存器(CLIDR_EL1),可以确定存在的缓存级别数。
- 缓存线大小在缓存类型寄存器(CTR_EL0)中给出。
- 如果需要通过运行在执行级别EL0的用户代码访问,可以通过设置系统控制寄存器(SCTLR/SCTLR_EL1)的UCT位来实现。
需要对两个独立寄存器进行异常级别访问,以确定缓存中的集合数和方式数。
- 代码必须首先写入缓存大小选择寄存器(CSSELR_EL1)以选择要为其提供信息的缓存。
- 然后,代码读取缓存大小ID寄存器(CCSIDR/CCSIDR_EL1)
- 数据缓存零ID寄存器(DCZID_EL0)包含要为零操作归零的块大小。
- SCTLR/SCTLR_EL1的[DZE]位和虚拟机监控程序配置寄存器(HCR/HCR_EL2)中的[TDZ]位控制哪些执行级别和哪些世界可以访问DCZID_EL0。CLIDR_EL1、CSSELR_EL1和CCSIDR_EL1只能通过特权代码访问,即AARC32中的PL1或更高版本,或AARC64中的EL1或更高版本
- 如果在异常级别禁止按虚拟地址执行数据缓存归零(DC ZVA)指令,EL0.DZE位由SCTLR_EL1控制,HCR_EL2.TDZ位用于在EL1和EL0中的非安全执行,然后读取该寄存器返回一个值,该值指示该指令不受支持。
- CLIDR寄存器只知道处理器本身集成了多少级缓存。它无法提供有关外部内存系统中任何缓存的信息。例如,如果只集成了L1和L2,则CLIDR/CLIDR_EL1会识别两个级别的缓存,而处理器不知道任何外部L3缓存。在执行缓存维护或与集成缓存保持一致性的代码时,可能需要考虑非集成缓存。
此外,在一个大端系统中,所描述的缓存层次结构在不同的核心之间可能有所不同,例如,Cortex-A53和Cortex-A57处理器有不同的CTR.L1IP字段。