I think this is best explained with examples. These principles of locality are often used to optimize things. A possible component in a modern CPU is a memory prefetcher, which will try to guess which memory you'll use and get it in cache by the time you need it. This relies heavily on the locality principle.
Take an array for example, if you do something like this (c++ example):
#include <iostream>
#include <vector>
int main()
{
std::vector<int> test = { 1, 2, 3, 4, 5};
for(int& i: test)
{
std::cout << i << std::endl;
}
}
In a vector (or array in other languages) elements are packed in a contiguous block with fixed strides. So if the first element of test
is at address X
, then the second element will be at X+Y
, the third at X+2Y
, ...
. A vector itself is therefore a very basic example of spatial locality, and even better the locality is very predictable.
Next to that the elements are accessed in a tight loop, so we have good temporal spatiality also. Because the elements are also sequentially accessed, we have an equidistant spatiality in 'spacetime'. This means that as soon the CPU recognizes the X+Y, X+2Y, X+3Y
pattern int he lookup it can start pulling in future elements in the cache.
You can contrast this with for example:
#include <iostream>
#include <list>
int main()
{
std::list<int> test = { 1, 2, 3, 4, 5};
for(int& i: test)
{
std::cout << i << std::endl;
}
}
In a linked list, elements refer to eachother, and individual elements can be at any place in the memory so you lose your spatial locality. You access the elements in a loop however, so you still have your temporal spatiality. Something like this is much harder to detect and optimize for prefetching (not impossible however).
Finally, as an indicator why combined spacetime spatiality is important, consider this (little bit contrived) example:
#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;
}
}
If you purely look at the test
container it had again good spatial locality (strided as in the first example). If you look at the test
access in the loop, you see there is a temporal locality in lookups. However, in "timespace" the lookups are not equidistant as you jump from one part of the array to the other, the access is not sequential, so there is no equidistant spatiality in both space and time. This is almost impossible to optimize.