前言代码由CPU运行。我们的代码好坏决定了CPU的执行效率。尤其是在编写计算量大的程序时,更要注意CPU的执行效率,否则会大打折扣。影响系统性能。CPUCache(高速缓存)嵌入在CPU内部。它的存储容量很小,但是非常接近CPU核心,所以缓存的读写速度极快。如果CPU计算,直接从CPUCache中读取数据,而不是从内存中读取,计算速度会非常快。然而,大多数人不了解CPUCache的运行机制,以至于不知道如何编写能够匹配CPUCache工作机制的代码。一旦掌握了,写代码的时候就会有新的优化思路。那么,让我们来看看CPUCache是??什么样子的,它是如何工作的,以及如何编写让CPU执行得更快的代码?字面上的CPU缓存有多快?你可能想知道为什么有内存还需要CPUCache?根据摩尔定律,CPU的访问速度每18个月翻一番,相当于每年增长60%左右。当然,内存的速度还会继续提升,但增速远小于CPU,年均增速只有7%左右。结果,CPU和内存访问性能之间的差距不断扩大。到目前为止,一次内存访问所需的时间超过200~300个时钟周期,这意味着CPU和内存的访问速度相差了200~300倍以上。为了弥补CPU和内存的性能差异,在CPU内部引入了CPUCache,也叫高速缓存。CPUCache通常分为大小不同的三级缓存,分别是L1Cache、L2Cache和L3Cache。由于CPUCache使用的材料是SRAM,所以价格要比内存使用的DRAM高很多。如今,生产1MBCPUCache的成本为7,而内存的成本仅为0.015。在成本方面有466的差异。次,所以CPUCache不像内存那样以GB计算,它的大小是以KB或MB计算的。在Linux系统中,我们可以通过下图来查看各级CPUCache的大小。比如我有的服务器,离CPU核心最近的L1Cache是??32KB,其次是L2Cache是??256KB,最大的L3CacheCache是??3MB。其中,L1Cache通常分为“数据缓存”和“指令缓存”,也就是说在L1Cache层面分别缓存数据和指令。上图中的index0为数据缓存,index1为指令缓存,通常大小相同。另外,你还会注意到L3Cache比L1Cache和L2Cache大很多,因为L1Cache和L2Cache是??每个CPU核心独有的,而L3Cache是??多个CPU核心共享的。程序执行时,内存中的数据会先被加载到共享的L3Cache中,然后加载到每个核心独有的L2Cache中,最后进入最快的L1Cache中才被CPU读取。它们之间的层次关系如下图所示:缓存离CPU核心越近,访问速度越快。CPU访问L1Cache只需要2到4个时钟周期,访问L2Cache大约需要10到20个时钟周期,访问L3Cache大约需要10到20个时钟周期。20~60个时钟周期,存取内存速度约为200~300个时钟周期。如下表:因此,CPU从L1Cache中读取数据的速度比从内存中读取数据的速度快100倍以上。CPUCache的数据结构和读取过程是怎样的?CPUCache的数据是从内存中读取的,它是以小块的方式读取数据,而不是按照单个数组元素来读取数据,在CPUCache中,这样一小块小块的数据称为CacheLines(缓存块)。您可以通过以下方式查看Linux系统中的CPUCacheLine。可以看到我的服务器L1CacheLine的大小是64字节,也就是说一次加载到L1Cache的数据大小是64个字。节日。比如有一个intarray[100]的数组,在加载array[0]时,由于这个数组元素的大小在内存中只占4个字节,不足64字节,所以CPU会依次加载数组元素intoarray[15],也就是说array[0]~array[15]数组元素会被缓存到CPUCache中,所以下次访问这些数组元素时,会直接从CPUCache中读取,而不是从内存中读取,这大大提高了CPU读取数据的性能。实际上,CPU读取数据时,不管数据是否存在Cache中,CPU都会先访问Cache,只有当Cache中找不到数据时,才会访问内存,读取Cache中的数据内存放入Cache中,CPU从CPUCache中读取数据。这样的访问机制与“内存作为硬盘缓存”的逻辑是一样的。如果内存中有缓存数据,则直接返回,否则需要访问慢速硬盘。CPU如何知道要访问的内存数据是否在Cache中?如果有,如何找到Cache对应的数据呢?我们先从最简单最基础的DirectMappedCache(直接映射缓存)开始,看一下整个CPUCache的数据结构和访问逻辑。前面我们提到,CPU访问内存数据时,读取的是一小块数据。这一小块数据的具体大小取决于coherency_line_size的值,一般为64字节。在内存中,这一块数据称为块,在读取时,我们需要获取数据所在块的地址。直接映射Cache采用的策略是始终将内存块的地址“映射”到一个CPULine(缓存块)的地址。至于映射关系的实现,则采用“取模运算”。模运算的结果是内存块地??址对应的CPULine(缓存块)的地址。比如内存分为32个内存块,CPUCache有8个CPULines。假设CPU要访问15号内存块,如果15号内存块中的数据已经缓存在CPULine中,那么必须映射到7号CPULine中,因为15%8的值为7.聪明的你一定发现了,如果使用模映射的方式,多个内存块会对应同一个CPULine。比如上面的例子,除了15号内存块映射到7号CPULine外,还有7号内存块,23号,31号内存块都映射到7号CPULine。因此,为了区分不同的内存块,我们还会在对应的CPULine中存储一个组标签(Tag)。这个组标志会记录当前CPULine中存储的数据对应的内存块。我们可以使用这个组标记来区分不同的内存块。除了组标签信息,CPULine还有两条信息:一条是从内存中加载的实际存储的数据(Data)。另一个是有效位(Validbit),用于标志对应CPULine中的数据是否有效。如果有效位为0,则无论CPULine中是否有数据,CPU都会直接访问内存并重新加载数据。CPU从CPUCache中读取数据时,并不是读取CPULine中的整个数据块,而是读取CPU需要的一段数据。这样的数据统称为一个词(Word)。那么如何在对应CPULine的数据块中找到需要的字呢?答案是需要一个偏移量(Offset)。因此,一个内存的访问地址包括三种信息:groupmark,CPULineindex,offset,CPU可以利用这些信息在CPUCache中找到缓存的数据。至于CPUCache中的数据结构,由索引+有效位+组标记+数据块组成。如果内存中的数据已经在CPUCahe中,当CPU访问一个内存地址时,会经过这4个步骤:根据内存地址中的索引信息,计算出CPUCahe中的索引,即找到对应的CPULine地址;找到对应的CPULine后,判断CPULine中的有效位,确认CPULine中的数据是否有效。如果无效,CPU将直接访问内存并重新加载数据。如果数据有效,则往下执行;将内存地址中的组标记与CPULine中的组标记进行比较,确认CPULine中的数据就是我们要访问的内存数据,如果不是,CPU会直接访问内存并重新加载数据,如果如果是,则向下执行;根据内存地址中的偏移信息,从CPULine的数据块中读取对应的字。至此,相信大家对直接映射Cache已经有了一定的了解,但其实除了直接映射Cache之外,还有其他策略可以通过内存地址在CPUCache中查找数据,比如全关联缓存(FullyAssociativeCache)、组关联缓存(SetAssociativeCache)等,这些策略的数据结构都比较相似。我们已经了解了直接映射Cache的工作方式。如果你有兴趣阅读其他攻略,相信你很快就能看懂。如何编写让CPU跑得更快的代码?我们知道CPU访问内存的速度要比访问CPUCache的速度慢100倍以上,所以如果CPU要操作的数据在CPUCache中,这会带来很大的性能提升改进。如果访问的数据在CPUCache中,则意味着缓存命中。缓存命中率越高,代码性能越好,CPU运行速度越快。因此,“如何编写使CPU运行得更快的代码?”的问题。可以改成“如何编写CPU缓存命中率高的代码?”。前面我也提到过,L1Cache通常分为“数据缓存”和“指令缓存”。这是因为CPU不会处理数据和指令,比如1+1=2这样的操作。在“指令缓存”中,输入的数字1会被放入“数据缓存”中。所以,我们要分别来看“数据缓存”和“指令缓存”的缓存命中率。如何提高数据缓存的命中率?假设要遍历一个二维数组,有以下两种形式。虽然代码执行结果是一样的,但是你觉得哪种形式效率最高呢?为什么高?经过测试,form1中array[i][j]的执行时间比form2中的array[j][i]快了好几倍,之所以有这么大的差距,是因为两者占用的内存维数组数组是连续的。例如,如果长度N的索引为2,那么数组元素在内存中的布局顺序如下:形式1使用array[i][j],数组元素的访问顺序是完全一样的作为数组元素存储在内存中的顺序。当CPU访问array[0][0]时,由于数据不在Cache中,所以会从内存中“依次”加载以下3个元素到CPUCache中,这样当CPU访问以下3个数组元素时当时,可以在CPUCache中成功找到数据,这意味着缓存命中率高,缓存命中的数据不需要访问内存,大大提高了代码的性能。而如果用第二种形式的array[j][i]来访问,访问的顺序是:可以看到访问的方式是跳转的,不是顺序的,那么如果N的值很大,那么运算array[j][i],没有办法将array[j+1][i]读入CPUCache,因为array[j+1][i]没有读入CPUCache,那么就需要数据元素从内存中读取。显然,这种对数据元素的不连续、跳跃式的访问可能无法充分利用CPUCache的特性,所以代码的性能并不高。访问array[0][0]元素时,CPU一次会从内存中加载多少个元素到CPUCache?这个问题,前面我们说了,跟CPUCacheLine有关,CPUCacheLine表示一次CPUCache加载数据的大小。可以通过Linux中的coherency_line_size配置查看其大小,一般为64字节。也就是说,当CPU访问内存数据时,如果数据不在CPUCache中,会一次性连续加载64字节的数据到CPUCache中,那么在访问array[0][0]时,由于到元素小于64字节,所以会依次读入array[0][0]~array[0][15]到CPUCache中。顺序访问的Array[i][j]利用了这个特性,所以它会比跳转访问的array[j][i]更快。所以,遇到这种遍历数组的情况,按照内存布局顺序访问,可以有效的利用CPUCache带来的好处,这样我们代码的性能就会有很大的提升。如何提高指令缓存的命中率?提高数据缓存命中率的方法就是按照内存布局顺序访问,那么如何提高指令的缓存呢?我们举个例子看看有一个一维数组,其元素是0到100之间的随机数:接下来对这个数组做两个操作:第一个操作,循环遍历数组,把数组中小于50的元素设置为0;第二个操作是对数组进行排序;那么问题来了,你觉得是先遍历再排序更快,还是先排序再遍历更快?在回答这个问题之前,我们先了解一下CPU的分支预测器。对于if条件语句,意味着此时至少可以选择跳转到两条不同的指令执行,即if或else中的指令。那么,如果分支预测能够预测到接下来会执行if或者else指令中的指令,就可以将这些指令“提前”放入指令缓存中,这样CPU就可以直接从Cache中读取指令,所以执行速度很快。当数组中的元素是随机的时候,分支预测就不能有效工作,而当数组中的元素是顺序的时候,分支预测器会根据历史命中数据动态预测未来,这样命中率就会高。所以,先排序再遍历会更快。这是因为排序后,数字是从小到大,所以前几次循环命中if<50的次数会更多,所以分支预测会将if[i]=0指令中的数组缓存到Cache中,后续CPU在执行指令时只需要从Cache中读取指令即可。如果确定代码中if中的表达式判断为真,我们可以使用显示分支预测工具。比如在C/C++语言中,编译器提供了likely和unlikely两个宏。如果if条件为true的概率高,可以使用likely宏将if中的表达式包裹起来,否则使用unlikely宏。其实CPU本身的动态分支预测是比较准确的,所以建议只有在非常确定CPU的预测不准确并且可以知道实际概率情况的情况下才使用这两个宏。如何提高多核CPU的缓存命中率?在单核CPU上,虽然只能执行一个进程,但操作系统会为每个进程分配一个时间片。当时间片用完后,再调度下一个进程,于是各个进程根据时间片交替占用CPU。从宏观上看,好像每个进程都在同时执行。现代CPU是多核的,进程可以在不同的CPU核之间来回切换。这对CPUCache不利。虽然L3缓存在多个内核之间共享,但L1和L2缓存对于每个内核都是唯一的。是的,如果一个进程在不同的核之间来回切换,每个核的缓存命中率都会受到影响。相反,如果进程都在同一个核上执行,则可以有效提高其数据的L1和L2Cache的缓存命中率。高缓存命中率意味着CPU可以降低访问内存的频率。当有多个“计算密集型”线程同时执行时,为了防止因为切换到不同的核心导致缓存命中率下降,我们可以将线程绑定到某个CPU核心上,这样性能就可以大大改善。相当大的改进。Linux上提供了sched_setaffinity方法来实现将一个线程绑定到某个CPU核心的功能。小结由于计算机技术的发展,CPU和内存的访问速度差距越来越大,现在差距高达数百倍。因此,CPUCache组件嵌入在CPU内部,作为内存和CPU之间的缓存层。CPUCache因为离CPU核心很近,所以访问速度也很快,但是由于需要的材料成本比较高,不像内存那么容易达到几GB大小,只有几十KB到MB大小。CPU访问数据时,首先访问CPUCache。如果缓存命中,则直接返回数据,无需每次都从内存中读取数据。因此,缓存命中率越高,代码的性能就越好。但需要注意的是,CPU访问数据时,如果CPUCache没有缓存数据,它会从内存中读取数据,但不是只读取一个数据,而是一次读取一个数据并存储在被CPU读取之前在CPU缓存中。将内存地址映射到CPUCache地址的策略有很多种。更简单的方法是直接映射Cache。它巧妙地将内存地址拆分为“索引+组标记+偏移量”,这使得我们可以将大的内存地址映射到小的CPUCache地址。如果你想写出让CPU跑得更快的代码,你就需要写出缓存命中率高的代码。CPUL1Cache分为数据缓存和指令缓存,所以需要分别提高它们的缓存命中率:对于数据缓存,我们在遍历数据时,应该按照内存布局的顺序进行操作。这是因为CPUCache是??根据CPUCacheLine对数据进行批量操作,所以在顺序操作连续内存数据时,可以有效提升性能;对于指令缓存,有规律的条件分支语句可以让CPU的分支预测器发挥作用,进一步提高执行效率;另外,对于多核CPU系统,线程可能会在不同的CPU核之间来回切换,这样每个核的缓存命中率都会受到影响,所以要提高进程的缓存命中率,可以考虑绑定线程到CPU核心。大家好,我是小林,喜欢给大家讲解计算机基础知识。如果您觉得文章对您有帮助,请分享给您的朋友,同时点击“找小林”。这对小林来说很重要,谢谢,也送给大家女士们,先生们,让我们一起牵手,下期见!推荐看我这周不知不觉输出了3篇。没有看过前2篇文章的同学们,赶快来看看吧!我的天啊!知道硬盘慢,没想到比CPUCache慢1000万倍。CPU执行程序的秘密就藏在这15张图里
