如何测试各级 cache 的访问时延

3,520 阅读6分钟

更多精彩技术文章,请关注作者的微信公众号:码工笔记

一、问题

如何通过程序测试各级 cache 的访问时延?

二、难点

  • 内存管理系统是一个复杂的系统,涉及很多相互影响的机制
  • 从外部测量需要构造能将特定 level 的 cache 访问时延独立出来的访存方案

三、Cache 的访问机制

程序访问内存的基本流程:

  • CPU 执行 load 指令时,将一个虚拟地址 v_addr 传送给指令执行部件
  • 指令执行部件将 v_addr 传给 MMU(内存管理部件)
  • MMU 收到虚拟地址 v_addr,通过页表转换成物理地址 p_addr,将之存入 TLB
    • 此页中的内存访问后续使用 TLB 来得到物理地址 p_addr,而不需要再次经过页表查询
  • MMU 用物理地址 p_addr(或虚拟地址,见下文详述) 到 L1/L2/L3 cache 中去查找,如果找到则返回 cache line 中的数据;否则去读物理内存并填充各级 cache

Cache 的作用是缓存数据,其输入是一个地址(虚拟地址或物理地址),如果这个地址的确在 Cache 中有缓存数据的话,输出就是地址所对应的物理地址存放的数据(输出粒度为一个 cache line)。

3.1 Cache line 与物理地址间的映射方式

根据物理地址与 cache 中 cache line 存放位置的映射关系不同,可以将 cache 分为以下三种:

  • 全相联映射:新 fetch 的 cache line 可以放在 cache 中的任意位置
  • 直接映射:新 fetch 的 cache line 只有一个可能的位置
  • 组相联映射(x 路组相联):新 fetch 进来的 cache line 可以放到 x 个位置中的任意一个 常见的 cache 采用组相联映射(L1/L2/L3 可能组内路数不同)。

3.2 访存步长对 Cache 访问的影响

举例:

  • Skylake L1d 采用 8 路组相联机制,其 cacheline 大小为 64 字节(占 6 bit),总大小为 32KB:

    • L1d 总大小:32KB = 32768 bytes
    • cache line 数 = 32768/line_size = 32768/64 = 512
    • 组数 = 512/组内路数 = 512/8 = 64(组)
  • Skylake 中 32KB 的 L1d 可以看作一个三维的盒子:

    • z 轴方向(向里)代表 cache line 大小,如:64 bytes
    • y 轴方向代表 cache 组内的路数,如:8 路(8 路组相联)
    • x 轴方向代表 cache 组的个数

image.png

上图中每个方格代表一个 cache line(64字节)。当访问某个物理地址(p_addr)时,会计算其所属的 cache 组序号(假设为 6),则 p_addr 所对应的一个 cache line 的数据,会被填入第 6 列的 8 个方格中的一个之中。

image.png

每个物理地址分为三部分:Tag、Index 和 Offset

image.png

其中, index 决定了 cache 组序号(上例中的 6),tag 和 index 用来唯一定位某个 cache line,offset 用来指定 cache line 中的具体字节。

从以上 L1 cache 的结构可以看出,如果访存的步长是 2^n 的形式,比如说上例中,步长是一整行(或其倍数),即多次读取都将落在同一列上。这将使 cache 的大小从 32KB 退化为 8 个 cache line(即 512 字节)。这种情况发生在 步长 = m * (index 的个数) * (cache line size) = m * 64 * 64 bytes = m * 4096 bytes (m >= 1) 的时候。

所以,要想充分利用 cache 的存储空间,避免 cache line 的频繁换出,需要设计相应的机制,保证访存步长不是 2 的大整数次幂。

3.3 Cache 查找方式分类

MMU 在接到一个内存地址后,根据其查找 cache line 时使用的地址的种类不同可分为两种查找方式:

  • VIPT(Virtually Indexted Physically Tagged)
    • 用虚拟地址中的某些位(Skylake 中的 6~11 位,所需位数对应于组数,64组需要6位)作为 cache index 来在 cache 中定位候选条目(相联组)

    • 用找出的候选条目中存放的物理地址 tag(仅高位)来与由虚拟地址经由TLB查询得到的物理地址高位 tag 相比较

    • 很多 L1 cache 都使用 VIPT,因为大多数情况下物理地址与虚拟地址的页内地址部分是相同的(4K大小的页即0~11位)

    • MMU 访问 TLB 和访问 L1 Cache 中的“VI”部分可以并行执行

    • VIPT 中的 page coloring 机制:

      • 当 cache index 位数(对应于组数) + cacheline 位数 > 页内地址所占的位数(页大小为 4KB 即 12 位),即访问 cache 时所用的 cache index 中有一部分位虚拟地址与物理地址可能不同

      • 此时如果有多个 mmap,将同一个物理地址 map 到了不同的虚拟地址,则vaddr1和 vaddr2 对应的 cache index 可能不同,导致同一个物理地址存在于 cache 中的不同 cache index 处,这可能会导致数据写丢失(比如两条 cache line 都被写入了新数据,则最后有一条会丢失)或数据不一致。

      • 解决办法:page coloring[4]

        • 假设 cache index 中有 n 位不属于页内地址部分,则每个虚拟内存页会根据n bit 的具体值被分配给 2^n 种颜色中的一种

        • 页分配器必须保证每个物理内存页只能对应同一种颜色的虚拟内存页

  • PIPT(Physically Indexed Physically Tagged)
    • 用物理地址中的某些位(cache line 以外的)作为 index 在 cache 中找查相应的组

    • 用物理地址中的其他位作为 tag 来在上一步找到的候选条目中匹配要找的行

    • L2 及其以下一般用的都是 PIPT

为什么有的 CPU 中 i-cache 是 VIPT 而 d-cache 是 PIPT?[5]

  • PIPT 更灵活,可以在不同进程间共享数据,且不需要在线程切换时做 cache flush。但它耗电多,因为它在每次访问 cache 时都需要查询 TLB,而不仅仅是 cache miss 加载进来一个 cache 行的时候。
  • i-cache 基本不需要这些优势因为指令是只读的,选用 VIPT 功耗较低。

3.4 TLB miss 对 Cache 访问的影响

L1 cache 是离 CPU 核心最近的 cache,其访问速度与寄存器相近(例:1/3)。一般会有专用于指令缓存的 L1I 和专用于数据的 L1D。

访问 L1 cache 时 CPU 需要同步访问 TLB 查询虚拟地址所对应的物理地址。

  • 使用 VIPT 机制的 L1 cache, CPU 访问 TLB 与 访问 L1 cache 的“VI”部分 可以并行执行。

但当读取数据范围大于 1 页(常见页大小为 4K 或 16K)时:

  • 与 L1 cache 同时访问的 TLB 有可能会 miss,导致测量时间包含了页表查询的时间

  • 不同页中的地址的页内偏移可能相同,可能会导致 cache 行的相互冲突(x路组相联)

四、解决方案

具体实现见 Linus Torvalds 的开源测试程序[6]。其要点包括:

  • 构造不同大小的内存数据,对内存块进行遍历,遍历方法为:本次访存读取出来的数值作为下次访存要访问的内存地址,并预先对所存内存地址值序列做随机排列处理

    • 以防止 CPU 自动预取机制生效
  • 用 cache line 大小作为步长,访问不同地址范围的内存数据(对应于上一步中构造的不同大小的内存块)

    • 如果构造的内存数据大小 < L1d,则不会发生 L1 miss

    • 如果 L1d < 内存数据大小 <= L2,则应该发生 L1 miss 而不会发生 L2 miss

    • 以此类推

  • 采用 huge-page 模式(Linux)

    • mmap 设置 madvise(map, mapsize, MADV_HUGEPAGE)

    • 保证 L2 中所有的数据都已被加载到物理内存中,不需要再进行虚拟地址->物理地址的转换,从而避免 TLB miss。

参考资料