Arm处理器cache的进化
Cache对CPU处理器的性能影响毋庸置疑。RISC构架成功的一个重要因素就是cache对内存访问性能的提升。RISC处理器普遍采用load-store的构架,随着pipeline的增强,如分支预测技术,超标量,乱序等技术的实现,对内存访问的带宽性能随之提高。现代CPU的设计很大的一块就是如何提升内存访问效率,其中越来越多的cache level, 和cache level之间的关系设计, 如何更高效的做推测性的cache prefetch。
另外随着多核技术的发展,如何更有能效(比如大小核)的,高效的实现cache一致性也是重要的技术。
以下阐述了arm应用处理器的Cache level进化历史,
Cortex-A8将L2 cache带进了处理器内部,并且L2 cache开始使用cache prefetch的功能,虽然还是需要软件控制PLE引擎来实现(后面转换成hardware prefetch). 并且L2 cache 可以独立于L1 cache进行开关。
Cortex-A8的cache line大小也随着总线宽度的提高变成64 bytes. 它之前的CPU的cache line为32 bytes。
在cache方面,在一个cluster里面的至多4个A9处理器的L1 data cache可以通过SCU的MESI协议做一致性维护。
大多数的A9多核系统会外接一个PL310 cache。
在big.LITTLE系统中,不同cluster之间的数据一致性问题。arm采用了将cache一致性协议扩展到cluster之间的方法,通过扩展总线协议为ACE总线,并将cluster通过cache coherency interconnector来实现。
为了进一步提供cluster内部的cache移植性维护效率和cache能力,在2010年发布的Cortex-A15和Cortex-A7处理器中,将L2 cache整合到cluster内部,可以供cluster里面的CPU共享(A7的L2 cache是可选的),并且A15和A7支持ACE协议。
如果A7和A15不组成big.LITTLE系统时,可以单独使用,配置为AXI4总线接口。
如果A7和A15组成big.LITTLE系统,可以使用CCI-400配合ACE总线接口,通过MESI协议实现A7和A15的cache一致性。
第一代的64位arm处理器大小核系统,还是以CCI-400为cache一致性互联。但如上图所示,CCI-400中没有snoop filter(也就没有对CPU的duplicated tag ram), 虽然可以减少系统的gate数,但所有的shareable内存的访问需要snoop的话,都必须直接去snoop到其他cluster中的CPU cache,在效率方面有一些问题。
因此,arm在CCI-400的基础上推出了CCI-500/550。它们带有snoop filter,在CCI中duplicate cluster里CPU的cache tag信息,帮助CPU之间的snoop,避免不必要的snoop到另一个cluster的CPU cache. CCI-500/550里的snoop filter当成一个snoop的中心,避免不必要的snoop broadcast。
为了近一步提供内存访问效率,system L3 cache开始使用。Arm引入了AMBA5 CHI 总线标准和CCN总线互联IP。这些IP实现了环形总线,并且包含L3 cache和snoop filter.
CCN系列的IP包括
CCN-502:
0~8MB L3 cache, 支持4个cluster,多达16 CPU
CCN-504:
1-16MB L3 cache, 支持4个cluster,多达16 CPU
CCN-508:
1-32MB L3 cache, 支持8个cluster,多达32 CPU
CCN-512:
1-32MB L3 cache, 支持12个cluster,多达48 CPU
在arm开创性的big.LITTLE技术的基础上,DynamIQ使大小核可以放在同一个DSU cluster. 这使得big LITTE CPU的配置更加灵活, workload在核间迁徙的latency更小。因为数据的迁徙可以在一个cluster里面完成,并且核间共用DSU L3 cache.
新的Cortex-A55, Cortex-A75/76/77/78处理器CPU将L1 cache和 L2 cache变为CPU私有cache, DSU cluster里面的CPU共享其L3 cache.
这种设计使CPU的私有cache大小提升了一个等级。
所有CPU之间的cache一致性 DSU中可以解决,但是可能会需要一个CCI来完成IO device和CPU的cache之间一致性维护。
随着arm处理器在PC和服务器市场的挺进。arm推出了Neoverse N和V系列处理器。如果你需要组成一个更大的系统,arm的CMN mesh网络互联可以帮忙,它支持2Dmesh网络连接,并且带分布式的system cache, 可以支持大规模的CPU系统。
CMN-600支持多达128 Neoverse CPU,可以配置高达120MB的system cache。
Neoverse处理器有一个独特的功能,就是可以实现指令cache-指令cache和数据cache的一致性维护。这是针对服务器市场特殊要求(特别是JIT虚拟的要求)设计的。
更进一步,CCIX和CXL将cache的一致性推向multi-socket 系统,更加友好和高效地完成多个socket CPU之间,CPU和加速器间的数据共享,cache 一致性维护。
如何屏蔽不同的cache结构,保持软件兼容性?
arm系统中 cache的结构可能完全不一样,有不同的cache level数,cache之间的inclusive/exclusive的关系也不尽相同。
这带来一个问题,如果需要做cache的维护操作(CMO),比如 Invalidate, Clean操作,在以操作系统,驱动,应用中是否要针对不同的cache结构写不同的代码。
如果这样,代码会平台相关,可移植性比较差。
怎么解决这个问题呢?其实CPU构架定义和软件构架定义类似,也需要将CMO操作根据其应用场景和要达到的目的进行抽象。Arm根据软件应用场景,从armv7构架开始定义了Point of Coherency (PoC)和Point of Unification (PoU)的概念。它们定义了一个对虚拟地址的CMO要对那些cache levels进行操作。
大多数情况下,cache自动工作,软件并不需要对其进行操作(对软件透明),但是在某些情况下需要软件干预,进行CMO操作,来保证内存访问(包括数据访问和取指令)的一致性,这通常是,
- CPU和 device的交互(如DMA)
- Self-modifying code, 这个场景包括JIT, code decompress, 热补丁等应用场景。改代码通过数据访问path经过数据 cache进行,取指令通过指令path经过指令 cache进行。这时需要cache CMO操作。
根据arm Architecture Reference Manual定义,
Point of Coherency (PoC)
The point at which all agents that can access memory are guaranteed to see the same copy of a memory location for accesses of any memory type or cacheability attribute. In many cases this is effectively the main system memory, although the architecture does not prohibit the implementation of caches beyond the PoC that have no effect on the coherency between memory system agents.
Note: The presence of system caches can affect the determination of the point of coherency as described in System level caches.
翻译:一个VA的CMO到PoC这点,可以保证系统里面所有可以访问内存的agent对这个可缓存的或是其他内存类型的地址访问都可以看到一样的copy。在很多情况下,它都是系统的主内存,虽然构架并没有禁止实现在PoC点之后但不影响访问内存的agent们间一致性的系统内存。
Point of Unification (PoU)
The PoU for a PE is the point by which the instruction and data caches and the translation table walks of that PE are guaranteed to see the same copy of a memory location. In many cases, the Point of Unification is the point in a uniprocessor memory system by which the instruction and data caches and the translation table walks have merged. The PoU for an Inner Shareable shareability domain is the point by which the instruction and data caches and the translation table walks of all the PEs in that Inner Shareable shareability domain are guaranteed to see the same copy of a memory location.
Defining this point permits self-modifying software to ensure future instruction fetches are associated with the modified version of the software by using the standard correctness policy of: 1.Clean data cache entry by address.2.Invalidate instruction cache entry by address.
翻译:在一个PE(可以理解为一个CPU或是一个 hardware线程)做一个VA的CMO到PoU这点,它保证这个PE的指令cache, 数据cache 和包含TLB的MMU端,对一个内存位置的访问可以看到同样的copy。在很多情况下,一个单核系统的PoU这点在指令,数据cache和包含TLB的MMU table walk访问被合并的地方。一个Inner Shareable shareability domain 系统的PoU保证这个Inner Shareable shareability domain里所有的PE的指令,数据cache和包含TLB的MMU table walk可以看到同样的copy.
定义PoU这点运行self-modifying 软件保证通过以下标准正确的方式让将来的取指令可以取到修改过的指令,
- 将这个被修改的指令的地址对应的数据cache做 Clean操作
- 将这个被修改的指令的地址对应的指令cache做 Invalidate操作
在定义PoU和PoC这个概念之后,软件只需要根据需求选择使用带PoU或PoC的CMO指令,而不需要关心具体的硬件cache结构。将保证PoU和PoC定义的目标的任务交给了SoC硬件设计。SoC硬件设计必须通过配置CPU Processor的硬件,interconnector的设计和硬件配置,准确无误地操作系统中某些cache levels达到上面定义的目标。不同的硬件系统同样的PoU和PoC指令,可能需要进行操作的cache levels不一样。可能需要进行 CMO的硬件 broadcast(广播)和propagate (下传)。
系统的PoU一般在L2 memory这个点,也就是只需要做L1 cache的CMO操作。而PoC根据不同系统设计,可以在不同的点,一般在外部内存。
这系统中,PoC操作可以影响L1,L2, L3 cache.
PoU在软件中的使用
PoU的使用的场景主要在self-modifying code, 包括热补丁,代码解压缩,JIT代码生成等。
下面举几个例子,
热补丁,在 Linux kernel里经常会用
int __kprobes aarch64_insn_patch_text_nosync(void *addr, u32 insn)
{
u32 *tp = addr;
int ret;
/* A64 instructions must be word aligned */
if ((uintptr_t)tp & 0x3)
return -EINVAL;
ret = aarch64_insn_write(tp, insn);
if (ret == 0)
__flush_icache_range((uintptr_t)tp,
(uintptr_t)tp + AARCH64_INSN_SIZE);
return ret;
}
进行kernel代码的 patching,在__flush_icache_range中会使用,
/*
* flush_icache_range(start,end)
*
* Ensure that the I and D caches are coherent within specified region.
* This is typically used when code has been written to a memory region,
* and will be executed.
*
* - start - virtual start address of region
* - end - virtual end address of region
*/
ENTRY(__flush_icache_range)
/* FALLTHROUGH */
/*
* __flush_cache_user_range(start,end)
*
* Ensure that the I and D caches are coherent within specified region.
* This is typically used when code has been written to a memory region,
* and will be executed.
*
* - start - virtual start address of region
* - end - virtual end address of region
*/
ENTRY(__flush_cache_user_range)
uaccess_ttbr0_enable x2, x3, x4
alternative_if ARM64_HAS_CACHE_IDC
dsb ishst
b 7f
alternative_else_nop_endif
dcache_line_size x2, x3
sub x3, x2, #1
bic x4, x0, x3
1:
user_alt 9f, "dc cvau, x4", "dc civac, x4", ARM64_WORKAROUND_CLEAN_CACHE
add x4, x4, x2
cmp x4, x1
b.lo 1b
dsb ish
7:
alternative_if ARM64_HAS_CACHE_DIC
isb
b 8f
alternative_else_nop_endif
invalidate_icache_by_line x0, x1, x2, x3, 9f
8: mov x0, #0
1:
uaccess_ttbr0_disable x1, x2
ret
9:
mov x0, #-EFAULT
b 1b
ENDPROC(__flush_icache_range)
.macro invalidate_icache_by_line start, end, tmp1, tmp2, label
icache_line_size \tmp1, \tmp2
sub \tmp2, \tmp1, #1
bic \tmp2, \start, \tmp2
9997:
USER(\label, ic ivau, \tmp2) // invalidate I line PoU
add \tmp2, \tmp2, \tmp1
cmp \tmp2, \end
b.lo 9997b
dsb ish
isb
.endm
可以看到其中使用了 dc cvau 指令来clean数据cache到PoU这点,使用ic ivau指令来invalidate指令cache到PoU这点。
在JIT虚拟机中会产生新的代码,这也相当于self-modifying code, 因此也需要对cache进行操作。对于像Andorid的用户空间虚拟机,它调用C库里提供的 __builtin___clear_cache 内建函数来完成这个功能。
比如https://github.com/llvm-mirro...
void __clear_cache(void *start, void *end) {
...
#elif defined(__aarch64__) && !defined(__APPLE__)
uint64_t xstart = (uint64_t)(uintptr_t)start;
uint64_t xend = (uint64_t)(uintptr_t)end;
uint64_t addr;
// Get Cache Type Info
uint64_t ctr_el0;
__asm __volatile("mrs %0, ctr_el0" : "=r"(ctr_el0));
// dc & ic instructions must use 64bit registers so we don't use
// uintptr_t in case this runs in an IPL32 environment.
const size_t dcache_line_size = 4 << ((ctr_el0 >> 16) & 15);
for (addr = xstart & ~(dcache_line_size - 1); addr < xend;
addr += dcache_line_size)
__asm __volatile("dc cvau, %0" ::"r"(addr));
__asm __volatile("dsb ish");
const size_t icache_line_size = 4 << ((ctr_el0 >> 0) & 15);
for (addr = xstart & ~(icache_line_size - 1); addr < xend;
addr += icache_line_size)
__asm __volatile("ic ivau, %0" ::"r"(addr));
__asm __volatile("isb sy");
其中也使用dc cvau和ic ivau来完成对修改的代码的数据/指令cache的一致性。
需要注意的是有些处理器可以通过硬件的优化来实现指令cache和数据cache的一致性,比如arm的Nevoerse N1处理器硬件可以配置为支持指令和数据cache的一致性,从而使得不用CMO到PoU的操作。是否具备这样的能力,会在处理器CTR_EL0寄存器中的DIC和IDC bit表示,参看arm构架文档.
因此可以看到上面代码中有 ARM64_HAS_CACHE_DIC, ARM64_HAS_CACHE_IDC 的检查,来决定是否需要对指令cache做CMO到PoU,或是数据cache CMO到PoU.
PoC在软件中的使用
CMO到PoC的操作基本都是为了维护device和CPU cache的一致性(如果硬件设计不能支持 device和CPU cache的一致性的话)。
其中一个例子就是流式 DMA映射,在映射和解除映射的时候需要做 CMO. 比如dma_map_single时,如果是DMA_FROM_DEVICE,会先做DC CIVAC - flush cache(cache & invalidate)到PoC, 对于DMA_TO_DEVICE会做DC CVAC - clean cache到PoC。
/*
* __dma_clean_area(start, size)
* - start - virtual start address of region
* - size - size in question
*/
__dma_clean_area:
dcache_by_line_op cvac, sy, x0, x1, x2, x3
ret
ENDPIPROC(__clean_dcache_area_poc)
ENDPROC(__dma_clean_area)
总结
arm通过总结软件对cache maintenance操作的理解,创新性地抽象出指令级的cache PoU和PoC操作,从而是软件可以适应从简单到复杂的cache level硬件设计,使得软件具有非常好的跨平台性。