一般来说,CPU不会直接从内存中读取数据,会按L1(线程本地缓存) -> L2(线程本地缓存) -> L3(线程共享缓存)的缓存顺序,先去CPU的缓存中查询是否有指定变量。如果没有,才会去主存中读取数据。下图是我整理的MAC的缓存模型,和一般的处理器有一些不一样,只有L2和L3级缓存,并没有L1级缓存,不过这并不影响我们学习。
CPU缓存模型.drawio.png
至于为什么会按照这个顺序去读取变量呢,我们可以看另外一张图。从这张图我们就能明显看到读取速度的差异,其中从寄存器中读取变量,和从磁盘中读取变量,速度可以相差1000万倍。所以,越靠近CPU核心,速度就越快。

这里会引申出一个问题,为什么CPU高速缓存这么快,L2才256KB, L3才6MB,加起来也不到10MB? 这是因为CPU这个元件本身很小,工艺非常复杂。如果要加更多的内存进去,成本非常高,所以也可以理解为在这中间做了一个取舍。

image.png
说到CPU的缓存模型,就必然会提到两个点,时间局部性和空间局部性

时间局部性

CPU认为,如果一个变量被使用,那么它会快会再次被使用。所以在一个变量使用完之后,不会立即被丢弃。而是在一段时间后才会失效。

空间局部性

CPU认为,如果一个变量被使用,那么它的临近变量也会很快被使用。所以当CPU从主存中读取一个变量的时候,也会同时将这个变量的临近变量一起读取到缓存中。

下面我们用一个例子来证明CPU的空间局部性

证明逻辑

  1. 初始化一个数组,数组大小是[1024 1024][64],也就是初始化一个64列,10241024行的数组
  2. 将数组的的全部位置初始化为1,然后对数组进行求和
  3. 第一种方式是行优先加,也就是先将一行累加结束,再累加第二行
  4. 第二种方式是列优先加,也就是先将一列累加结束,再累加第二列
  5. 假设:按照空间局部性原则,一行数组的地址连续,在读取第一行的时候,就会将这一行所有的数据加载到缓存中,一行也就只会读取一次缓存,也按列加的话,每次读取一个数据,都需要从内存中去加载。按照这个假设,我们可以得出以下结论。
    1. 按行加读取内存次数:1024 * 1024,也就是行数
    2. 按列加读取内存次数:1024 1024 64,所有元素的总数
  6. 在证明之前,我们先看一张图,了解一下数组在内存中是如何存储的。可以看到,每行的数据在内存中地址是连续的,而列并不连续。

数组在内存中是怎么存放的.drawio.png

  1. 代码证明

    1. public class SpacePartTest {
    2. /**
    3. * 测试空间局部性
    4. *
    5. * cpu在读取一个变量的时候,如果在内部缓存读取不到,会直接读取内存。
    6. * 但cpu认为,如果一个变量被使用到,那么它临近的变量也会很快被使用到
    7. * 所以cpu在读取一个变量的同时,也会读取这个变量的临近变量
    8. *
    9. * 下面是一个测试,初始化一个行数为1024 * 1024,列数为64的数组,将数组的全部值初始化为1
    10. * 我们来测试一个结论,按行加,使用到空间局部性,速度会快于按列加
    11. *
    12. *
    13. * @param args
    14. */
    15. public static void main(String[] args) {
    16. int xLen = 64;
    17. int yLen = 1024 * 1024;
    18. int[][] nums = new int[yLen][xLen];
    19. for (int y = 0; y < yLen; y++) {
    20. for (int x = 0; x < xLen; x++) {
    21. nums[y][x] = 1;
    22. }
    23. }
    24. int sum = 0;
    25. StopWatch stopWatch = new StopWatch();
    26. stopWatch.start("行优先加");
    27. for (int y = 0; y < yLen; y++) {
    28. for (int x = 0; x < xLen; x++) {
    29. sum += nums[y][x];
    30. }
    31. }
    32. stopWatch.stop();
    33. stopWatch.start("列优先加");
    34. sum = 0;
    35. for (int x = 0; x < xLen; x++) {
    36. for (int y = 0; y < yLen; y++) {
    37. sum += nums[y][x];
    38. }
    39. }
    40. stopWatch.stop();
    41. System.out.println(stopWatch.prettyPrint());
    42. }
    43. }
  2. 测试结果,从结果中我们可以明显的看到,按列加占据了绝大部分的时间。 ```java StopWatch ‘’: running time = 1265081620 ns


ns % Task name

076201035 006% 行优先加 1188880585 094% 列优先加 ```

  1. 结论:假设正确