# 6.2.1 对程序数据引用的局部性

考虑图 6-17a 中的简单函数,它对一个向量的元素求和。这个程序有良好的局部性吗?要回答这个问题,我们来看看每个变量的引用模式。在这个例子中,变量 sum 在每次循环迭代中被引用一次,因此,对于 sum 来说,有好的时间局部性。另一方面,因为 sum 是标量,对于 sum 来说,没有空间局部性。

int sumvec(int v[N])
{
    int i, sum = 0;

    for (i = 0; i < N; i++)
        sum += v[i];
    return sum;
}

a) 一个具有良好局部性的程序

地址 0 4 8 12 16 20 24 28
内容 v0 v1 v2 v3 v4 v5 v6 v7
访问顺序 1 2 3 4 5 6 7 8

b) 向量 v 的引用模式(N = 8)

图 6-17 注意如何按照向量元素存储在内存中的顺序来访问它们

正如我们在图 6-17b 中看到的,向量 v 的元素是被顺序读取的,一个接一个,按照它们存储在内存中的顺序(为了方便,我们假设数组是从地址 0 开始的)。因此,对于变量 v,函数有很好的空间局部性,但是时间局部性很差,因为每个向量元素只被访问一次。因为对于循环体中的每个变量,这个函数要么有好的空间局部性,要么有好的时间局部性,所以我们可以断定 sumvec 函数有良好的局部性。

我们说像 sumvec 这样顺序访问一个向量每个元素的函数,具有步长为 1 的引用模式(stride-1 reference pattern)(相对于元素的大小)。有时我们称步长为 1 的引用模式为顺序引用模式(sequential reference pattern)。一个连续向量中,每隔 k 个元素进行访问,就称为步长为 k 的引用模式(stride-k reference pattern)。步长为 1 的引用模式是程序中空间局部性常见和重要的来源。一般而言,随着步长的增加,空间局部性下降。

对于引用多维数组的程序来说,步长也是一个很重要的问题。例如,考虑图 6-18a 中的函数 sumarrayrows,它对一个二维数组的元素求和。双重嵌套循环按照行优先顺序(row-major order)读数组的元素。也就是,内层循环读第一行的元素,然后读第二行,依此类推。函数 sumarrayrows 具有良好的空间局部性,因为它按照数组被存储的行优先顺序来访问这个数组(图 6-18b)。其结果是得到一个很好的步长为 1 的引用模式,具有良好的空间局部性。

int sumarrayrows(int a[M][N])
{
    int i, j, sum = 0;

    for (i = 0; i < M; i++)
        for (j = 0; j < N; j++)
            sum += a[i][j];
    return sum;
}

a) 另一个具有良好局部性的程序

地址 0 4 8 12 16 20
内容 a00 a01 a02 a10 a11 a12
访问顺序 1 2 3 4 5 6

b) 数组 a 的引用模式(M = 2,N = 3)

图 6-18 有良好的空间局部性,是因为数组是按照与它存储在内存中一样的行优先顺序来被访问的

一些看上去很小的对程序的改动能够对它的局部性有很大的影响。例如,图 6-19a 中的函数 sumarraycols 计算的结果和图 6-18a 中函数 sumarrayrows 的一样。唯一的区别是我们交换了 ij 的循环。这样交换循环对它的局部性有何影响?函数 sumarraycols 的空间局部性很差,因为它按照列顺序来扫描数组,而不是按照行顺序。因为 C 数组在内存中是按照行顺序来存放的,结果就得到步长为 N 的引用模式,如图 6-19b 所示。

int sumarraycols(int a[M][N])
{
    int i, j, sum = 0;

    for (j = 0; j < N; j++)
        for (i = 0; i < M; i++)
            sum += a[i][j];
    return sum;
}

a) 一个空间局部性很差的程序

地址 0 4 8 12 16 20
内容 a00 a01 a02 a10 a11 a12
访问顺序 1 3 5 2 4 6

b) 数组 a 的引用模式(M = 2,N = 3)

图 6-19 函数的空间局部性很差,这是因为它使用步长为 N 的引用模式来扫描