数组大小对 Cache 访问性能影响

在知乎看到一个回答很有意思

“用C++写一个有栈协程,协程的栈设置为4096时,运行效率比4096-8或者4096+8慢了40%左右。。。。简直快要把脑袋钻到显示屏里了。。

结果努力的读了又读N-way associative cache相关的内容, 最终确定仅仅是CPU cache的问题。。还好2天之内就定位到问题了。

-----------------------------------------------------------------------------------------------------------------------

这么多人赞就分享一个相关的图(和我的协程无关,但情况完全一致,测试用例设法避免了别的因素的影响,让4K的慢显得特别明显。)”

写这篇的时候还没有意识到这个本来是计组的内容其实是计算机学生的常识,等到之后的一年学了更多东西的时候才发现,这个确实要成为常识,重复在数据库课程、操作系统课程、网络编程里面出现。

   

    所以问题就是在 page aligned 的情况下以特定步长访问连续数组的时候会出现不同的访问速度,

本文就来分析一下这个问题. 结合 CSAPP Cache 内容 以及 Intel 的实际 Cache 的一些参数来分析.

结合有一篇文章值得一看: Data alignment and caches (danluu.com)

组相联 Cache 原理两种视角

    先给出 Intel 某代处理器的 L1 Cache spec:  64 sets of 8-way associative scheme, 64 bytes per line.

    我们可以计算L1 Cache 的大小: 64*64*8 = 32,768 bytes = 32KB, 也可以换算成 8PGSIZE  (默认的 PGSIZE 应该是 4KB).

    简要复习 Cache 直接映射的思路,给 DRAM 根据块大小(一般是 4KB)分成一块一块的,假如有 4 块 Cache 块(16KB)编号 0 1 2 3,内存就分成 RAM/16KB 个组,每组(一组16KB)编号 0 1 2 3,规定只有相同编号的内存块和 Cache 块能匹配。这样做的好处是比较简单,不好是对于对齐的数据访问容易出现抖动(假如对同样的编号的数据进行访问,就会频繁地逐出和载入某个编号的 Cache 块)。而全相联的相联比较器成本较高,所以引入了组相联,思路只是简单地搞多几个并列的 Cache 编号,比如常见的 8 路组相联即搞 8 个上面说的这个 0 1 2 3 编号的 Cache 组。下面画图分析组相联。为了说明的方便,下列使用 Group 作为内存分组(直接映射的一个 Cache 块,内含多个 set)标识,对应内存地址中的 Tag;使用 Index 标识 Cache 内分块的编号。

    之后我们可以来分析 Cache 组相联的第一种视角, 即从直接映射改进法.

    考虑原来的直接映射方案, 认为是 1-way set associative, 这样每个拥有相同的 Index_i 就只能出现其中的一个, 比如这里 Group0 和 Group1 的 index 编号为 0 的两个块只能 Cache 一个, 如果我需要在 group0 和 Group1 之间来回访问, 就必定会出现频繁的逐出和加载.

    改进就是我提供多路的支持, 让每个 index 相同的都能出现 k way, 就是组相联. 我们这里是 8-ways 就说明同时能加载 8个 Group 的 Index 为 0 的块.

    第二种视角就是有8路去读取同一个编号的不同 Tag(group), 每个 Group都有64个编号.

协程栈问题分析

    接下来我们来分析协程库进行 stack 切换的问题.

    为了方便表达4K等, 这里还是用PAGE 和GROUP/TAG, INDEX 等词对应图片来说明.

    我们一开始请求很多个 PAGE 作为多个 stack 使用. 这样得到的应该是对齐的n个page(kalloc). 考虑我们运行大于8个协程, 这些协程在不断的 context switching. 根据计算我们可以得知一个 TAG(Group) 刚好就是一个 PAGE, 这样又因为 page aligned 的关系, 我们最多会保持8个(粗略估计)携程的栈在 Cache 中不会 miss (实际 Cache 不会完全用来放协程栈), 实际运行中, 我们一秒进行一亿次 context switch 的话, 由于Cache 每次都以一个 block 或者Cache line 容量64bytes进行逐出和重载, 考虑我们执行和数据,  绝对估计的情况是每 8 次切换就要 Cache 重载.

    Cache 重载又涉及3级的层次结构, 最终引发瓶颈.

    考虑如果把栈的大小设置为 Cache 一整个Group(64sets) 的大小的一半的话, 这样能同时进入Cache的协程上限会加一倍, 减少一半的装载.

    现在分析不对齐的情况, 我们图片画 2 sets 的情况, 但是仍然考虑一个 Page 占完一个 Set, 问题是等价的.

    这里看实际上的程序运行时根据局部性原理并不会一整个64sets都装的同一个协程的内容, 实际上还是交错的也就是一个Cache组64个sets会加载大于1小于64个协程的栈的一些小块.

    因为这里确实是涉及局部性的内容具体说的话会很乱, 我们理想化考虑, 先考虑Cache中加载了8个协程的栈, 而且恰好是完全载入的, 也就是载入了8个4kb栈.

    为了更好看出两种方案的区别, 我们假定协程都是在频繁访问他们的栈的同一个地方. 如果是一开始的页面对齐方案, 很容易想到每切换8个协程就要重载Cache, 我们分析其中一种可能的情况:

    前8协程内切换->载入新的8个协程->回到前8个协程, 这里对齐必须8路全部逐出再读回来. 如果是不对齐的情况, 很多都不需要逐出, 因为我们的不同协程频繁访问的那个点代表的 Index(sets内的编号)已经不是同一个了, 错开得好的时候, 这种情况, 甚至可能会出现编号分别从0到63, 这样64个协程同时出现, 减少了切换次数.

矩阵求转置问题分析

    上面例子是比较复杂的, 我们不知道他短时间内切换回Cache曾经用过的频率是多少. 很难精确描述这个性能差. 接下来再看一个矩阵转置的例子.

    为了编得更准确, 重新复习一下Intel这款处理器的L1 Cache 的参数: 64 sets of 8-way associative scheme, 64 bytes per line.

    可以计算得出一个Group的大小是 64*64=4,096 bytes, 我们假定sizeof(int) = 4, 那么就使用 1024为矩阵的边长. 这样一个以1024步长来访问的时候肯定会导致 Cache miss.

    转置的时候, 主要是二层循环, 内部 [i][j] <-> [j][i], 然后第一次有一个数组叫做B吧, 步长是内层按1024访问的, 另一个按1访问, 但是内层结束后A将回到一开始的地方按步长1增加之后再进入内层循环. 我们画图说明.

    图中红色的就是数组的访问, 荧光色是Cache的块. 复习前面对那个协程库的分析(虽然有点模糊), 不难得出, B数组的访问基本是没有利用到Cache的, 这是因为他不满足数据的局部性, 同一个块两次访问的间隔太久了(1024轮后才回访), 但是我们可以想到保留这个块用于将来访问不行吗? 实际本来是有的, 我们在访问到B第九行之前的前八行按理想情况都能保留在Cache中, 但是到第九行之前都没有回访, 第九行访问到之后, 由于这些块是对齐的拥有相同的Index, 他们会立即要求发生evict然后填充新的块.

    为了利用好 Cache 使数据保留得更久, 我们必须错开这些步长访问的块的 Index 的编号. 一种方案是之前说的位移错开, 这也就是为什么+- 都能比幂更快. 第二种方法是分块之后再转置, 等价于减少步长提高数据的局部性, 证明分块转置的正确性可以通过列转移方程来得到.

    实际可以编程序统计运行时间感受一下 4KB 相关的数据访问会出现时间上性能的差异。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值