欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Cache Line 缓存行

程序员文章站 2022-05-26 23:53:03
...

缓存行为何存在?

没有缓存行的情况会出现的问题

  • 当我们要求计算机显示某些数据时,这是先不谈深度的“缓存”的概念,CPU 会去内存中寻找我们需要的数据,其实这本身可以说是正确的,
  • 只是后来人们发现,当计算机去一块内存中寻找某份数据之后,下一次需要的数据往往在上一次找到的数据的附近
  • 这是因为当我们把某种数据存入计算机时,代表这个数据的所有信息会作为一个整体存放到某个位置
  • 所以如果是检索数据,当第一次查找的数据的地址是 0 ,那么下一次查找到的数据地地址就很可能就在 0 的附近,甚至就是 1 的位置+ 那么如果我们每次从内存中重新寻找数据,就显得有些呆滞

解决方案

  • 为了解决这个问题,计算机中引入缓存行的概念,每次从内存中加载数据的时候,都会把相邻的数据一起返回给CPU,CPU 将数据存到自己的缓存中,下次查找数据时会优先从自己的缓存中查找,如果自己的缓存中没有对应的数据才会从内存中获取
  • 那么每次CPU 从内存中拿走多少数据呢?太多会浪费资源,太少又会减少下次从CPU 内存中获取数据的命中率
  • Intel 的处理器:64个字节,笔者了解到这是一个工业上测试的结果,认为64个字节是比较好的,并没有严谨的数学依据

链路说明

了解了上面的思想,我们来看一下 CPU 的真实数据链路

Cache Line 缓存行

 

  • 首先CPU不是每次获取数据都要去内存条中获取数据的,他要先去自己的一级缓存中找,一级缓存中没有,再去二级缓存中找,二级缓存中也没有再去三级缓存,最后才会去主内存中找
  • 那么如果CPU真的去主内存中找数据了,他会先把数据存在三级缓存中,然后存在二级缓存中,然后是一级换粗,最后返回给CPU
  • 当CPU处理完数据需要些回内存时,也是按照顺序一级一级的写入,最后写回到主内存中

shut up , show me your code !

说了这么多,怎么证明呢?
下面两段代码可以证明这件事情
我们使用数组进行测试,使用 volatile 关键字保证当前数组对另一个线程可见

​    public static volatile long[] arr = new long[2];
​
    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            for (long i = 0; i < 10000_0000L; i++) {
                arr[0] = i;
            }
        });
​
        Thread t2 = new Thread(()->{
            for (long i = 0; i < 10000_0000L; i++) {
                arr[1] = i;
            }
        });
​
        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)/100_0000);
    }
}

------------------------------------ 我是分割线 --------------------------------------

 public static volatile long[] arr = new long[16];
​
    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            for (long i = 0; i < 10000_0000L; i++) {
                arr[0] = i;
            }
        });
​
        Thread t2 = new Thread(()->{
            for (long i = 0; i < 10000_0000L; i++) {
                arr[8] = i;
            }
        });
​
        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)/100_0000);
    }


我们发现,第二段代码的执行速度更快,但是明显是第一段代码创建的数组更小,为什么执行效率却不如第二段代码呢 ?

这是因为,一个 long 占 8 个字节,第一段代码只创建了一个长度为 2 的数组,这个两个 long 型数据存很有可能被存在一个缓存行中,而我们又对 arr 添加了 volatile 关键字,t1 的 修改状态必须对 t2 可见,所以 t1 修改数据后就要把数据写回到主内存,并且通知 t2 数据被修改,需要重新从主内存获取,我们可以大致理解为,t1 修改后把数据写回主内存,通知 t2 重新获取,t2 获取后在修该,然后又写回主内存通知 ti1 获取,形成一个循环,两个线程之间的通信浪费了很多的性能

而第二段代码则直接创建了一个长度为 16 的数组,第一个数组在 arr[0],第二个数据在 arr[8],我们知道 8 个 long 就是 64 个字节,刚好够一个缓存行的大小,也就是说,arr[0] 和 arr[8] 不会出现在同一个缓存行中,那么 t1 和 t2 就可以自己修改自己的数据,不必进行通信,屏蔽了第一段代码的通信过程的消耗