大大多数读者都清楚缓存是一个用于存储比来拜访的内存地位的高速小容量存储器。这个描绘是相当精确的,可是处置器缓存怎么任务的“无聊”细节可以更好的协助我们了解程序的功能。
在这篇博客中,我会用代码示例来讲明缓存任务的各个方面及其在实践运转中对程序功能的影响。
这些示例是用C#写的,但分歧言语的完成将会对功能的得分和得出的结论有一些小的影响。
你以为轮回2的运转时间比轮回1要快几多?
int[] arr = new int[64 * 1024 * 1024]; // Loop 1 for (int i = 0; i < arr.Length; i++) arr[i] *= 3; // Loop 2 for (int i = 0; i < arr.Length; i += 16) arr[i] *= 3;
轮回1将数组中的每一个值都乘以3,轮回2将数组中每隔16个元素的值乘以3。第二个轮回仅仅只做了第一个轮回所做的6%,可是在如今的机械上,这两个轮回破费的时间简直类似:辨别为80ms和78ms。
这个景象呈现的缘由和内存有关。上述轮回运转的时间首要破费在数组的内存拜访上,而不是在整数乘法上。我会在例2中说明为何硬件对两个轮回履行了类似的内存拜访。
让我们深化探究这个例子。接下来我们将会运用其它步长值,不单单是1和16:
for (int i = 0; i < arr.Length; i += K) arr[i] *= 3;
下面就是在轮回中分歧的步长值(K)的运转时间:
留意,步长在1到16之间变更,可是运转时间却简直没有改动。可是从16当前,步长每增加一倍,运转时间就为本来的一半。
这类景象的缘由就是当代的CPU并非以1byte1byte的拜访内存,而是,它们会一块为单元为获得,典范的就是64bytes,这就叫做cache lines.当你要读取特定的内存时,这cache line就会获得整块内存到缓存中,并且,从这cache line中拜访其它值更快。
由于16 ints 就是64bytes(一个cache line),for轮回步长从1到16,都在会运用类似的cache lines: 一切的cache lines都在数组中。可是一旦步长是32,我们就会运用其它一个的cache line,步长为64时,就会运用4个cache line.
了解了cache line,关于某些类型的程序优化非常主要。例如,数据行列可能会影响到一个操作运用一个或者两个cache line。正如我们上面看到那样,在非线性的状况,这个操作可能会慢两倍。
如今的盘算机都有二级或三级缓存,凡是称为L1, L2可能另有L3。假如你想晓得分歧缓存的巨细,可使用CoreInfo 如许的SysInternalsTools,或运用GetLogicalProcessorInfo 如许的Windows API 挪用。两种办法都会在缓存巨细以外,通知你高速缓存行的巨细。
在我的机械上,CoreInfo陈述称我有一个32kB L1数据缓存,一个32kB L1指令缓存,和一个4MB L2数据缓存。L1缓存是每一个核一个,L2缓存则在双核之间同享:
Logical Processor to Cache Map: *--- Data Cache 0, Level 1, 32 KB, Assoc 8, LineSize 64 *--- Instruction Cache 0, Level 1, 32 KB, Assoc 8, LineSize 64 -*-- Data Cache 1, Level 1, 32 KB, Assoc 8, LineSize 64 -*-- Instruction Cache 1, Level 1, 32 KB, Assoc 8, LineSize 64 **-- Unified Cache 0, Level 2, 4 MB, Assoc 16, LineSize 64 --*- Data Cache 2, Level 1, 32 KB, Assoc 8, LineSize 64 --*- Instruction Cache 2, Level 1, 32 KB, Assoc 8, LineSize 64 ---* Data Cache 3, Level 1, 32 KB, Assoc 8, LineSize 64 ---* Instruction Cache 3, Level 1, 32 KB, Assoc 8, LineSize 64 --** Unified Cache 1, Level 2, 4 MB, Assoc 16, LineSize 64
我们经过试验来验证一下这些数字。为了做到这一点,我们逐渐操作一个数组,这个数组每隔16个整数增加一——这是编辑每一个缓存行的一种容易办法。当我们拜访到最初一个数值,再重新轮回。我们拿分歧的数组巨细实行试验,我们可以看到在数组巨细溢出一个缓存级此外时分,功能有一个降落。
这里是程序:
int steps = 64 * 1024 * 1024; // Arbitrary number of steps int lengthMod = arr.Length - 1; for (int i = 0; i < steps; i++) { arr[(i * 16) & lengthMod]++; // (x & lengthMod) is equal to (x % arr.Length) }这里是所用时间:
你可以看到在32kB和4MB前面有分明的降落——这是我的机械L1 和 L2缓存的巨细。
如今,我们看一些纷歧样的工具。在这两个轮回中,你以为哪个会更快一些?
int steps = 256 * 1024 * 1024; int[] a = new int[2]; // Loop 1 for (int i=0; i<steps; i++) { a[0]++; a[0]++; } // Loop 2 for (int i=0; i<steps; i++) { a[0]++; a[1]++; }
结论是第二个轮回约莫是第一个轮回的两倍快,最少在我测试的一切机械上是如许的。为何呢?它与两个轮回体之间的操作有关。
在第一个轮回体内,操作之间互相依靠关系以下:
但在第二个例子中,我们只要这些依靠:
当代处置器有多种部件具有必定的并行性:它可以同时拜访L1中的两个内存地址,或者履行两个容易的算术运算。在第一个轮回中,处置器不能发扬这类指令级此外并行性,但在第二个轮回中,它可以。
【更新】:reddit网站上很多人讯问编译器优化,能否{ a[0]++; a[0]++; }会优化为{ a[0]+=2; }。实践上,C#编译器和CLR JIT(Common Language Runtime通用言语运转时 Just In-Time compile即时编译 )不会做这类优化——当牵扯到拜访数组时不会。我在发布形式创立了一切的测试(即具有优化形式),可是我察看了JIT-ted编译,验证了优化不会影响到这个后果。
缓存设计的一个最重要的要素就是主存块可否被存储在缓存槽傍边或者就是缓存槽的一部分。
内存映照到缓存槽有三条可能的道路:
每一个内存块只能被缓存中的一个特殊的缓存槽所存储。一个容易的处理方法就是用内存块的索引chunk_index弛缓存槽来实行映照(经过chunk_index和cache_slots求余的方法)。两个映照到类似缓存槽的内存块不能同时被贮存在缓存傍边。
任何一个内存块只能被缓存里的N个组中的一个缓存槽所存储。举个例子,在16路缓存中,每个内存块可以被16个分歧的缓存槽所存储,普通状况下,索引最低位字节类似的内存块将会同享16个缓存槽。
每一个内存块可以被任何缓存槽所存储。实践上,这类缓存的方法和哈希表是一样的。
直接缓存映照方法的效力将会遭到抵触的影响——当多个值配合竞争统一缓存槽时,它们将不时互相驱赶,射中坦白线降落。另外一方面,全联系关系映照方法的硬件完成庞杂而又昂贵。所以今朝处置器缓存的典范处理计划是 N路组联系关系映照方法,由于它在完成的容易性和较好的射中率之间做了很好的衡量。
例如,我电脑上的4MB二级缓存采取的是16路组联系关系映照方法。一切64字节的内存块将被分红组(依照内存块索引的最低位字节来划分),类似组的内存块配合竞争二级缓存中的16个缓存槽。
本文中的一切译文仅用于进修和交换目标,转载请务必注明文章译者、出处、和本文链接。 2KB翻译任务按照 CC 协议,假如我们的任务有进犯到您的权益,请实时联络我们。2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务