我认为这最好用例子来解释。这些局部性原则通常用于优化事物。现代 CPU 中一个可能的组件是内存预取器,它将尝试猜测您将使用哪个内存,并在您需要时将其放入缓存中。这在很大程度上依赖于局部性原则。
以数组为例,如果您执行以下操作(c++ 示例):
#include <iostream>
#include <vector>
int main()
{
std::vector<int> test = { 1, 2, 3, 4, 5};
for(int& i: test)
{
std::cout << i << std::endl;
}
}
在向量(或其他语言的数组)中,元素被打包在一个具有固定步幅的连续块中。因此,如果 的第一个元素test
位于 address X
,那么第二个元素将位于X+Y
,第三个位于X+2Y
, ...
。因此,向量本身是空间局部性的一个非常基本的例子,更好的是,局部性是非常可预测的。其次,元素是在一个紧密的循环中访问的,所以我们也有很好的时间空间性。因为元素也是按顺序访问的,所以我们在“时空”中具有等距的空间性。这意味着一旦 CPU 识别出X+Y, X+2Y, X+3Y
他查找的模式,它就可以开始在缓存中拉入未来的元素。
您可以将此与例如:
#include <iostream>
#include <list>
int main()
{
std::list<int> test = { 1, 2, 3, 4, 5};
for(int& i: test)
{
std::cout << i << std::endl;
}
}
在链表中,元素相互引用,单个元素可以在内存中的任何位置,因此您失去了空间局部性。但是,您可以循环访问元素,因此您仍然具有时间空间性。像这样的事情更难检测和优化预取(但并非不可能)。
最后,作为为什么组合时空空间性很重要的一个指标,考虑这个(有点做作的)例子:
#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
#include <iterator>
int main()
{
std::vector<int> test = { 1, 2, 3, 4, 5 };
std::vector<unsigned int> indices = { 0, 1, 2, 3, 4 };
std::random_device rd;
std::shuffle(std::begin(indices), std::end(indices), std::mt19937 { rd() });
for (unsigned int& i : indices)
{
std::cout << test[i] << std::endl;
}
}
如果您只看test
容器,它又具有良好的空间局部性(如第一个示例中那样跨步)。如果您查看test
循环中的访问,您会发现查找中存在时间局部性。但是,在“时间空间”中,当您从数组的一部分跳到另一部分时,查找不是等距的,访问不是连续的,因此空间和时间都没有等距的空间性。这几乎是不可能优化的。