CSDN|如何写出让 CPU 跑得更快的代码?( 二 )


你可以在你的 Linux 系统 , 用下面这种方式来查看 CPU 的 Cache Line , 你可以看我服务器的 L1 Cache Line 大小是 64 字节 , 也就意味着 L1 Cache 一次载入数据的大小是 64 字节 。
CSDN|如何写出让 CPU 跑得更快的代码?
本文插图
比如 , 有一个int array[100]的数组 , 当载入array[0]时 , 由于这个数组元素的大小在内存只占 4 字节 , 不足 64 字节 , CPU 就会顺序加载数组元素到array[15] , 意味着 array[0]~array[15] 数组元素都会被缓存在 CPU Cache 中了 , 因此当下次访问这些数组元素时 , 会直接从 CPU Cache 读取 , 而不用再从内存中读取 , 大大提高了 CPU 读取数据的性能 。
事实上 , CPU 读取数据的时候 , 无论数据是否存放到 Cache 中 , CPU 都是先访问 Cache , 只有当 Cache 中找不到数据时 , 才会去访问内存 , 并把内存中的数据读入到 Cache 中 , CPU 再从 CPU Cache 读取数据 。
CSDN|如何写出让 CPU 跑得更快的代码?
本文插图
这样的访问机制 , 跟我们使用「内存作为硬盘的缓存」的逻辑是一样的 , 如果内存有缓存的数据 , 则直接返回 , 否则要访问龟速一般的硬盘 。
那 CPU 怎么知道要访问的内存数据 , 是否在 Cache 里?如果在的话 , 如何找到 Cache 对应的数据呢?我们从最简单、基础的直接映射 Cache(Direct Mapped Cache) 说起 , 来看看整个 CPU Cache 的数据结构和访问逻辑 。
前面 , 我们提到 CPU 访问内存数据时 , 是一小块一小块数据读取的 , 具体这一小块数据的大小 , 取决于 coherency_line_size 的值 , 一般 64 字节 。 在内存中 , 这一块的数据我们称为内存块(Bock) , 读取的时候我们要拿到数据所在内存块的地址 。
对于直接映射 Cache 采用的策略 , 就是把内存块的地址始终「映射」在一个 CPU Line(缓存块) 的地址 , 至于映射关系实现方式 , 则是使用「取模运算」 , 取模运算的结果就是内存块地址对应的 CPU Line(缓存块) 的地址 。
举个例子 , 内存共被划分为 32 个内存块 , CPU Cache 共有 8 个 CPU Line , 假设 CPU 想要访问第 15 号内存块 , 如果 15 号内存块中的数据已经缓存在 CPU Line 中的话 , 则是一定映射在 7 号 CPU Line 中 , 因为 15 % 8 的值是 7 。
机智的你肯定发现了 , 使用取模方式映射的话 , 就会出现多个内存块对应同一个 CPU Line , 比如上面的例子 , 除了 15 号内存块是映射在 7 号 CPU Line 中 , 还有 7 号、23 号、31 号内存块都是映射到 7 号 CPU Line 中 。
CSDN|如何写出让 CPU 跑得更快的代码?
本文插图
因此 , 为了区别不同的内存块 , 在对应的 CPU Line 中我们还会存储一个组标记(Tag) 。 这个组标记会记录当前 CPU Line 中存储的数据对应的内存块 , 我们可以用这个组标记来区分不同的内存块 。除了组标记信息外 , CPU Line 还有两个信息:

  • 一个是 , 从内存加载过来的实际存放数据(Data) 。
  • 另一个是 , 有效位(Valid bit) , 它是用来标记对应的 CPU Line 中的数据是否是有效的 , 如果有效位是 0 , 无论 CPU Line 中是否有数据 , CPU 都会直接访问内存 , 重新加载数据 。
CPU 在从 CPU Cache 读取数据的时候 , 并不是读取 CPU Line 中的整个数据块 , 而是读取 CPU 所需要的一个数据片段 , 这样的数据统称为一个字(Word) 。 那怎么在对应的 CPU Line 中数据块中找到所需的字呢?答案是 , 需要一个偏移量(Offset) 。因此 , 一个内存的访问地址 , 包括组标记、CPU Line 索引、偏移量这三种信息 , 于是 CPU 就能通过这些信息 , 在 CPU Cache 中找到缓存的数据 。 而对于 CPU Cache 里的数据结构 , 则是由索引 + 有效位 + 组标记 + 数据块组成 。


推荐阅读