范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

这篇CPUCache,估计也没人看

  无论你写什么样的代码都会交给 CPU 来执行,所以,如果你想写出性能比较高的代码,这篇文章中提到的技术还是值得认真学习的。另外,千万别觉得这些东西没用,这些东西非常有用,十多年前就是这些知识在性能调优上帮了我的很多大忙,从而跟很多人拉开了差距……
  基础知识
  首先,我们都知道现在的 CPU 多核技术,都会有几级缓存,老的 CPU 会有两级内存(L1 和 L2),新的CPU会有三级内存(L1,L2,L3 ),如下图所示:
  其中:
  L1 缓存分成两种,一种是指令缓存,一种是数据缓存。L2 缓存和 L3 缓存不分指令和数据。
  L1 和 L2 缓存在每一个 CPU 核中,L3 则是所有 CPU 核心共享的内存。
  L1、L2、L3 的越离CPU近就越小,速度也越快,越离 CPU 远,速度也越慢。
  再往后面就是内存,内存的后面就是硬盘。我们来看一些他们的速度:
  L1 的存取速度:4 个CPU时钟周期
  L2 的存取速度:11 个CPU时钟周期
  L3 的存取速度:39 个CPU时钟周期
  RAM内存的存取速度 :107 个CPU时钟周期
  我们可以看到,L1 的速度是 RAM 的 27 倍,但是 L1/L2 的大小基本上也就是 KB 级别的,L3 会是 MB 级别的。例如:Intel Core i7-8700K ,是一个 6 核的 CPU,每核上的 L1 是 64KB(数据和指令各 32KB),L2 是 256K,L3 有 2MB(我的苹果电脑是 Intel Core i9-8950HK,和Core i7-8700K 的Cache大小一样)。
  我们的数据就从内存向上,先到 L3,再到 L2,再到 L1,最后到寄存器进行 CPU 计算。为什么会设计成三层?这里有下面几个方面的考虑:
  一个方面是物理速度,如果要更大的容量就需要更多的晶体管,除了芯片的体积会变大,更重要的是大量的晶体管会导致速度下降,因为访问速度和要访问的晶体管所在的位置成反比,也就是当信号路径变长时,通信速度会变慢。这部分是物理问题。
  另外一个问题是,多核技术中,数据的状态需要在多个CPU中进行同步,并且,我们可以看到,cache 和RAM 的速度差距太大,所以,多级不同尺寸的缓存有利于提高整体的性能。
  这个世界永远是平衡的,一面变得有多光鲜,另一面也会变得有多黑暗。建立这么多级的缓存,一定就会引入其它的问题,这里有两个比较重要的问题,
  一个是比较简单的缓存的命中率的问题。
  另一个是比较复杂的缓存更新的一致性问题。
  尤其是第二个问题,在多核技术下,这就很像分布式的系统了,要对多个地方进行更新。
  缓存的命中
  在说明这两个问题之前。我们需要要解一个术语 Cache Line。缓存基本上来说就是把后面的数据加载到离自己近的地方,对于 CPU 来说,它是不会一个字节一个字节的加载的,因为这非常没有效率,一般来说都是要一块一块的加载的,对于这样的一块一块的数据单位,术语叫 Cache Line,
  一般来说,一个主流的 CPU 的 Cache Line 是 64 Bytes(也有的CPU用32Bytes和128Bytes),64 Bytes也就是 16 个 32 位的整型,这就是 CPU 从内存中捞数据上来的最小数据单位。
  比如:Cache Line是最小单位(64Bytes),所以先把 Cache 分布多个 Cache Line,比如:L1 有 32KB,那么,32KB/64B = 512 个 Cache Line。
  一方面,缓存需要把内存里的数据放到放进来,英文叫 CPU Associativity。Cache 的数据放置的策略决定了内存中的数据块会拷贝到 CPU Cache 中的哪个位置上,因为 Cache 的大小远远小于内存,所以,需要有一种地址关联的算法,能够让内存中的数据可以被映射到 Cache 中来。这个有点像内存地址从逻辑地址向物理地址映射的方法,但不完全一样。
  基本上来说,我们会有如下的一些方法。
  一种方法是,任何一个内存地址的数据可以被缓存在任何一个 Cache Line 里,这种方法是最灵活的,但是,如果我们要知道一个内存是否存在于 Cache 中,我们就需要进行 O(n) 复杂度的 Cache 遍历,这是很没有效率的。
  另一种方法,为了降低缓存搜索算法,我们需要使用像Hash Table这样的数据结构,最简单的hash table就是做求模运算,比如:我们的 L1 Cache 有 512 个 Cache Line,那么,公式:(内存地址 mod 512)* 64就可以直接找到所在的Cache地址的偏移了。但是,这样的方式需要我们的程序对内存地址的访问要非常地平均,不然冲突就会非常严重。这成了一种非常理想的情况了。
  为了避免上述的两种方案的问题,于是就要容忍一定的hash冲突,也就出现了 N-Way 关联。也就是把连续的N 个 Cache Line 绑成一组,然后,先把找到相关的组,然后再在这个组内找到相关的 Cache Line。这叫 Set Associativity。如下图所示。
  对于 N-Way 组关联,可能有点不好理解,这里个例子,并多说一些细节(不然后面的代码你会不能理解),Intel 大多数处理器的 L1 Cache 都是 32KB,8-Way 组相联,Cache Line 是 64 Bytes。这意味着,
  32KB的可以分成,32KB / 64 = 512 条 Cache Line。
  因为有8 Way,于是会每一Way 有 512 / 8 = 64 条 Cache Line。
  于是每一路就有 64 x 64 = 4096 Byts 的内存。
  为了方便索引内存地址,
  Tag:每条 Cache Line 前都会有一个独立分配的 24 bits来存的 tag,其就是内存地址的前24bits
  Index:内存地址后续的 6 个 bits 则是在这一 Way 的是Cache Line 索引,2^6 = 64 刚好可以索引64条Cache Line
  Offset:再往后的 6bits 用于表示在 Cache Line 里的偏移量
  如下图所示:(图片来自《Cache: a place for concealment and safekeeping》)
  当拿到一个内存地址的时候,先拿出中间的 6bits 来,找到是哪组。
  然后,在这一个 8 组的 cache line 中,再进行 O(n) n=8 的遍历,主是要匹配前 24bits 的 tag。如果匹配中了,就算命中,如果没有匹配到,那就是 cache miss,如果是读操作,就需要进向后面的缓存进行访问了。
  L2/L3 同样是这样的算法。而淘汰算法有两种,一种是随机一种是 LRU。现在一般都是以 LRU 的算法(通过增加一个访问计数器来实现)
  这也意味着:
  L1 Cache 可映射 36bits 的内存地址,一共 2^36 = 64GB 的内存
  当 CPU 要访问一个内存的时候,通过这个内存中间的 6bits 定位是哪个 set,通过前 24bits 定位相应的Cache Line。
  就像一个 hash Table 的数据结构一样,先是 O(1)的索引,然后进入冲突搜索。
  因为中间的 6bits 决定了一个同一个 set,所以,对于一段连续的内存来说,每隔 4096 的内存会被放在同一个组内,导致缓存冲突。
  此外,当有数据没有命中缓存的时候,CPU 就会以最小为 Cache Line 的单元向内存更新数据。当然,CPU 并不一定只是更新 64Bytes,因为访问主存实在是太慢了,所以,一般都会多更新一些。好的 CPU 会有一些预测的技术,如果找到一种 pattern 的话,就会预先加载更多的内存,包括指令也可以预加载。
  这叫 Prefetching 技术 (参看,Wikipedia 的 Cache Prefetching 和 纽约州立大学的 Memory Prefetching)。比如,你在for-loop访问一个连续的数组,你的步长是一个固定的数,内存就可以做到prefetching。(注:指令也是以预加载的方式执行)
  了解这些细节,会有利于我们知道在什么情况下有可以导致缓存的失效。
  缓存的一致性
  对于主流的 CPU 来说,缓存的写操作基本上是两种策略,
  一种是 Write Back,写操作只要在 cache 上,然后再 flush 到内存上。
  一种是 Write Through,写操作同时写到 cache 和内存上。
  为了提高写的性能,一般来说,主流的 CPU(如:Intel Core i7/i9)采用的是 Write Back 的策略,因为直接写内存实在是太慢了。
  好了,现在问题来了,如果有一个数据 x 在 CPU 第 0 核的缓存上被更新了,那么其它 CPU 核上对于这个数据 x 的值也要被更新,这就是缓存一致性的问题。(当然,对于我们上层的程序我们不用关心 CPU 多个核的缓存是怎么同步的,这对上层的代码来说都是透明的)
  一般来说,在 CPU 硬件上,会有两种方法来解决这个问题。
  Directory 协议。这种方法的典型实现是要设计一个集中式控制器,它是主存储器控制器的一部分。其中有一个目录存储在主存储器中,其中包含有关各种本地缓存内容的全局状态信息。当单个 CPU Cache 发出读写请求时,这个集中式控制器会检查并发出必要的命令,以在主存和 CPU Cache之间或在 CPU Cache自身之间进行数据同步和传输。
  Snoopy 协议。这种协议更像是一种数据通知的总线型的技术。CPU Cache 通过这个协议可以识别其它Cache上的数据状态。如果有数据共享的话,可以通过广播机制将共享数据的状态通知给其它 CPU Cache。这个协议要求每个 CPU Cache 都可以窥探数据事件的通知并做出相应的反应。如下图所示,有一个 Snoopy Bus 的总线。
  因为 Directory 协议是一个中心式的,会有性能瓶颈,而且会增加整体设计的复杂度。而 Snoopy 协议更像是微服务+消息通讯,所以,现在基本都是使用 Snoopy 的总线的设计。
  这里,我想多写一些细节,因为这种微观的东西,让人不自然地就会跟分布式系统关联起来,在分布式系统中我们一般用 Paxos/Raft 这样的分布式一致性的算法。
  而在 CPU 的微观世界里,则不必使用这样的算法,原因是因为 CPU 的多个核的硬件不必考虑网络会断会延迟的问题。所以,CPU 的多核心缓存间的同步的核心就是要管理好数据的状态就好了。
  这里介绍几个状态协议,先从最简单的开始,MESI 协议,这个协议跟那个著名的足球运动员梅西没什么关系,其主要表示缓存数据有四个状态:Modified(已修改), Exclusive(独占的),Shared(共享的),Invalid(无效的)。
  这些状态的状态机如下所示(有点复杂,你可以先不看,这个图就是想告诉你状态控制有多复杂):
  下面是个示例(如果你想看一下动画演示的话,这里有一个网页(MESI Interactive Animations),你可以进行交互操作,这个动画演示中使用的 Write Through 算法):当前操作CPU0CPU1Memory说明1) CPU0 read(x)x=1 (E)
  x=1只有一个CPU有 x 变量, 所以,状态是 Exclusive2) CPU1 read(x)x=1 (S)x=1(S)x=1有两个CPU都读取 x 变量, 所以状态变成 Shared3) CPU0 write(x,9)x=9 (M)x=1(I)x=1变量改变,在CPU0中状态 变成 Modified,在CPU1中 状态变成 Invalid4) 变量 x 写回内存x=9 (M)X=1(I)x=9目前的状态不变5) CPU1 read(x)x=9 (S)x=9(S)x=9变量同步到所有的Cache中, 状态回到Shared
  MESI 这种协议在数据更新后,会标记其它共享的 CPU 缓存的数据拷贝为 Invalid 状态,然后当其它 CPU 再次read 的时候,就会出现 cache miss 的问题,此时再从内存中更新数据。从内存中更新数据意味着 20 倍速度的降低。
  我们能不能直接从我隔壁的 CPU 缓存中更新?是的,这就可以增加很多速度了,但是状态控制也就变麻烦了。还需要多来一个状态:Owner(宿主),用于标记,我是更新数据的源。于是,出现了 MOESI 协议
  MOESI 协议的状态机和演示示例我就不贴了(有兴趣可以上Berkeley上看看相关的课件),我们只需要理解MOESI协议允许 CPU Cache 间同步数据,于是也降低了对内存的操作,性能是非常大的提升,但是控制逻辑也非常复杂。
  顺便说一下,与 MOESI 协议类似的一个协议是 MESIF,其中的 F 是 Forward,同样是把更新过的数据转发给别的 CPU Cache 但是,MOESI 中的 Owner 状态 和MESIF 中的 Forward 状态有一个非常大的不一样—— Owner 状态下的数据是 dirty 的,还没有写回内存,Forward 状态下的数据是 clean的,可以丢弃而不用另行通知。
  需要说明的是,AMD 用 MOESI,Intel 用 MESIF。所以,F 状态主要是针对 CPU L3 Cache 设计的(前面我们说过,L3 是所有 CPU 核心共享的)。(相关的比较可以参看StackOverlow上这个问题的答案)
  程序性能
  了解了我们上面的这些东西后,我们来看一下对于程序的影响。
  示例一
  首先,假设我们有一个64M长的数组,设想一下下面的两个循环: const int LEN = 64*1024*1024;
  int *arr = new int[LEN];
  for (int i = 0; i < LEN; i += 2) arr[i] *= i;
  for (int i = 0; i < LEN; i += 8) arr[i] *= i;
  按我们的想法来看,第二个循环要比第一个循环少4倍的计算量,其应该也是要快4倍的。但实际跑下来并不是,在我的机器上,第一个循环需要 127 毫秒,第二个循环则需要 121 毫秒,相差无几。
  这里最主要的原因就是 Cache Line,因为 CPU 会以一个 Cache Line 64Bytes 最小时单位加载,也就是 16 个 32bits 的整型,所以,无论你步长是 2 还是 8,都差不多。而后面的乘法其实是不耗 CPU 时间的。
  示例二
  我们再来看一个与缓存命中率有关的代码,我们以一定的步长increment来访问一个连续的数组。 for (int i = 0; i < 10000000; i++) {
  for (int j = 0; j < size; j += increment) {
  memory[j] += j;
  }
  }
  我们测试一下,在下表中, 表头是步长,也就是每次跳多少个整数,而纵向是这个数组可以跳几次(你可以理解为要几条 Cache Line),于是表中的任何一项代表了这个数组有多少,而且步长是多少。
  比如:横轴是 512,纵轴是4,意思是,这个数组有 4*512 = 2048个长度,访问时按512步长访问,也就是访问其中的这几项:[0, 512, 1024, 1536]这四项。
  表中同的项是,是循环 1000 万次的时间,单位是"微秒"(除以1000后是毫秒) | count | 1 | 16 | 512 | 1024 |
  ------------------------------------------
  | 1 | 17539 | 16726 | 15143 | 14477 |
  | 2 | 15420 | 14648 | 13552 | 13343 |
  | 3 | 14716 | 14463 | 15086 | 17509 |
  | 4 | 18976 | 18829 | 18961 | 21645 |
  | 5 | 23693 | 23436 | 74349 | 29796 |
  | 6 | 23264 | 23707 | 27005 | 44103 |
  | 7 | 28574 | 28979 | 33169 | 58759 |
  | 8 | 33155 | 34405 | 39339 | 65182 |
  | 9 | 37088 | 37788 | 49863 |156745 |
  | 10 | 41543 | 42103 | 58533 |215278 |
  | 11 | 47638 | 50329 | 66620 |335603 |
  | 12 | 49759 | 51228 | 75087 |305075 |
  | 13 | 53938 | 53924 | 77790 |366879 |
  | 14 | 58422 | 59565 | 90501 |466368 |
  | 15 | 62161 | 64129 | 90814 |525780 |
  | 16 | 67061 | 66663 | 98734 |440558 |
  | 17 | 71132 | 69753 |171203 |506631 |
  | 18 | 74102 | 73130 |293947 |550920 |
  我们可以看到,从 [9,1024]以后,时间显著上升。包括[17,512]和[18,512]也显著上升。这是因为,我机器的 L1 Cache 是 32KB, 8 Way 的,前面说过,8 Way 的有 64 组,每组 8 个 Cache Line,当 for-loop步长超过 1024 个整型,也就是正好 4096 Bytes 时,也就是导致内存地址的变化是变化在高位的 24bits 上,
  而低位的1 2bits 变化不大,尤其是中间6bits没有变化,导致全部命中同一组 set,导致大量的 cache 冲突,导致性能下降,时间上升。而 [16, 512]也是一样的,其中的几步开始导致L1 Cache开始冲突失效。
  示例三
  接下来,我们再来看个示例。下面是一个二维数组的两种遍历方式,一个逐行遍历,一个是逐列遍历,这两种方式在理论上来说,寻址和计算量都是一样的,执行时间应该也是一样的。 const int row = 1024;
  const int col = 512
  int matrix[row][col];
  //逐行遍历
  int sum_row=0;
  for(int _r=0; _r
终于要来了!潘粤明张雨绮昆仑神宫发布预告,铁三角集结凤凰神宫灾难之海,远古秘境险象环生。由潘粤明张雨绮姜超主演的昆仑神宫发布全员小心版预告,铁三角深入昆仑雪域,唤醒恶罗海城古老生物,在离奇诡异的地下世界展开殊死搏斗,惊险的画面逼真的糊了凉了丑了,8届金鹰女神,只有3人红到如今二年一度的金鹰节颁奖礼又来了,除了每年的奖项备受关注外,金鹰女神也是各位网民讨论的对象,然而时过境迁,已经选出的8位金鹰女神现在发展也各不相同,差距明显。1刘亦菲刘亦菲19岁就已经张纪中被娇妻训得跟孙子似的,只因卖货帮倒忙,亏惨了著名导演张纪中最近在直播间被娇妻骂得跟孙子似的。看到娇妻一脸怒气,他大气不敢出,任凭责骂!到底发生了什么?原来当天直播卖货之时,张纪中看到娇妻忙里忙外太辛苦,自己一把岁数了也不能指是枝裕和的自我重复与水土不服李宁作为忠实的是枝裕和粉丝,我不得不承认,掮客是一部令人失望的作品。影片延续了是枝裕和在如父如子海街日记小偷家族等前作中对于临时家庭社会底层等议题的热衷,镜头语言也是一贯的克制静观罗志祥高调复出,要把失去的全部拿回来,还要到上海开演唱会?7月9日,沉寂两年的时间管理大师罗志祥,终于按捺不住了。他在小巨蛋开演唱会,高调宣布复出。而且,现场还喊出了这样一句豪言壮语我会把我失去的,全部拿回来!罗志祥在喊这句话的时候,摩拳基于SpringBootMybatisPlusVue的开源自定义表单问卷系统真正的大师,永远都怀着一颗学徒的心!一项目简介今天说的这个软件是一款基于SpringBootMybatisPlusVue的开源自定义问卷系统。二实现功能公开反馈结果,即可以看到他人这一波助燃剂,有望拉动5000亿元汽车消费新京智库不同地区的产业结构不同经济发展程度不同,地方政府扶持的表现形式和重点领域也有所差别。北京市昌平区发放1000万元汽车消费券,针对不同购车需求设置了额度不等的购车补贴,可通过微信小程还差106万桶日,欧佩克6月产油量仍未达标!明年油市依然吃紧?OPEC将今年全球经济增长预期从3。5下调至3。2,这意味着石油需求可能下降。OPEC对2023年石油市场的首次展望表明,油市吃紧并未得到缓解,尽管多数成员国已经开足马力增产,但该诺基亚发布新机,设计再次惊艳所有人说到诺基亚这个品牌,可谓充满了很多人关于青春的回忆!曾经的诺基亚就像如今的苹果一样,是手机界王者级的存在,街上每10个人手中必有9个人用的都是诺基亚手机。当时的诺基亚手机不仅销量好宋佳滨让户外也会有在家的感觉要说当代青年最潮最火的生活方式,露营绝对算其中之一。几乎每个周末或是假期,你都会在你的朋友圈刷到那么几个去露营的朋友,他们三五成群,背上行囊带上宠物,在自然的环抱中涮着火锅吃着烧烤OPPOA97,搭载天玑810,12256G顶配1999元,大家感觉怎么样?我原以为绿厂看到蓝厂出了一个Y77,会把A系列的配置稍微往上调一调,结果还是么样子,不过相对于之前的a系列来说还连场凑合,但是对于这款手机的配置我属实不敢恭维。我们来简单了解一下吧
17岁K宝变成熟了!恋爱后有女人味,身材日趋丰满,近照好迷人近期,俄罗斯花滑名将K宝瓦利耶娃(KamilaValieva)抵达雅库茨克,参加GTO比赛开幕式及其相关活动,成为第三届青年节穆斯乌斯塔的特邀嘉宾,现年17岁的瓦利耶娃愈发成熟,有高级并且有内涵的清醒文案博主巨宝藏的朋友圈1。怀疑一旦产生,罪名便会成立。2。生于尘埃,溺于人海,死于理想高台。3。野生的玫瑰才不会爱上温室里的雏菊。4。后来烟雨落盛京,一人撑伞两人行。5。你对我皮囊失望女人的身体,为什么大多数都是白白嫩嫩软软的?女人的身体,为什么大多数都是白白嫩嫩软软的?女人的肌肤常常被称为柔软白嫩,这种描述可能会被人们认为与女人的性别有关。然而,科学研究表明,肌肤的颜色和柔软度是由多个因素共同决定的。首全上海都在出游?人头比花瓣多,到处有挨骂的男人ampampquot2小时高铁圈ampampquot成新热门!最近的几个周末朋友圈久违地热闹起来上海人都出门白相啦!赏花踏青晒太阳花花摄影大赛又开始了!赏花游玩人比花多?上海迪士尼乐园的人流从来不会让人失望在徐汇滨江赏花人比树还多!在顾村公园为什么别的男人那样有魅力?原因很简单,他们做到了以下这些事男性身体健康,是人们一直关注和重视的话题。然而,很多男性却对自己的健康状况不够重视,日常生活中也存在不良的生活习惯和错误的健康认知。这些不良习惯和错误认知,往往会影响男性身体健康,费雯丽是我见过最端庄的女人裙长而不拖,发卷而不乱,真美究竟是怎样的美貌才能让奥斯卡评委也感慨她有如此美貌,根本不必有如此演技?看了费雯丽才知道,原来真的能有人用美貌颠倒众生。费雯丽是典型的猫系美人,当你还沉浸在她的美貌中时,早有人开始那个男人,回来了本文是老石谈芯全球芯闻栏目,每周一到周五早七点半发布老石带你总览全球大小芯闻,欢迎关注!一芯知精选马云回来了,聊了ChatGPT教育,还有啥?(中国企业家杂志)JackMa回归,相女人就要把个性张扬出来每个人都有自己的个性。个性是一个人区别另一个人的标志之一。个性为也会产生魅力。张扬个性,特别是把自己迷人的个性展示出来,是一个女人掌握的生活细节之一。现代女性都希望自己活得潇洒,活女人旺自己的8个习惯,坚持下去头条创作挑战赛其实你想成为什么样的人,靠的是自己,别人帮不了你。可能有人会说嫁一个好男人就可以衣食无忧,旺自己。但事实上旺自己的人不是他人而是自己,只有自己足够优秀足够努力才能过好男孩比女孩难带的原因是什么?可能是我们的了解还不够多陪伴男孩成长的道路,注定会和女孩有所不同。父母需要了解男孩的特质,对症下药带着温柔和耐心,静静等待他长成优秀独立的男子汉。虎妈作者兰妈来源兰妈谈育儿(头条号)开学的前一天晚上,儿子奋进春天里!苏州高铁新城晒出企业敢干精气神一年春为首,新城沃土春意盎然生机勃勃,不负春光奋力奔跑。苏州高铁新城多家智能网联汽车企业项目入选签约合作创新研发,进展频频,他们的朋友圈晒出了,企业敢干的精气神。3月捷报!3篇来自