分享

ARMv8-A编程指导之Caches

 waston 2024-04-08 发布于捷克

        当ARM架构最初被开发时,处理的时钟速率和内存的访问速率基本相当。现在处理器core更加复杂且是内存时钟的指数级倍。但外部总线和内存设备的频率却没有达到这个范围。实现小块的速率可以达到core的片上SARM是可能的,但是这种RAM与标准DRAM块相比昂贵很多,标准DRAM比这种RAM容量大得多。在很多ARM处理器系统中,访问外部内存需要10倍甚至100倍的core周期。

        Cache就是小块的,快速的内存,它位于core和主内存之间。它保存着主内存的拷贝。访问cache明显快于访问主内存。当core访问(读或写)某个特殊地址,它首先在cache中查找。如果在cache中找到这个地址,它使用cache中的数据,而不是发起主内存的访问。这能通过减少缓慢的外部内存访问次数来明显提升系统潜在的性能。它也通过避免驱动外部信号来减少系统功耗。

        实现了ARMv8-A架构的处理器通常实现了二级或更多级的cache。这通常意味着处理器对每个core有小的L1指令和数据cache。Cortex-A53和Cortex-A57处理器正常来说实现了两级或更多级的cache,即小的L1指令和数据cache,和大的统一的L2 cache(它被一个cluster中的多个core共享)。另外,也可以有外部L3 cache作为外部硬件块被多个cluster共享。

        初次访问会将数据提供给cache,并不会比正常访问快。后续对已缓存的数据的访问将快于正常访问,从此时性能会增加。core的硬件会检查cache中的所有获取的指令和读写的数据,虽然你需要标记部分内存,比如包含外设的作为non-cacheable。因为cache仅存储主内存的一个子集,你需要一种方式来决定是否这个地址是你在cache中想要查找的。

        有时cache中的指令和数据和外部内存中的数据可能不一样,这是因为处理器可以更新cache中的内容,但并没有写回到主内存中。与此相对,agent可能在core获取到自己的拷贝后更新主内存。这个问题就是一致性问题,将在后续章节中描述。当你有多个core或内存agent(如外部的DMA控制器)时这将成一个特殊的问题。

1 Cache术语

        在冯诺依曼架构中,单个cache用于指令和数据(统一cache)。修改的哈佛架构有分开的指令和数据总线,因此有两个cache:指令cache(I-cache)和数据cache(D-cache)。在ARMv8处理器中,有不同的指令和数据L1 cache,但有统一的L2 cache。

        Cache要求能够缓存地址,数据和一些状态信息。

        下面为使用的一些术语的简单介绍,以及说明缓存基本结构的图表:

(1)tag为存储在cache中的内存地址的一部分,它来区分与数据线相关的主内存地址。

        64bit地址的高位说明了来自主内存中的信息的cache的位置,它是tag位。总共的cache大小为它能够保存数据的大小,虽然RAM用于保存tag值。但是,tag占用了cache的空间。

(2)对每个tag地址保存一个字是没有效率的,因此通常多个位置为一组使用相同的tag。这里逻辑块为cache line,它为最小的cache可加载单元,内存中连续的一块字。当它包含可缓存的数据或指令时cache line被认为valid,否则被认为invalid。

        与每个数据线相关的是一个或多个状态位。通常,你有一个有效位,它标识数据线包含着可以使用的数据。这意味着地址tag表示一些真实的值。在数据cache中,你可以也有一个或多个dirty位来标识是否cache line包含数据,但数据和主存中的数据不一样。

(3)index为内存地址的一部分,它决定地址可以在哪个cache line中被找到。

        地址的中间位或index区分cache line。index被用来作为cache RAM的地址且不需要作为tag一部分。这将在本章节的后面详细介绍。

(4)way为cache的细分,每中方式的大小相等,并以相同的方式索引。Set由共享某个index的所有way的cache line组成。

(5)这意味着地址的最后几位,称为offset,并不要求被存储在tag。你要求整个line的地址,而不是在line中的每个byte,因此5或6个最低位通常为0。

 1.1 组相连cache和路

        ARM核的cache通常使用一组相连的cache实现。这明显减少直接映射缓存出现cache thrashing的可能性,提高了程序执行速度,赋予更多的确定性执行。它是以增加硬件的复杂度和轻微的功耗为代价,因为多个tag在一个周期被比较。

        在这种cache组织中,cache被分为很多相同大小的片,称为way。一个内存位置可以映射到way而不是cache line。地址的index域继续用于选择特定的line,但是现在它只想每个way中的单个line。通常,每个L1 数据cache有二个或四个way。Cortex-A57有3路L1指令cache。通常对于L2 cache,它有16路。

        一个外部L3 cache的实现,如ARM CCN-504 Cache Coherent Network,可以有大量的way,它们高度相关,由于它们更大的size。有相同的index值的cache line属于同一个组。为了判断是否命中,你必须检查set中每个tag。

        如下图为2路cache。来自地址0x00,0x40或0x80的数据可能在line0中被找到,但并不是在两个cache路中同时被找到。

        增加cache的相连减少了thrashing的可能性。最理想的为全相连cache,任何主存的位置可以映射到cache中,但是,构建这种cache只在非常小的cache时实用,比如MMU TLB。在实际情况中,对于8路以上的性能改进最小,16路关联对较大的L2缓存更有用。

1.2 Cache tags和物理地址

        每个cache line都有一个tag与之相关,该tag记录了与cache line相关的外部内存的物理地址。Cache line的大小是由实现定义的。但是,由于内部互连所有的core需要有相同大小的cache line。

        访问的物理地址用来决定数据在cache中的位置。最低位用于选择cache line中的相关项。中间位作为index用来选在cache组中的特定的line。最高位用于标识地址的其他部分,并用于与该line存储的标识进行比较。在ARMv8中,数据cache通常为PIPT(物理index物理tag),但也可以为无别名的VIPT(虚拟index物理tag)。

        Cache中每个cache line包含:

  1. 相关物理地址的tag值;

  2. 有效位用来表明该line是否在cache中,即tag是否有效。如果cache的一致性跨越多个core时有效位也可以为MESI的状态位;

  3. Dirty数据位表明是否cache line中的数据和外部内存中的数据保持一致;

        ARM cache为组相连。这意味着对于给定的地址有多个可能的cache位置或路。一个组相连的cache明显减少cache thrashing的可能性且改善程序执行速度,但代价为增加硬件复杂度和对功耗有轻微增加。

        一个简单的4路组相连的32KB L1 cache(比如Cortex-A57处理器的数据cache),16word cache line长度,如下图所示:

1.3 包含和独占caches

        考虑一个简单的内存读,比如在单个core处理器上执行LDR X0, [X1]。

(1)如果X1指向内存中的一个位置,它被标记为可cacheable,然后再L1数据cache中会有cache查找;

(2)如果在L1 cache中找到地址,然后数据从L1 cache中读取并返回给core;

 

(3)如果在L1 cache中没有找到地址,但在L2 cache中,cache line会从L2 cache加载到L1 cache中,然后数据返回到core。这会导致cache line从L1中被回收腾出空间,但仍在L2 cache中存在;

 (4)如果地址既不在L1 cache也不再L2 cache中,数据被同时从外部内存加载到L1和L2 cache中,然后返回给core。这也会导致cache line被回收。

        这是相当简化后的视角。对于多核和多cluster系统中,在从外部内存加载之前,需要检查L2 cache或在cluster中的L1 cache或其他cluster。另外,在这里没有考虑L3 cache或系统cache。

        这就是包含cache模型,这里相同的数据可能在L1和L2 cache中同时存在。在独占cache中,数据只能在一种cache中存在,地址不能够同时在L1和L2 cache中存在。

2 cache控制器

        Cache控制器为负责管理cache内存的硬件模块,它是以一种程序不可见的方式。它自动从主存中将代码或数据写到cache中。它从core中获取读写内存请求,并对cache内存或外部内存发起必要的行为。

        当它接受到来自core的请求时,它必须检查请求的地址是否在cache中。这就是cache查找过程。它是通过将请求的地址位的子集与cache中tag值做比较。若匹配即命中,cache line被标记为有效,可以使用cache中的内存进行读或写。

        当core请求某个地址的指令或数据时,在cache tag中没有匹配,或tag无效,cache没有命中,请求必须被传递到内存层次的下一级,如L2 cache或外部内存。它也会造成cache line的填充。同时请求的数据或指令被发送到core。这个过程是透明的且对软件开发者不可见。core不需要在使用数据前等待cache line填充的完成。cache控制器通常访问cache line中的critical word。比如,你发送一个加载指令,但在cache中没有命中,会触发cache line的填充,core首先取出包含请求的数据的部分cache line。这些critical数据被发送到core的流水线,同时cache硬件和外部总线接口然后在后台读取剩下的cache line。

3 cache策略

        当一个cache line被分配为数据cache,cache策略描述了当store指令执行且在数据cache中被命中时应该如何做。

        cache分配策略如下:

Write allocation(WA):

        一个cache line在写未命中时分配。这意味着在处理器上执行store指令可能会导致突发读产生。对于cache line,在写发出前通过linefill来获取数据。cache包含整个cache line,它是加载的最小单元,即使你仅在cache line写单个byte。

Read allocation(RA)

        一个cache line在读未命中时分配。

        cache更新策略如下:

Write-back(WB):

写只更新cache且将cache line标识为脏。当cache line被回收或明确的清除时才更新外部的内存。

Write-through(WT)

        写同时更新cache和外部内存系统,这不会标记cache line为脏。

        数据读在cache中命中的行为在WT和WB cache模式中一样。

        正常内存的cacheable属性可分为inner和outer属性。Inner和outer的分界线由实现决定的,这在Memory Ordering中最更详细的描述。通常,inner属性用于集成cache,outer属性用于外部cache使用的处理器内存总线。

        正常内存可能被处理器预测性访问,这意味着它可以自动将数据加载到cache中,而无需程序员显示的请求特定的地址。这也在Memory Ordering中最更详细的描述。

        但是它也可以让程序员提供暗示给core在将来那些数据可以被使用。ARMv8-A提供提前加载的暗示指令。它是由实现来定义是否cache支持预测和提前加载。下列指令有效:

  1. AArch64:PRFM PLDL1KEEP,[Xm, #imm]; 这表示提前从Xm+offset中加载到L1 cache中作为临时预取,这意味着数据可能被多次使用。

  2. AArch32:PLD Rm; 从Rm地址中提前加载数据到cache中

        更普遍的,A64指令用于预取内存的有如下形式:

PRFM <prfop>, addr

        这里,<prfop>  <type><target><policy> | #uimm5

<type> PLD对加载做预取,PST对store做预取

<target> L1/L2/L3

<policy> 用于保留和临时预取的KEEP意味着正常分配在cache中;用于streaming或非临时预取的STRM意味着内存只使用一次

Uimm5 表示暗示被编码为5bit。它是可选的。

4 PoC和PoU

        对于组为基础和路为基础的清除和无效化,操作针对的是cache中的某一级cache。对于使用VA的操作,架构定义了连个点:

(1)一致性点PoC。对于某个地址,PoC为所有能够访问内存的观察者比如core,DSP,dma引擎保证看到一个内存的位置是相同内容。通常,主要是外部系统内存。

(2)统一点PoU。对于core来说,PoU为指令/数据cache以及core中转换表walk保证看到一个内存位置是相同的内容。比如,一个统一的L2 cache将是哈佛系统中L1 cache和缓存转换表项的TLB的PoU。如果没有外部cache存在,主存为PoU。

        了解PoU可以自我修改代码,以保证将来从修改后的代码中正确获取指令。他们从过两个阶段来做到这点:

  1. 清楚地址相关的数据cache项;

  2. 无效化地址的指令cache项;

        ARM架构不要求硬件来保证指令cache和内存之间的一致性,即使是共享内存的位置。

5 cache维护

        有时软件进行清除或无效化cache是必要的。当外部内存的内容被修改时,有必要移除cache中无效的数据。在MMU相关活动中(如更改访问权限,cache策略或虚拟地址到物理地址的映射)之后,或者当必须为动态生成的代码(如JIT编译器和动态库加载程序)同步I和D cache时,也可能需要它。

  1. cache或cache line的无效化意味着通过清cache line中的valid位来清楚cache line中的内容。在复位之后cache必须为无效的因为它的内容没有定义。这也可以看着是使cache之外的内存域的更改对cache用户可见的一种方式。

  2. 清楚cache line意味着写cache line的内容将其标为脏,写入到下一级cache,或到主存,并在cache line中清脏页。这使得cache line的内容与下一级的cache或内存系统中一致。这仅应用于在回写策略使用时的数据cache。这也是一种在cache中修改,让outer内存domain的用户可见,但这仅对数据cache有用。

  3. 清零。这会将cache中的内存块清零,而不需要首先从outer domain读取它们的内容。这也仅对数据cache有用。

对这些操作,你可以选择操作应该应用于哪些项:

  1. All,意味着所有的cache和对数据cache或统一cache没有用;

  2. MVA(修改的虚拟地址),VA的另外一个名字,包含某个虚拟地址的cache line;

  3. 在cache结构中通过它们的位置来选择的特定的组或路的cache line;

        AArch64 cache维护操作通过如下格式发出:

<cache> <operation>{,<Xt>}

下列操作有用:

Cache

Operation

Description

AArch32

DC

CISW

通过路/组清除和无效化

DCCISW

CIVAC

通过虚拟地址来清除和无效化到PoC

DCCIMVAC

CSW

通过路/组清除

DCCSW

CVAC

通过虚拟地址清除到PoC

DCCMVAC

CVAU

通过虚拟地址清除到PoU

DCCMVAU

ISW

通过路/组无效化

DCISW

IVAC

通过虚拟地址无效化到PoC

DCIMVAC

DC

ZVA

通过虚拟地址对cache清零

-

IC

IALLUIS

无效化所有到PoU,inner shareable

ICIALLUIS

IALLU

无效化所有到PoU

ICIALLU

IVAU

通过虚拟地址无效化到PoU

ICIMVAU

        接受地址参数的指令采用64位寄存器,该寄存器保存要维护的虚拟地址。此地址使用于对其限制。采用set/way/level参数的指令采用64为寄存器,其低32位遵循ARMv7体系结构中描述的格式。AArch64数据cache按地址无效化DC IVAC需要有写入权限,否则会产生权限错误。

        所有cache维护指令可以以其他指令cache维护指令,数据cache维护指令,以及加载和存储的任何顺序执行,除非在指令之间执行DSB。

        除了DC ZVA,指定地址的数据cache操作只有在相同地址的情况下,才保证以相对于彼此程序的顺序执行。指定地址的操作相对于所有为指定地址的维护操作,按程序顺序执行。

        考虑如下代码时序:

IC IVAU, X0 //通过地址进行指令cache的无效化到PoU

DC CVAC, X0 //通过地址进行数据cache的清楚到PoC

IC IVAU, X1 //若X0不等于X1,由于之前的操作导致无序

        前两个指令以顺序执行,因为它们涉及到相同的地址。但是,最后指令相对之前的操作重新排序,因为它涉及一个不同的地址。

IC IVAU, X0 //通过地址进行I cache无效化到PoU

IC IALLU //无效化I cache所有到PoU

        这仅应用于发指令。完成只有DSB指令之后来保证。

        在ARMv8-A中通过DC ZVA指令来提前用0来加载数据cache是新的。处理器可以运行明显快于外部内存系统,它有时会需要花长的时间来从内存加载cache line。

        Cache line清零的行为方式和预取类似,它会提示处理器某些地址在将来很可能被用到。但是,清零操作可以更快因为它不需要等待外部内存访问完成。

        取代从内存中获取数据到cache中,你可以将cache line填充0。它允许向处理器提示代码完全覆盖了cache line的内容,因此不需要初始化读取。

        考虑情况:你需要一个大的临时存储buffer或正初始化新的结构体。你可以让代码简单的开始使用内存,或你可以在使用之前写代码预取。两者都会在读取初始内容到cache过程中使用很多cycle和内存带宽。通过使用cache清零选项,你可能节省这些浪费的带宽并让代码执行更快。

        cache维护点定义依赖于是否可以通过VA或SET/WAY来执行指令。

        你可以选择范围,或PoC或PoU,这些操作也可以被广播,看Multi-core processors,你可以选择shareability。

        一些点需要注意:

  1. 在正常情况下,清除或无效化cache项仅firmware来做的,作为core的上下电的一部分。它也花费明显的时间,在L2 cache中cache line数量可能很大,有必要一个一个循环它们。因此这种清除行为明确定义为某些特殊场景。

  2. cache维护操作如DC CSW在前面节已经做描述。

  3. cache在时序的开始必须禁用,避免中间时序的新的cache line的分配。若cache为互斥的,cache line可能在多个level之间迁移。

  4. 在SMP系统中,另外的core可能将脏的cache line从看那个cache 中间时序去除,避免达到PoC。Cortex-A53和Cortex-A57处理器都会做这个操作。

  5. 如果在EL3,cache必须从安全world进行无效化且不能从正常world无效。如果保持不变,secure dirty数据会因为安全或正常world中的cache被回收时破坏内存系统。

        如果软件要求指令执行和内存的一致性,它必须使用ISB和DSB内存屏障和cache维护指令来管理一致性。

6 cache的发现

        cache维护操作可以从过cache set或way或VA来发出。平台无关的代码需要知道cache的大小,cache line的大小,组和路的数目,有多少级的cache。此需求最可能出现在post-reset后cache无效化和清零操作中。所有其他对架构cache的操作都是基于PoC和PoU。

        这里有一些系统控制寄存器包含这些信息:

  1. 存在的cache level数目由软件读取CLIDR_EL1决定;

  2. Cache line大小由CTR_EL0决定;

  3. 若需要从用户代码访问,执行在EL0,可通过设置SCTLR/SCTLR_EL1的UCT位决定;

        要求异常级别访问两个分开的寄存器来决定cache中组和路的数目。

  1. 代码必须首先写CSSELR_EL1来选择你将信息缓存到哪个cache;

  2. 代码都CCSIDR/CCSIDR_EL1;

  3. DCZID_EL0包含清零操作需要清零的块大小;

  4. SCTLR/SCTLR_EL1的DZE位和HCR/HCR_EL2中的TDZ位控制哪个异常级别和那个world可以访问DCZID_EL0. CLIDR_EL1, CSSELR_EL1和CCSIDR_EL1只通过权限代码访问,即AArch32中的PL1或更高,AArch64的EL1或更高;

  5. 若DC ZVA指令允许在异常级别执行,由SCTRL_EL1.DZE位控制,HCR_EL2.TDZ位的非安全指令,读取寄存器,返回表明是否指令支持的值;

  6. CLIDR寄存器仅意识到多少级的cache被集成到处理器中。它不能提供外部内存系统的任何cache的信息。

        比如,如果仅L1和L2集成的,CLIDR/CLIDR_EL1定义了两级cache且处理器没有意识到任何外部的L3 cache。

        当发出cache维护或代码来位置集成cache一致性时,有必要考虑非集成cache。

        另外,big.LITTLE系统中,描述的cache层次结构每个core都不一样,比如,Cortex-A53和Cortex-A57处理器在CTR.L1IP域不一样。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多