📜  计算机组织 |局部性和缓存友好代码

📅  最后修改于: 2021-09-27 15:02:31             🧑  作者: Mango

高速缓存是更快的存储器,用于处理数据读取操作中的处理器-内存间隙,即 CPU 寄存器中的数据读取操作与主存储器中的数据读取操作的时间差。寄存器中的数据读取操作通常比主存储器中快 100 倍,并且随着我们在存储器层次结构中的下降,它继续大幅增加。

缓存安装在 CPU 寄存器和主内存的中间,以弥补数据读取的时间差距。缓存用作存储在相对较慢的主存储器中的数据和指令子集的临时暂存区。由于缓存的大小很小,所以缓存中只存储处理器在程序执行过程中经常使用的数据。 CPU 缓存这些经常使用的数据消除了一次又一次从较慢的主内存中获取数据的需要,这需要数百个 CPU 周期。

围绕计算机程序的一个基本属性(称为局部性)缓存有用数据的想法。具有良好局部性的程序倾向于从内存层次结构的上层(即缓存)一遍又一遍地访问同一组数据项,因此运行得更快。

示例:执行相同数量的算术运算但具有不同局部性的不同矩阵乘法内核的运行时间可以相差 20 倍!

地区类型:

  • 时间地点 –
    时间局部性表明,在程序执行期间,CPU 可能多次重复使用相同的数据对象。一旦数据对象在第一次未命中时被写入缓存,就可以预期对该对象的多次后续命中。由于缓存比下一个较低级别的存储(如主存储器)快,因此这些后续命中可以比原始未命中快得多。
  • 空间局部性——
    它指出,如果一个数据对象被引用一次,那么它的邻居数据对象在不久的将来也很有可能被引用。内存块通常包含多个数据对象。由于空间局部性,我们可以预期在未命中后复制块的成本将通过对该块内其他对象的后续引用来分摊。

位置的重要性 –
程序中的局部性对硬件和软件系统的设计和性能有着巨大的影响。在现代计算系统中,基于局部性的优势不仅限于体系结构,而且操作系统和应用程序的构建方式可以充分利用局部性。

在操作系统中,局部性原则允许系统使用主内存作为最近引用的虚拟地址空间块的缓存,以及磁盘文件系统中最近使用的磁盘块的缓存。

类似地,Web 浏览器等应用程序通过在本地磁盘上缓存最近引用的文档来利用时间局部性。高容量 Web 服务器将最近请求的文档保存在前端磁盘缓存中,以满足对这些文档的请求,而无需服务器的任何干预。

缓存友好代码 –
具有良好局部性的程序通常运行得更快,因为与具有较差局部性的程序相比,它们具有较低的缓存未命中率。在良好的编程实践中,缓存性能始终被视为分析程序性能的重要因素之一。关于代码如何缓存友好的基本方法是:

  • 经常使用的情况需要更快:程序通常将大部分时间投入到几个核心功能上,而这些功能作为回报与循环有很大关系。因此,这些循环的设计方式应使其具有良好的局部性。
  • 多个循环:如果一个程序由多个循环组成,那么尽量减少内循环中的缓存未命中,以降低代码的性能。

Example-1:上面的上下文可以通过下面的多维数组代码的简单例子来理解。考虑 sum_array()函数,它按行主顺序对二维数组的元素求和:

int sumarrayrows(int a[8][4])
{
 int i, j, sum = 0;
 for (i = 0; i < 8; i++)
    for (j = 0; j < 4; j++)
     sum += a[i][j];
 return sum;
}

假设高速缓存的块大小为每个 4 个字,字大小为 4 个字节。它最初是空的,因为 C 以行优先顺序存储数组,因此引用将导致以下命中和未命中模式,与缓存组织无关。

包含 w[0]–w[3] 的块从内存加载到缓存中,对 w[0] 的引用是未命中,但接下来的三个引用都是命中。当一个新块被加载到缓存中时,对 v[4] 的引用会导致另一个未命中,接下来的三个引用是命中,依此类推。一般来说,四分之三的引用会命中,这是冷缓存所能做到的最好的情况。因此,命中率为 3/4*100 = 75%

示例 2:现在, sum_array()函数按列主序对二维数组的元素求和。

int sum_array(int a[8][8])
{
 int i, j, sum = 0;
 for (j = 0; j < 8; j++)
   for (i = 0; i < 8; i++)
   sum += a[i][j];
 return sum;
}

程序的缓存布局将如图所示:

由于 C 以行优先顺序存储数组,但在这种情况下,数组是以列优先顺序访问的,因此在这种情况下局部性受损。引用将按顺序进行:a[0][0]、a[1][0]、a[2][0] 等。由于缓存大小较小,每次引用都会由于程序的局部性差而导致未命中。因此,命中率将为 0。低命中率最终会降低程序的性能并导致执行速度变慢。在编程中,应该避免这些类型的做法。

结论 –
当谈到现实生活中的应用程序和编程领域时,优化的缓存性能为程序提供了良好的加速,即使程序的运行时复杂度很高。一个很好的例子是快速排序。虽然它的最坏情况复杂度为 O(n 2 ),但它是最流行的排序算法,重要因素之一是比许多其他排序算法更好的缓存性能。代码的编写方式应该能够最大限度地利用缓存以加快执行速度。