较复杂的内存和缓存概念

较复杂的内存和缓存概念

内存缓存基础概念 中,我们讨论了一些内存和缓存的基本概念,本部分主要讨论一些复杂的内存和缓存概念。

hash组相联和skewed-associative

组相联较好的平衡了电路复杂度和缓存miss率,但是他也有一定的不足。以常见的基于内存地址的某几位的组相联为例:

  1. 假设cache line=64Byte,四路组相联,那么相当于每间隔64*4=256B的内存地址就会被映射到同一路中。如果我们有一个二维数组double a[100][8],我们在访问第二维(a[i][0])时,这些地址实际上会被映射到同一路cache中,导致cache miss上升且cache利用率不高。
  2. 恶意程序可以通过观测不同缓存组的命中率进行攻击。比如将cache填满无用数据造成大量cache miss进而导致性能下降,推测目标进程的物理地址并注入等。

现代处理器会使用hash函数而不是内存地址的固定某几位判断属于哪个组。这样可以将内存地址与组的联系黑盒化,提升整体cache利用率。诸如Skylake的12-way,Xeon的16-way,20-wayL3cache都是通过将内存地址取hash后填入对应cache set的。

skewed-associative是另一种降低cache miss的方式。在普通组相联的情况下,假设内存地址f1,f2,f3他们经过hash后得到的结果一致,那么他们就会被映射到同一个组的cache line中。skewed-associative则将不同地址直接与cache line联系起来,比如f1可以放在set1line1-2,set2line3-4;f2可以放在set1line1,3,set2line2,4。这样打破了组相联不同组之间的屏障,降低cache miss率。arm的某些处理器可以配置sekwed-associative。

值得注意的是,这些设计都是在硬件层实现的,对于开发者来说应该是透明的。同时,实际测试经验表明传统的一些方法,如间隔插入变量手动映射地址到不同set没什么效果(因为已经被硬件做好了),但是内存地址对齐到cache line的效果还是比较明显的,尤其是在avx等向量化指令下。

VIVT,VIPT,PIVT和PIPT

cache中的cache line与内存地址通过index和tag对应起来,之前提到过,内存地址存在虚拟地址和物理地址,因此index和tag既可能是虚拟地址(Virtual)计算出的,又可能是物理地址(Physical)计算出的,组合后有VIVT,VIPT,PIVT和PIPT四种。对于CPU来说,其执行的地址是虚拟地址,也就是说VI和VT操作可以直接计算,而PI和PT操作需要通过MMU查TLB计算。

VIVT

VIVT cache的好处是快,因为不需要通过MMU转换地址,但是由于虚拟地址的特性,VIVT存在2个潜在缺点

  1. 假设有两个不同的进程A,B想要修改某个地址的值,其虚拟地址va相同而物理地址pa不同。如果进程A修改了它的va地址中的值,在B中观察到的现象就是cache中B的va的值也被改变了,因为cache中AB的va地址的index和tag是一致的。
  2. 假设有两个不同的进程A,B想要修改某个地址的值,其物理地址pa相同而虚拟地址va不同。如果进程A修改了A中va地址的值,在B中无法观察到这一更新,因为cache中AB的va和index和tag是不同的。

解决方法一般有两种,一是在切换进程时清空缓存,这样能保证cache中的数据时正确的,但是性能会受到较大损失。二是增加一个标记标记cache line归属于哪个进程,这样会增加电路设计复杂度。

VIPT

VIPT计算index时使用虚拟地址,而计算tag时采用物理地址。如果设计合理,比如计算index的内存位数都在page offset的bit中,那么VI和PI的计算结果时一致的。此时VIPT的性能比PIPT好,且避免了VIVT中的问题2。而在一般情况下,VIPT也能避免VIVT中的问题1。

PIPT

PIPT虽然慢,但是能完美解决VIVT中的问题1,2。其主要收益是切换进程不用清空缓存。

PIVT

PIVT使用较少,原因是index一般比tag先计算(先计算在哪个组,在比较组中的cache line查看命中)。在此情况下,使用PIPT和PIVT的性能非常接近,因此往往直接采用PIPT的设计。

VIVT一般被用来设计指令cache(l1i,l2i),因为其缺点可以被避免(切换进程肯定要清空指令),而收益较大。
VIPT被大量应用,如intel和AMD的cpu的l1d,l2等都采用了VIPT。
PIPT有时候被arm采用,因为在移动端处理器上频繁flush cache可能带来较大的能耗。

值得注意的是,有些设计暴露给开发者的是VIPT,但是底层硬件实际是PIPT,如arm的某些处理器。开发者在非必要的情况下无需关注cache line的具体组成结构,因为大多数完备的硬件已经保证cache能被合理的利用。

NUMA与缓存一致性

NUMA指的是在多节点下,不同CPU核心只能直接访问部分内存地址或缓存,访问剩下的内存需要让能直接访问这些地址的CPU核心代为访问并转发,这是提升横向扩展性的代价之一。常见的例子从多路服务器到家用电脑,比如Ryzen 3100和3300x之间性能的区别。经验表明对于内存密集型任务来说,NUMA能较好地利用多个node之间的带宽,但是其延迟较大,例如Xeon E5V3直接访问内存比访问另一个CPU连接的内存要快3倍。

缓存一致性指的是多核系统不同核心之间独立的l1(l2)缓存与共享的内存(有时包括llc)中数据发生冲突的情况。这可能发生在运行在不同核心上的多个程序试图访问/修改同一个内存地址,或者一个线程被调度到其他核心上,需要访问其在之前运行的核心上已经修改的内存数据。常见的解决方案是设置一个脏位标记,并通过一种控制协议,如MSI等控制。简单来说,这些协议通过M(modified),S(Shared),I(Invalid)等状态标记数据的有效性,同时决定何时将缓存中的数据传送到另一个缓存。也有不同的状态组合,比如MI,MEI,MSEI等等,一般状态越多能更完备的表示数据的状态,其性能也应该越好。

Disclaimer

Disclaimer

Contents on this website are licensed under a CC BY-SA 4.0 License.

This website is designed for knowledge sharing. The author are not liable for the accuracy and correctness of any information, ideas or opinions on this website.

内存缓存基础概念

内存缓存基础概念

本部分主要讨论一些内存缓存上的基础概念,下一章主要讨论一些复杂的内容和结合工业界实际情况(如ARM)的介绍。

内存基础概念

物理地址、虚拟地址

简单但并不完全准确来说,物理地址就是物理内存上可以直接寻址的地址,虚拟地址是操作系统虚拟出来的,一般程序能接触到的地址。物理地址和虚拟地址通过MMU进行映射,对于大部分程序来说是透明的,不需要关心具体的物理地址。

使用虚拟地址主要的好处有:

  1. 增加可申请内存大小。虚拟地址可以比物理地址有更多位数,比如x64至少可以支持48位。
  2. 不常用的虚拟内存地址对应的页可以被保存在磁盘上,提高内存利用率和效率。
  3. 屏蔽了不连续的物理内存地址与内存碎片,暴露给程序的虚拟内存地址一般是连续的。
  4. 使程序的物理内存地址随机化,增加安全性。

但是对于部分系统,如嵌入式系统,实时系统等,可能有以下缺点:

  1. 缺页中断时,TLB未命中时,需要从后一级缓存中加载数据,耗时较长且不稳定。
  2. 少数高性能场合性能优化不可控,见进阶内容中VIPT,VIVT和PIPT的讨论与缓存命中率的讨论。

TLB

TLB主要用于快速的转换虚拟地址和物理地址,可以理解成储存了一部分物理地址和虚拟地址对应关系的字典,一般位于CPU和cache中间。它相当于分页表的缓存,分页表中这保存了虚拟地址对应的物理地址,位于内存中。如果查询的地址不在TLB中,则需要到分页表中拿取对应的地址并将TLB中的一条记录替换,需要耗费较长的时间。

缓存

这里的缓存一般指CPU内部的cache,它减少了延迟并提高了数据带宽。cache遵循了2个局部性原则:被使用的数据在之后也可能被重复使用和被使用的数据周围的数据在之后可能被使用。现代处理器可能有多级缓存,比如L1,L2,L3等。其中部分缓存可能将指令区与数据区分开,如L1d和L1i。最后一级缓存也被称为LLC(Last Level Cache)或者LL,一般这个参数能描述出整体缓存效果,因为在L1-LLC阶段虽然cycle递增,但是整体耗时可接受,而如果数据在内存或者磁盘中,则需要显著更多的cycle去获取数据。

直接映射,全相联和组相联

cache与内存地址的映射关系一般有直接映射,全相联和组相联。

  • 直接映射:每个内存地址只能对应一个cache位置
  • 全相联:每个内存地址可以对应任意cache位置
  • 组相联:cache分为几组,每个内存位置可以对应一组中的任意一个cache位置

注意到,我们在这里没有区分虚拟地址和物理地址,这是因为cache具体结构可能使用物理地址也可能使用虚拟地址,详见进阶内容中的VIVT,VIPT,PIPT的讨论。另外,组相联还有其他变种如skewed associative cache等,详见进阶内容中的讨论。

直接映射电路简单,但是缓存的冲突率高;全相联虽然效果较好,但是电路过于复杂。组相联相对适中,电路不是很复杂,而缓存冲突率和全相联相近。

cache line

缓存的最小单位是cache line,一般是32B,64B或128B。读入或写回内存时,都需要以cache line位单位。

在组相联模式下,内存地址被分为了三个部分与cache line对应:index,tag和offset。index决定了这个内存地址具体放入哪个组,tag用来标记这个cache line,这样在判断一个地址是否已经在缓存中时只需要比较tag是和这个组某个cache line的tag一致即可,offset表示其在cache line中的偏移量。

注意到,我们同样没有指出内存时物理地址还是虚拟地址,详见进阶内容中的VIVT,VIPT,PIPT的讨论。

一般来说,你可以近似认为index,tag和offset是直接由内存地址的某几位决定的。但是注意到在实际上,出于安全和更高缓存利用率等的原因,他们更有可能是内存地址经过某种哈希函数后得到的,例如Intel Skylake的12way组相联和Xeon的20way组相联等。

缓存冲突与miss

注意,此处讨论的不是缓存一致性的冲突问题。

cache miss指的是CPU需要访问某个内存地址时,其数据不在缓存中的情况。此时需要向下一级缓存或内存请求数据。一般每向下一级缓存就需要耗费10倍以上的cycle。

缓存冲突表示在缓存从下一级缓存取回数据时,所有的cache line已满,必须要将某条cache line移出的情况。