首页 > 其他分享 >缓存行与伪共享问题

缓存行与伪共享问题

时间:2023-03-30 17:56:53浏览次数:43  
标签:arr 缓存 long 问题 static new 共享 public

局部性原理

时间局部性:如果数据正在被访问,那么在近期它很可能还会被再次访问。
比如循环、方法的反复调用等。

空间局部性:如果存储器的位置被引用,那么将来他附近的位置也会被引用。
比如顺序结构、数组。

CPU缓存

执行程序是靠CPU执行主存中代码,但是CPU和主存的速度差异是非常大的,为了降低这种差距,在架构中使用了CPU缓存,现在的计算机架构中普遍使用了缓存技术。常见一级缓存、二级缓存、三级缓存,数据获取访问速度如下:
从CPU到 大约需要的 CPU 周期 大约需要的时间
主存   约60-80纳秒
QPI 总线传输 (between sockets, not drawn)   约20ns
L3 cache 约40-45 cycles, 约15ns
L2 cache 约10 cycles, 约3ns
L1 cache 约3-4 cycles, 约1ns
寄存器 1 cycle  
每个缓存里面都是由缓存行组成的,缓存系统中以缓存行(cache line)为单位存储的。缓存行大小是64字节。由于缓存行的特性,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享(下面会介绍到)。有人将伪共享描述成无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享问题。 需要注意,数据在缓存中不是以独立的项来存储的,它不是我们认为的一个独立的变量,也不是一个单独的指针,它是有效引用主存中的一块地址。一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量。 缓存行的这种特性也决定了在访问同一缓存行中的数据时效率是比较高的。比如当你访问java中的一个long类型的数组,当数组中的一个值被加载到缓存中,它会额外加载另外7个,因此可以非常快速的遍历这个数组。实际上,你可以非常快速的遍历在连续的内存块中分配的任意数据结构。

伪共享问题

处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2,L3)后再进行操作,但操作完之后不知道何时会写到内存;如果对声明了volatile 变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在的缓存行的数据写回到系统内存。但就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读取到处理器缓存里。 为了说明伪共享问题,下面举一个例子进行说明:两个线程分别对两个变量(刚好在同一个缓存行)分别进行读写的情况分析。 在core1上线程需要更新变量X,同时core2上线程需要更新变量Y。这种情况下,两个变量就在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新对应的变量。如果core1获得了缓存行的所有权,那么缓存子系统将会使core2中对应的缓存失效。相反,如果core2获得了所有权然后执行更新操作,core1就要使自己对应的缓存行失效。这里需要注意:整个操作过程是以缓存行为单位进行处理的,这会来来回回的经过L3缓存,大大影响了性能,每次当前线程对缓存行进行写操作时,内核都要把另一个内核上的缓存块无效掉,并重新读取里面的数据。如果相互竞争的核心位于不同的插槽,就要额外横跨插槽连接,效率可能会更低。

缓存行对齐

基于以上问题的分析,在一些情况下,比如会频繁进行操作的数据,可以根据缓存行的特性进行缓存行对齐(即将要操作的数据凑一个缓存行进行操作)下面使用一个示例进行说明:
package com.example.demo;
 
public class Cacheline_nopadding {
    public static class T{
        //8字节
        private volatile long x = 0L;
    }
    private static T[] arr = new T[2];
 
    static {
        arr[0] = new T();
        arr[1] = new T();
    }
 
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(()->{
            for(long i = 0;i < 1000_0000L;i++){
                //volatile的缓存一致性协议MESI或者锁总线,会消耗时间
                arr[0].x = i;
            }
        });
 
        Thread thread2 = new Thread(()->{
            for(long i = 0;i< 1000_0000L;i++){
                arr[1].x = i;
            }
        });
        long startTime = System.nanoTime();
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("总计消耗时间:"+(System.nanoTime()-startTime)/100_000);
    }
}
总计消耗时间:3381
对齐缓存行:
private static class Padding{
     //7*8字节
     public volatile long p1,p2,p3,p4,p5,p6,p7;
}
public static class T extends Padding{
     //8字节
     private volatile long x = 0L;
}

优化后:

package com.example.demo;
 
public class Cacheline_padding {
    private static class Padding{
        //7*8字节
        public volatile long p1,p2,p3,p4,p5,p6,p7;
    }
    public static class T extends Padding{
        //8字节
        private volatile long x = 0L;
    }
    private static T[] arr = new T[2];
 
    static {
        arr[0] = new T();
        arr[1] = new T();
    }
 
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(()->{
            for(long i = 0;i < 1000_0000L;i++){
                //volatile的缓存一致性协议MESI或者锁总线,会消耗时间
                arr[0].x = i;
            }
        });
 
        Thread thread2 = new Thread(()->{
            for(long i = 0;i< 1000_0000L;i++){
                arr[1].x = i;
            }
        });
        long startTime = System.nanoTime();
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("总计消耗时间:"+(System.nanoTime()-startTime)/100_000);
    }
}
总计消耗时间:1428
从上面可以看到,使用缓存对齐,相同操作情况下对齐后的时间比没对齐的时间减少一半。 上面这种缓存行填充的方法在早期是比较流行的一种解决办法,比较有名的Disruptor框架就采用了这种解决办法提高性能,Disruptor是一个线程内通信框架,用于线程里共享数据。与LinkedBlockingQueue类似,提供了一个高速的生产者消费者模型,广泛用于批量IO读写,在硬盘读写相关的程序中应用十分广泛,Apache旗下的HBase、Hive、Storm等框架都有使用Disruptor。

JAVA8对伪共享的解决

进入到JAVA8后,官方已经提供了对伪共享的解决办法,那就是sun.misc.Contended注解,有了这个注解解决伪共享就变得简单多了。
@sun.misc.Contended
public static class T{
        //8字节
        private volatile long x = 0L;
}
默认情况下此注解是无效的,需要在JVM启动时设置-XX:-RestrictContended

标签:arr,缓存,long,问题,static,new,共享,public
From: https://www.cnblogs.com/zhengbiyu/p/17273810.html

相关文章

  • 视频融合平台EasyCVR设备录像因时间导致播放异常问题的排查与解决
    EasyCVR视频融合平台可提供丰富的视频能力,支持视频直播、录像、回放、检索、云存储、告警上报、语音对讲、集群、电子地图、智能分析以及平台级联等。平台可支持多协议、多类型设备接入,包括国标GB28181、RTMP、RTSP/Onvif、海康SDK、大华SDK、海康Ehome等,近期我们又拓展了更多SDK......
  • 网页接口偶发性502的问题
    现象:网页接口一直偶发性502,概率大概20%左右 排查过程:架构是用户->WAF->lvs->NGINX->后端 1、尝试减少接口请求,依然会502,可知和接口服务能力无关。2、WAF侧,更换服务IP,依然不行,可知不是WAF某个节点网络有问题。3、Nginx侧,查看日志,并未发现502日志,可知502的请求没到nginx,怀......
  • 缓存更新策略
      TRANSLATEwithxEnglishArabicHebrewPolishBulgarianHindiPortugueseCatalanHmongDawRomanianChineseSimplifiedHungarianRuss......
  • System.Drawing跨平台问题
     一、问题描述项目报出了如下错误:  二、问题分析后端项目部署在Linux系统,有一个接口涉及到数据流转图片,部分代码如下:Imageimage=Image.FromStream(stream);......
  • windows安装mongodb,配置服务名,简化启动及遇到的问题
    1、官网下载官网地址:https://www.mongodb.com进到官网之后,Products->CommunitiServer(或者直接访问 https://www.mongodb.com/try/download/community )   ......
  • SpringCloud常见问题描述
    1什么是SpringCloudSpringcloud流应用程序启动器是基于SpringBoot的Spring集成应用程序,提供与外部系统的集成。SpringcloudTask,一个生命周期短暂的微服务框......
  • php 浮点数转int精度丢失问题解决办法
    方案一:先将浮点金额strval后再转int。(推荐)$param['order_price']=intval(strval($param['order_price']*100)); 方案二: echoround(19.99*100); 这种方案出来是......
  • MySQL8给已有表新增自增列赋初始值的问题
    错误1:[22001][1138]Datatruncation:InvaliduseofNULLvalue原因:如果你xxx表已有数据,你是无法新增自增列的,需要中转一下。因为自增列需要是key。解决:第一步,给xxx......
  • 四个常见的Linux面试问题
    刚毕业要找工作了,只要是你找工作就会有面试这个环节,那么在面试环节中,有哪些注意事项值得我的关注呢?特别是专业技术岗位,这样的岗位询问一般都是在职的工程师,如何在面试环节更......
  • 解决ubuntu 20.04、22.04 即新版本 fcitx 无法使用的问题
    前提已在系统设置中将fcitx设置为默认fcitx开机自启配置的过程不在本文讨论范围之内开机自启可通过安装gnome-tweaks配置实现问题分析流程手动启动fcitx时提......