缓存行

作者: lenny611 | 来源:发表于2020-10-25 20:05 被阅读0次

    在介绍缓存行之前,我们需要先了解操作系统的存储器的层次结构,下图为CSAPP(原书第三版)中存储器层次结构图:


    存储器层次结构图

    一般的程序执行都是由CPU执行,但是由于CPU与主存的速度差异过大,因此引入了CPU缓存,一般分为三级缓存(至于为什么是三级,不是二级,四级,是因为工业系统长期实际得出的结果,三级缓存为最优):L1,L2,L3,与CPU更接近则访问速度越快。CPU访问缓存的顺序是L1,L2,L3,L1没命中,则去访问L2;L2没命中,则去访问L3,以此类推,一直到命中为止。而在L1,L2,L3中存储的数据并不是以某个变量单独存储的,在每个缓存里面是以缓存行(cache line)为单位存储的(这也是CPU的实现规定的),一般来说一个缓存行的大小在32kb-256kb之间,而通常的缓存行大小则为64kb(同样为工业得出的最优解)。这里带出来一个隐藏的问题就是:多线程修改互相独立的变量且这些变量位于同一个缓存行时,这会影响性能且很难发现,这也被称为伪共享。看以下模拟伪共享代码(即两个变量处于同一个缓存行):

    public class CacheLineFalse {
        private static long operationTimes=100000000L;
        private static class T{
            public volatile long data=0L;
        }
        public  static T[] array= new T[2];
        static {
            //分配空间
            array[0]=new T();
            array[1]=new T();
        }
        public static void main(String[] args) throws InterruptedException {
            CountDownLatch latch=new CountDownLatch(2);
            // 线程1操作array[0].x
             Thread thread1=new Thread(()->{
                for (int i=0;i<operationTimes;i++){
                    array[0].data=i;
                }
                latch.countDown();
             });
             //线程2操作array[1].x
            Thread thread2=new Thread(()->{
                for (long i = 0; i < operationTimes; i++) {
                    array[1].data=i;
                }
                latch.countDown();
            });
            final long startTime=System.nanoTime();
            thread1.start();
            thread2.start();
            latch.await();
            System.out.println((System.nanoTime()-startTime)/100_0000);
        }
    }
    

    执行结果如下:


    伪共享执行结果

    再来看两个变量处于不同的缓存行:

    public class CacheLineTrue {
        private static long operationTimes=100000000L;
        private static class T{
            //一个long占8个字节,8个long类型的刚好满一个缓存行
            private long p1,p2,p3,p4,p5,p6,p7;
            public volatile long data=0L;
            private long p9,p10,p11,p12,p13,p14,p15;
        }
        public  static CacheLineTrue.T[] array= new CacheLineTrue.T[2];
        static {
            //分配空间
            array[0]=new CacheLineTrue.T();
            array[1]=new CacheLineTrue.T();
        }
        public static void main(String[] args) throws InterruptedException {
            CountDownLatch latch=new CountDownLatch(2);
            // 线程1操作array[0].x
            Thread thread1=new Thread(()->{
                for (int i=0;i<operationTimes;i++){
                    array[0].data=i;
                }
                latch.countDown();
            });
            //线程2操作array[1].x
            Thread thread2=new Thread(()->{
                for (long i = 0; i < operationTimes; i++) {
                    array[1].data=i;
                }
                latch.countDown();
            });
            final long startTime=System.nanoTime();
            thread1.start();
            thread2.start();
            latch.await();
            System.out.println((System.nanoTime()-startTime)/1000_000);
        }
    }
    

    执行结果如下:


    处于不同缓存行执行结果

    可以看到在时间上还是有差距的,那么来看下两个代码的区别:


    区别
    为什么多定义了几个long变量,就导致这么大的差距?下面解释下原因:
    一个long变量占8个字节,而一个缓存行占64字节,8个long类型的变量刚好充满一个缓存行,而左边这样定义的结果就会导致两个线程访问的data一定不在同一个缓存行内,而右边这样定义会导致两个线程需要重新读取缓存行的内容(假设线程1获得了CPU执行权,volatile的作用会让线程2读取的缓存行失效。当线程2获得了CPU执行权然后执行更新操作,线程1读取的缓存行则会失效),把时间都耗费在这上面,由此产生的时间差,以及带来的性能问题就是这样产生的。

    Java8为了避免这种伪共享,实现了字节填充。
    JVM参数 -XX:-RestrictContended
    @Contended 位于 sun.misc 用于注解java 属性字段,自动填充字节,防止伪共享

    相关文章

      网友评论

          本文标题:缓存行

          本文链接:https://www.haomeiwen.com/subject/xvogmktx.html