learn-tech/专栏/深入浅出计算机组成原理/37理解CPUCache(上):“4毫秒”究竟值多少钱?.md
2024-10-16 09:22:22 +08:00

129 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
37 理解CPU Cache“4毫秒”究竟值多少钱
在这一节内容开始之前,我们先来看一个 3 行的小程序。你可以猜一猜,这个程序里的循环 1 和循环 2运行所花费的时间会差多少你可以先思考几分钟然后再看我下面的解释。
int[] arr = new int[64 * 1024 * 1024];
// 循环 1
for (int i = 0; i < arr.length; i++) arr[i] *= 3;
// 循环 2
for (int i = 0; i < arr.length; i += 16) arr[i] *= 3
在这段 Java 程序中我们首先构造了一个 64×1024×1024 大小的整型数组在循环 1 我们遍历整个数组将数组中每一项的值变成了原来的 3 在循环 2 我们每隔 16 个索引访问一个数组元素将这一项的值变成了原来的 3
按道理来说循环 2 只访问循环 1 116 的数组元素只进行了循环 1 116 的乘法计算那循环 2 花费的时间应该是循环 1 116 左右但是实际上循环 1 在我的电脑上运行需要 50 毫秒循环 2 只需要 46 毫秒这两个循环花费时间之差在 15% 之内
为什么会有这 15% 的差异呢这和我们今天要讲的 CPU Cache 有关之前我们看到了内存和硬盘之间存在的巨大性能差异 CPU 眼里内存也慢得不行于是聪明的工程师们就在 CPU 里面嵌入了 CPU Cache高速缓存来解决这一问题
我们为什么需要高速缓存?
按照摩尔定律CPU 的访问速度每 18 个月便会翻一番相当于每年增长 60%。内存的访问速度虽然也在不断增长却远没有这么快每年只增长 7% 左右而这两个增长速度的差异使得 CPU 性能和内存访问性能的差距不断拉大到今天来看一次内存的访问大约需要 120 CPU Cycle这也意味着在今天CPU 和内存的访问速度已经有了 120 倍的差距
如果拿我们现实生活来打个比方的话CPU 的速度好比风驰电掣的高铁每小时 350 公里然而它却只能等着旁边腿脚不太灵便的老太太也就是内存以每小时 3 公里的速度缓慢步行因为 CPU 需要执行的指令需要访问的数据都在这个速度不到自己 1% 的内存里
随着时间变迁CPU 和内存之间的性能差距越来越大
为了弥补两者之间的性能差异我们能真实地把 CPU 的性能提升用起来而不是让它在那儿空转我们在现代 CPU 中引入了高速缓存
CPU Cache 被加入到现有的 CPU 里开始内存中的指令数据会被加载到 L1-L3 Cache 而不是直接由 CPU 访问内存去拿 95% 的情况下CPU 都只需要访问 L1-L3 Cache从里面读取指令和数据而无需访问内存要注意的是这里我们说的 CPU Cache 或者 L1/L3 Cache不是一个单纯的概念上的缓存比如之前我们说的拿内存作为硬盘的缓存而是指特定的由 SRAM 组成的物理芯片
这里是一张 Intel CPU 的放大照片这里面大片的长方形芯片就是这个 CPU 使用的 20MB L3 Cache
现代 CPU 中大量的空间已经被 SRAM 占据图中用红色框出的部分就是 CPU L3 Cache 芯片
在这一讲一开始的程序里运行程序的时间主要花在了将对应的数据从内存中读取出来加载到 CPU Cache CPU 从内存中读取数据到 CPU Cache 的过程中是一小块一小块来读取数据的而不是按照单个数组元素来读取数据的这样一小块一小块的数据 CPU Cache 里面我们把它叫作 Cache Line缓存块)。
在我们日常使用的 Intel 服务器或者 PC Cache Line 的大小通常是 64 字节而在上面的循环 2 里面我们每隔 16 个整型数计算一次16 个整型数正好是 64 个字节于是循环 1 和循环 2需要把同样数量的 Cache Line 数据从内存中读取到 CPU Cache 最终两个程序花费的时间就差别不大了
知道了为什么需要 CPU Cache接下来我们就来看一看CPU 究竟是如何访问 CPU Cache 以及 CPU Cache 是如何组织数据使得 CPU 可以找到自己想要访问的数据的因为 Cache 作为缓存的意思在很多别的存储设备里面都会用到为了避免你混淆在表示抽象的缓存概念时用中文的缓存”;如果是 CPU Cache我会用高速缓存或者英文的Cache”,来表示
Cache 的数据结构和读取过程是什么样的
现代 CPU 进行数据读取的时候无论数据是否已经存储在 Cache CPU 始终会首先访问 Cache只有当 CPU Cache 中找不到数据的时候才会去访问内存并将读取到的数据写入 Cache 之中当时间局部性原理起作用后这个最近刚刚被访问的数据会很快再次被访问 Cache 的访问速度远远快于内存这样CPU 花在等待内存访问上的时间就大大变短了
这样的访问机制和我们自己在开发应用系统的时候,“使用内存作为硬盘的缓存的逻辑是一样的在各类基准测试Benchmark和实际应用场景中CPU Cache 的命中率通常能达到 95% 以上
问题来了CPU 如何知道要访问的内存数据存储在 Cache 的哪个位置呢接下来我就从最基本的直接映射 CacheDirect Mapped Cache说起带你来看整个 Cache 的数据结构和访问逻辑
在开头的 3 行小程序里我说过CPU 访问内存数据是一小块一小块数据来读取的对于读取内存中的数据我们首先拿到的是数据所在的内存块Block的地址而直接映射 Cache 采用的策略就是确保任何一个内存块的地址始终映射到一个固定的 CPU Cache 地址Cache Line)。而这个映射关系通常用 mod 运算求余运算来实现下面我举个例子帮你理解一下
比如说我们的主内存被分成 031 号这样 32 个块我们一共有 8 个缓存块用户想要访问第 21 号内存块如果 21 号内存块内容在缓存块中的话它一定在 5 号缓存块21 mod 8 = 5中。
Cache 采用 mod 的方式把内存块映射到对应的 CPU Cache
实际计算中有一个小小的技巧通常我们会把缓存块的数量设置成 2 N 次方这样在计算取模的时候可以直接取地址的低 N 也就是二进制里面的后几位比如这里的 8 个缓存块就是 2 3 次方那么在对 21 取模的时候可以对 21 2 进制表示 10101 取地址的低三位也就是 101对应的 5就是对应的缓存块地址
Block 地址的低位就能得到对应的 Cache Line 地址除了 21 号内存块外13 5 号等很多内存块的数据都对应着 5 号缓存块中既然如此假如现在 CPU 想要读取 21 号内存块在读取到 5 号缓存块的时候我们怎么知道里面的数据究竟是不是 21 号对应的数据呢同样建议你借助现有知识先自己思考一下然后再看我下面的分析这样会印象比较深刻
这个时候在对应的缓存块中我们会存储一个组标记Tag)。这个组标记会记录当前缓存块内存储的数据对应的内存块而缓存块本身的地址表示访问地址的低 N 就像上面的例子21 的低 3 101缓存块本身的地址已经涵盖了对应的信息对应的组标记我们只需要记录 21 剩余的高 2 位的信息也就是 10 就可以了
除了组标记信息之外缓存块中还有两个数据一个自然是从主内存中加载来的实际存放的数据另一个是有效位valid bit)。啥是有效位呢它其实就是用来标记对应的缓存块中的数据是否是有效的确保不是机器刚刚启动时候的空数据如果有效位是 0无论其中的组标记和 Cache Line 里的数据内容是什么CPU 都不会管这些数据而要直接访问内存重新加载数据
CPU 在读取数据的时候并不是要读取一整个 Block而是读取一个他需要的整数这样的数据我们叫作 CPU 里的一个字Word)。具体是哪个字就用这个字在整个 Block 里面的位置来决定这个位置我们叫作偏移量Offset)。
总结一下一个内存的访问地址最终包括高位代表的组标记低位代表的索引以及在对应的 Data Block 中定位对应字的位置偏移量
内存地址到 Cache Line 的关系
而内存地址对应到 Cache 里的数据结构则多了一个有效位和对应的数据索引 + 有效位 + 组标记 + 数据组成如果内存中的数据已经在 CPU Cache 里了那一个内存地址的访问就会经历这样 4 个步骤
根据内存地址的低位计算在 Cache 中的索引
判断有效位确认 Cache 中的数据是有效的
对比内存访问地址的高位 Cache 中的组标记确认 Cache 中的数据就是我们要访问的内存数据 Cache Line 中读取到对应的数据块Data Block
根据内存地址的 Offset Data Block 读取希望读取到的字
如果在 23 这两个步骤中CPU 发现Cache 中的数据并不是要访问的内存地址的数据 CPU 就会访问内存并把对应的 Block Data 更新到 Cache Line 同时更新对应的有效位和组标记的数据
好了讲到这里相信你明白现代 CPU是如何通过直接映射 Cache来定位一个内存访问地址在 Cache 中的位置了其实除了直接映射 Cache 之外我们常见的缓存放置策略还有全相连 CacheFully Associative Cache)、组相连 CacheSet Associative Cache)。这几种策略的数据结构都是相似的理解了最简单的直接映射 Cache其他的策略你很容易就能理解了
减少 4 毫秒公司挣了多少钱?
刚才我花了很多篇幅讲了 CPU 和内存之间的性能差异以及我们如何通过 CPU Cache 来尽可能解决这两者之间的性能鸿沟你可能要问了这样做的意义和价值究竟是什么毕竟一次内存的访问只不过需要 100 纳秒而已1 秒钟时间内足有 1000 万个 100 纳秒别着急我们先来看一个故事
2008 一家叫作 Spread Networks 的通信公司花费 3 亿美元做了一个光缆建设项目目标是建设一条从芝加哥到新泽西总长 1331 公里的光缆线路建设这条线路的目的其实是为了将两地之间原有的网络访问延时 17 毫秒降低到 13 毫秒
你可能会说仅仅缩短了 4 毫秒时间啊却花费 3 个亿真的值吗为这 4 毫秒时间买单的其实是一批高频交易公司它们以 5 1400 万美元的价格使用这条线路利用这短短的 4 毫秒的时间优势这些公司通过高性能的计算机程序在芝加哥和新泽西两地的交易所进行高频套利以获得每年以 10 亿美元计的利润现在你还觉得这个不值得吗
其实只要 350 微秒的差异就足够高频交易公司用来进行无风险套利了 350 微秒如果用来进行 100 纳秒一次的内存访问大约只够进行 3500 而引入 CPU Cache 之后我们可以进行的数据访问次数提升了数十倍使得各种交易策略成为可能
总结延伸
很多时候程序的性能瓶颈来自使用 DRAM 芯片的内存访问速度
根据摩尔定律自上世纪 80 年代以来CPU 和内存的性能鸿沟越拉越大于是现代 CPU 的设计者们直接在 CPU 中嵌入了使用更高性能的 SRAM 芯片的 Cache来弥补这一性能差异通过巧妙地将内存地址拆分成索引 + 组标记 + 偏移量的方式使得我们可以将很大的内存地址映射到很小的 CPU Cache 地址里 CPU Cache 带来的毫秒乃至微秒级别的性能差异又能带来巨大的商业利益十多年前的高频交易行业就是最好的例子
在搞清楚从内存加载数据到 Cache以及从 Cache 里读取到想要的数据之后我们又要面临一个新的挑战了CPU 不仅要读数据还需要写数据我们不能只把数据写入到 Cache 里面就结束了下一讲我们就来仔细讲讲CPU 要写入数据的时候怎么既不牺牲性能又能保证数据的一致性
推荐阅读
如果你学有余力这里有两篇文章推荐给你阅读
如果想深入了解 CPU 和内存之间的访问性能你可以阅读What Every Programmer Should Know About Memory
现代 CPU 已经很少使用直接映射 Cache 通常用的是组相连 Cacheset associative cache想要了解组相连 Cache你可以阅读计算机组成与设计硬件 / 软件接口 5.4.1 小节