当ARM架构首次开发时,处理器的时钟速度和内存的访问速度大致相似。如今的处理器内核要复杂得多,而且可以更快地实现数个数量级的时钟。然而,外部总线和内存设备的频率并没有达到相同的程度。
可以实现小的片上SRAM块,这些块可以以与内核相同的速度运行,但与标准DRAM块相比,这种RAM非常昂贵,标准DRAM块的容量可以高出数千倍。在许多基于ARM处理器的系统中,对外部存储器的访问需要数十甚至数百个核心周期。
缓存是位于核心和主内存之间的一个小而快速的内存块。它在主内存中保存项目的副本。对缓存的访问速度明显快于对主存的访问速度。每当内核读取或写入特定地址时,它首先在缓存中查找它。如果它在缓存中找到地址,它将使用缓存中的数据,而不是执行对主内存的访问。通过减少缓慢的外部内存访问时间的影响,这将显著提高系统的潜在性能。通过避免驱动外部信号,它还降低了系统的功耗。
ARMv8-A体系结构的处理器通常采用两级或两级以上的缓存。这通常意味着处理器对每个核心都有小的L1指令和数据缓存。Cortex-A53和Cortex-A57处理器通常使用两个或两个以上级别的缓存来实现,即一级指令和数据缓存,以及更大的二级缓存,二级缓存在集群中的多个核心之间共享。此外,还可以有一个外部三级缓存作为外部硬件块,在集群之间共享。
向缓存提供数据的初始访问速度并不比正常速度快。对缓存值的后续访问会更快,而性能的提高正是源于此。核心硬件检查缓存中的所有指令获取和数据读取或写入,但必须将内存的某些部分(例如包含外围设备的部分)标记为不可缓存。因为缓存只保存主内存的一个子集,所以需要一种快速确定要查找的地址是否在缓存中的方法。
有时,缓存中的数据和指令与外部内存中的数据可能不一样;这是因为处理器可以更新尚未写回主存的缓存内容。或者,一个代理可以在一个核心获取自己的副本后更新主内存。这是第14章描述的一致性问题。当有多个内核或内存代理(如外部DMA控制器)时,这可能是一个特殊的问题。
在冯·诺依曼体系结构中,指令和数据使用单个缓存(统一缓存)。改进后的哈佛体系结构有单独的指令和数据总线,因此有两个缓存,一个指令缓存(I-cache)和一个数据缓存(D-cache)。在ARMv8处理器中,有不同的指令和数据一级缓存,由统一的二级缓存支持。
缓存需要保存地址、一些数据和一些状态信息。
以下是对使用的一些术语的简要总结,以及说明缓存基本结构的图表:
ARM内核的主缓存总是使用一组关联缓存来实现。这显著降低了直接映射缓存中出现的缓存抖动的可能性,提高了程序执行速度,并提供了更具确定性的执行。它的代价是硬件复杂度增加,功耗略有增加,因为每个周期都会比较多个标签。
通过这种缓存构型,缓存被划分为许多大小相同的块,称为“方式”。然后,内存位置可以映射到一条路径,而不是一条直线。地址的索引字段继续用于选择特定的行,但现在它以各种方式指向单独的行。通常,一级数据缓存有两种或四种方式。Cortex-A57有一个三种一级指令缓存。二级缓存通常有16种方式。
外部三级缓存实现,例如ARM CCN-504缓存一致性网络(请参阅第14-18页的计算子系统和移动应用程序),由于其规模大得多,可以有更多的方式,即更高的关联性。具有相同索引值的缓存线被称为属于一个集合。要查看命中率,必须查看集合中的每个标记。
在图11-3中,显示了一个双向缓存。地址0x00、0x40或0x80中的数据可能位于其中一种缓存方式的第0行,但不能同时位于这两种缓存方式中。
增加缓存的关联性可以降低抖动的概率。理想的情况是完全关联缓存,其中任何主内存位置都可以映射到缓存中的任何位置。然而,除了非常小的缓存之外,构建这样的缓存是不切实际的,例如,与MMU TLB关联的缓存。在实践中,对于8路以上的缓存,性能改进最小,16路关联性对于更大的二级缓存更有用。
每一行都有一个与之相关联的标记,该标记在与该行相关联的外部内存中记录物理地址。缓存线的大小由实现定义。然而,由于互连的原因,所有的内核应该具有相同的缓存线大小。
访问的物理地址用于确定数据在缓存中的位置。最低有效位用于选择缓存线中的相关项。中间位用作索引,以选择缓存集中的特定行。最高有效位标识地址的剩余部分,并用于与该行存储的标记进行比较。在ARMv8中,数据缓存通常是物理索引、物理标记(PIPT)的,但也可以是非锯齿的虚拟索引、物理标记(VIPT)。
缓存中的每一行包括:
ARM缓存设置为关联。这意味着对于任何给定的地址,都有多个可能的缓存位置或方式。集合关联缓存显著降低了缓存抖动的可能性,从而提高了程序执行速度,但代价是硬件复杂度增加,功耗略有增加。
图11-4显示了一个简化的四路集合关联32KB一级缓存(如Cortex-A57处理器的数据缓存),缓存线长度为16字(64字节):
考虑一个简单的内存读取,例如,在单核处理器中的LDR X0,[X1]
如果X1指向内存中标记为可缓存的位置,则一级数据缓存中存在缓存查找
如果在一级缓存中找不到地址,但在二级缓存中,则缓存线将从二级缓存加载到一级缓存中,数据将返回到核心。这可能会导致一行从一级缓存中移出以腾出空间,但它可能仍存在于较大的二级缓存中
如果地址不在L1或L2缓存中,数据将从外部内存加载到L1和L2缓存中,并提供给内核。这可能会导致线路被逐出。
这是一个相当简单的观点。对于多核和多集群系统,在从外部内存执行加载之前,还可以检查集群内或其他集群的核心的L2或L1缓存的缓存。此外,此时不考虑L3或系统缓存。
这是一种包容性缓存模型,其中一级缓存和二级缓存中都可以存在相同的数据。在独占缓存中,数据只能存在于一个缓存中,并且不能同时在一级缓存和二级缓存中找到地址。
高速缓存控制器是一个硬件块,负责管理高速缓存内存,其方式对程序来说基本上是不可见的。它会自动将代码或数据从主存写入缓存。它接收来自内核的读写内存请求,并对缓存或外部内存执行必要的操作。
当它收到来自内核的请求时,它必须检查请求的地址是否在缓存中找到。这就是所谓的缓存查找。将此标记的值与缓存线中与其关联的地址行的位进行比较。如果存在匹配项(称为命中),并且该行被标记为有效,则使用高速缓存进行读取或写入。
当内核从特定地址请求指令或数据,但与缓存标记不匹配,或标记无效时,会导致缓存未命中,请求必须传递到内存层次结构的下一级、二级缓存或外部内存。它还可能导致缓存线填充。缓存线填充会将一段主内存的内容复制到缓存中。同时,请求的数据或指令被传输到内核。这个过程是透明的,软件开发人员无法直接看到。在使用数据之前,内核无需等待填充完成。缓存控制器通常首先访问缓存线中的关键字。例如,如果执行的加载指令未命中缓存并触发缓存线填充,则内核首先检索缓存线中包含请求数据的部分。这些关键数据被提供给内核管道,而缓存硬件和外部总线接口随后在后台读取缓存线的其余部分。
缓存策略使我们能够描述何时应该将一行分配给数据缓存,以及当执行命中数据缓存的存储指令时应该发生什么。
缓存分配策略包括:
缓存更新策略包括:
普通内存的可缓存属性分别指定为内部和外部属性。内部和外部实现之间的界限已定义,并在第13章中有更详细的介绍。通常,内部属性由集成缓存使用,外部属性在处理器内存总线上可供外部缓存使用。
处理器可以有预测地访问普通内存,这意味着它可以潜在地自动将数据加载到缓存中,而无需程序员明确请求特定地址。这将在第13章内存排序中详细介绍。然而,程序员也可以向内核指示将来使用哪些数据。ARMv8-A提供预加载提示说明。缓存是否支持推测和预加载由实现定义。以下说明可用:
对于使用虚拟地址的操作,体系结构定义了两点:
对PoU的了解使代码能够自我修改,以确保将来正确地从修改后的代码版本获取指令。他们可以通过使用两个阶段的过程来实现这一点:
软件有时需要清除缓存或使其失效。当外部内存的内容已更改,并且需要从缓存中删除过时数据时,可能需要执行此操作。在与MMU相关的活动(如更改访问权限、缓存策略或虚拟地址到物理地址映射)之后,或者必须为动态生成的代码(如JIT编译器和动态库加载器)同步I缓存和D缓存时,也可能需要使用它。
对于这些操作中的每一项,您可以选择操作应应用于下列条目:
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。
需要注意以下几点:
如果软件需要指令执行和内存之间的一致性,它必须使用ISB和DSB内存屏障以及缓存维护指令来管理这种一致性。示例11-4中所示的代码序列可用于此目的。
此代码序列仅对适合单个I或D缓存线的指令序列有效。
该代码通过虚拟地址清除数据和指令缓存并使其失效,虚拟地址的起始区域为x0中给定的基址和x1中给定的长度。
缓存维护操作可以通过缓存集、缓存方式或虚拟地址来执行。独立于平台的代码可能需要知道缓存的大小、缓存线的大小、集合和方式的数量,以及系统中的缓存级别。重置后缓存失效和零操作最有可能出现这种要求。架构缓存上的所有其他操作可能都是基于PoC或PoU进行的。
有许多系统控制寄存器包含以下信息:
需要对两个独立寄存器进行异常级别访问,以确定缓存中的集合数和方式数。
此外,在一个大端系统中,所描述的缓存层次结构在不同的核心之间可能有所不同,例如,Cortex-A53和Cortex-A57处理器有不同的CTR.L1IP字段。