背景
在自然环境中事物的发生和存在的概率并不是平均分布的,平均分布(Uniform distribution) 是概率论中几何分布的一种特殊形式,非常简单易懂,但属于一种理想模型。我们更常听到的分布方式叫高斯分布(Gaussian distribution) 或者被称为 正态分布(Normal distribution),如下图所示,举个例子,大多数的诸如人群的身高、体重、血压、智商等特征数值也是如正态分布。在抽象层面来说,造成这种情况的原因是宇宙中的 熵(Entropy) 分布的不平均导致的,存在局部的熵值低的情况。熵是一种用于量化系统混乱程度的函数,当熵值高的时候系统混乱程度就高,当熵值低的时候系统就相对有序。如果宇宙中的熵值处处一致,导致的结果就是一片混沌。
局部性(Locality)和存储器系统(Memory System)
局部性的 本质 就是统计学中的概率不均等,而计算机架构设计中优化思想的基石之一就是 局部性(Locality) 原理。一个简单的计算机系统模型包含两个部分,CPU 执行指令以及为 CPU 存放指令和数据的存储器系统。在简单模型中,存储器系统是一个线性的字节数组,而 CPU 能够在一个常数时间内访问每个存储器位置。虽然这是一个有效的模型,但它没有反映计算机系统实际工作的方式。
实际上,存储器系统(Memory System) 是一个如下图所示具有不同容量、成本和访问时间的存储设备的层次结构。CPU 寄存器(Register Files) 保存着最常用的数据。靠近 CPU 的小的、快速的 高速缓存存储器(Cache Memory) 作为一部分存储在相对慢速的 主存储器(Main Memory) 中数据和指令的缓冲区域。主存负责缓存存储在容量较大的、慢速磁盘上的数据,而这些磁盘常常又作为存储在通过网络连接的其他机器的磁盘或磁带上的数据的缓冲区域。之所以产生这种存储器层次结构思想,正是由于计算机程序也具备 局部性 的属性。具有良好局部性的程序倾向于一次又一次地访问相同地数据项集合,或是倾向于访问邻近的数据项集合。具有良好局部性的程序比局部性差的程序更多地倾向于从存储器层次结构中较高层次处访问数据项,因此运行得更快。
时间局部性(Temporal Locality)
局部性通常有两种不同得形式:时间局部性(Temporal Locality) 和空间局部性(Spatial Locality)。在一个具有良好时间局部性的程序中,被引用过一次的内存位置很可能在不远的将来再次被多次引用。举个例子,如下图(a)所示的简单函数,它的意义是对一个向量的元素求和。在这个例子中,变量 sum 在每次循环迭代中被引用一次,因此,对于 sum 来说,它具有良好的时间局部性。另一方面,因为 sum 是标量,对于 sum 来说,没有空间局部性。
空间局部性(Spatial Locality)
空间局部性指的是,一个具有良好空间局部性的程序中,如果一个内存位置被引用了一次,那么程序很可能在不远的将来引用附近的一个内存位置。回到上图(b),我们可以看到的,向量 v 的元素是被顺序读取的,一个接一个,按照他们存储在内存中的顺序(我们假设数组是从地址 0 开始的)。因此,我们可以说变量 v 具有良好的空间局部性,但是时间局部性很差,因为每个向量元素只被访问一次。对于上图循环体中的每个变量或者标量,要么有良好的空间局部性,要么有良好的时间局部性,所以我们可以断定 sumvec 函数具有良好的局部性。
像 sumvec 这样顺序访问一个向量每个元素的函数,具有步长为 1 的引用模式(Stride-1 Reference Pattern)(相对于元素的大小)。有时我们称步长为 1 的引用模式为顺序引用模式(Sequential Reference Pattern)。一个连续向量中,每隔 k 个元素进行访问,就称为步长为 k 的引用模式(Stride-k Reference Pattern)。步长为 1 的引用模式是程序中空间局部性常见和重要的来源。一般而言,随着步长的增加,空间局部性下降。
对于引用多维数组的程序来说,步长也是一个重要的问题。我们把例子做的更复杂一点。如下图所示,图(a)中的函数 sumarrayrows,是对一个二维数组的元素求和。这个双重嵌套循环按照 行优先顺序(Row-Major Order) 读数组的元素。也就是说,内层循环读第一行的元素,然后读第二行,依次类推。我们认为函数 sumarrayrows 具有良好的空间局部性,因为在 C 编译中的数组是按行优先的顺序依次排布存储的,这个函数也是以相同的方式访问这块内存空间的。其结果是得到一个很好的步长为 1 的引用模式,具有良好的空间局部性。
一些看上去很小的程序改动能够对它的局部性有很大的影响。例如下图中的函数 sumarraycols 实现的功能和 sumarrayrows 一样。唯一的区别就是我们交换了 i 和 j 的循环次序。这样的交换循环破坏了它的局部性,函数 sumarraycols 的空间局部性很差,因为它是按照列顺序来扫描数组,而不是按照行顺序,不符合 C 数组在内存中存放的顺序,结果就是得到步长为 N 的引用模式。
取指的局部性
因为程序指令也是存放在内存中的,CPU 必须取出或者读取这些指令,所以我们也应该评价一个程序关于取指令的局部性。例如 sumvec 中的 for 循环里的指令是按照连续的内存顺序执行的,因此循环有良好的空间局部性。因为循环体会被执行多次,所以它也有很好的时间局部性。
指令区别于程序数据的一个重要属性就是在运行的时候不能被修改。当程序正在执行时候,CPU 只从内存中读出它的指令。CPU 很少会重写或修改这些指令。
总结
在本文,我们介绍了局部性的基本思想,还给出量化评价程序中局部性的一些简单原则:
- 重复引用相同变量的程序有良好的时间局部性。
- 对于具有步长为 k 的引用模式的程序,步长越小,空间局部性越好。具有步长为 1 的引用模式的程序有很好的空间局部性。在内存中以大步跳来跳去的程序空间局部性会很差。
- 对于取指令而言,循环有好的时间和空间局部性。循环体越小,循环迭代次数越多,局部性越好。