首页 > 其他分享 >剖析Disruptor:为什么会这么快?(三)伪共享(转)

剖析Disruptor:为什么会这么快?(三)伪共享(转)

时间:2023-08-04 17:05:13浏览次数:43  
标签:Disruptor 缓存 long final 剖析 线程 static 共享 public


缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。缓存行上的写竞争是运行在SMP系统中并行线程实现可伸缩性最重要的限制因素。有人将伪共享描述成无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享。

为了让可伸缩性与线程数呈线性关系,就必须确保不会有两个线程往同一个变量或缓存行中写。两个线程写同一个变量可以在代码中发现。为了确定互相独立的变量是否共享了同一个缓存行,就需要了解内存布局,或找个工具告诉我们。Intel VTune就是这样一个分析工具。本文中我将解释Java对象的内存布局以及我们该如何填充缓存行以避免伪共享。

剖析Disruptor:为什么会这么快?(三)伪共享(转)_Disruptor

图 1.

图1说明了伪共享的问题。在核心1上运行的线程想更新变量X,同时核心2上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。如果核心1获得了所有权,缓存子系统将会使核心2中对应的缓存行失效。当核心2获得了所有权然后执行更新操作,核心1就要使自己对应的缓存行失效。这会来来回回的经过L3缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。

Java内存布局(Java Memory Layout)

对于HotSpot JVM,所有对象都有两个字长的对象头。第一个字是由24位哈希码和8位标志位(如锁的状态或作为锁对象)组成的Mark Word。第二个字是对象所属类的引用。如果是数组对象还需要一个额外的字来存储数组的长度。每个对象的起始地址都对齐于8字节以提高性能。因此当封装对象的时候为了高效率,对象字段声明的顺序会被重排序成下列基于字节大小的顺序:

  1. doubles (8) 和 longs (8)
  2. ints (4) 和 floats (4)
  3. shorts (2) 和 chars (2)
  4. booleans (1) 和 bytes (1)
  5. references (4/8)
  6. <子类字段重复上述顺序>

(译注:更多HotSpot虚拟机对象结构相关内容:)

了解这些之后就可以在任意字段间用7个long来填充缓存行。在Disruptor里我们对RingBuffer的cursor和BatchEventProcessor的序列进行了缓存行填充。

为了展示其性能影响,我们启动几个线程,每个都更新它自己独立的计数器。计数器是volatile long类型的,所以其它线程能看到它们的进展。

01	public final class FalseSharing
02	    implements Runnable
03	{
04	    public final static int NUM_THREADS = 4; // change
05	    public final static long ITERATIONS = 500L * 1000L * 1000L;
06	    private final int arrayIndex;
07	  
08	    private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
09	    static
10	    {
11	        for (int i = 0; i < longs.length; i++)
12	        {
13	            longs[i] = new VolatileLong();
14	        }
15	    }
16	  
17	    public FalseSharing(final int arrayIndex)
18	    {
19	        this.arrayIndex = arrayIndex;
20	    }
21	  
22	    public static void main(final String[] args) throws Exception
23	    {
24	        final long start = System.nanoTime();
25	        runTest();
26	        System.out.println("duration = " + (System.nanoTime() - start));
27	    }
28	  
29	    private static void runTest() throws InterruptedException
30	    {
31	        Thread[] threads = new Thread[NUM_THREADS];
32	  
33	        for (int i = 0; i < threads.length; i++)
34	        {
35	            threads[i] = new Thread(new FalseSharing(i));
36	        }
37	  
38	        for (Thread t : threads)
39	        {
40	            t.start();
41	        }
42	  
43	        for (Thread t : threads)
44	        {
45	            t.join();
46	        }
47	    }
48	  
49	    public void run()
50	    {
51	        long i = ITERATIONS + 1;
52	        while (0 != --i)
53	        {
54	            longs[arrayIndex].value = i;
55	        }
56	    }
57	  
58	    public final static class VolatileLong
59	    {
60	        public volatile long value = 0L;
61	        public long p1, p2, p3, p4, p5, p6; // comment out
62	    }
63
————————————————

结果(Results)

运行上面的代码,增加线程数以及添加/移除缓存行的填充,下面的图2描述了我得到的结果。这是在我4核Nehalem上测得的运行时间。

剖析Disruptor:为什么会这么快?(三)伪共享(转)_Java_02

图 2.

从不断上升的测试所需时间中能够明显看出伪共享的影响。没有缓存行竞争时,我们几近达到了随着线程数的线性扩展。

这并不是个完美的测试,因为我们不能确定这些VolatileLong会布局在内存的什么位置。它们是独立的对象。但是经验告诉我们同一时间分配的对象趋向集中于一块。

所以你也看到了,伪共享可能是无声的性能杀手。

标签:Disruptor,缓存,long,final,剖析,线程,static,共享,public
From: https://blog.51cto.com/u_2650279/6964787

相关文章

  • 如何使用Disruptor(二)如何从Ringbuffer读取(转)
    ConsumerBarrier与消费者这里我要稍微反过来介绍,因为总的来说读取数据这一过程比写数据要容易理解。假设通过一些“魔法”已经把数据写入到RingBuffer了,怎样从RingBuffer读出这些数据呢?(好,我开始后悔使用Paint/Gimp 了。尽管这是个购买绘图板的好借口,如果......
  • 如何使用 Disruptor(三)写入 Ringbuffer(转)
    本文的 重点 是:不要让Ring重叠;如何通知消费者;生产者一端的批处理;以及多个生产者如何协同工作。ProducerBarriersDisruptor 代码给 消费者 提供了一些接口和辅助类,但是没有给写入RingBuffer的 生产者 提供接口。这是因为除了你需要知道生产者之外,没有别人需要访问它。......
  • 为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的
    程序计数器、虚拟机栈和本地方法栈是线程私有的,而堆和方法区是线程共享的,这是由于它们在Java虚拟机中的作用和特性所决定的。程序计数器:程序计数器是一块较小的内存区域,用于存储当前线程正在执行的字节码指令的地址。每个线程都有自己独立的程序计数器,用于记录各自线程的执行......
  • 【设计模式】享元模式Flyweight:通过共享对象减少内存加载消耗
    (目录)享元模式Flyweight:通过共享对象减少内存加载消耗享元模式的用意享元模式以共享的⽅法⾼效地⽀持⼤量的细粒度对象,享元对象能做到共享的关键是区分内蕴状态和外蕴状态。⼀个内蕴状态是存储在享元对象内部的,并且是不会随环境改变⽽有所不同的,因此⼀个享元可以具有内蕴状态......
  • volatile关键字剖析
    这里引入一个案例:实现单例模式的双重检查锁packagecom.chunling.cloud.test;publicclassSingleton{privatestaticSingletoninstance;privateintvalue;privateSingleton(){try{Thread.sleep(100);}catch(Interrupted......
  • 【视频】R语言用线性回归预测共享单车的需求和可视化|数据分享
    全文链接:https://tecdat.cn/?p=33350原文出处:拓端数据部落公众号分析师:ShuliWang自行车共享系统是新一代的传统自行车租赁,从会员,租赁到归还的整个过程已经自动化。通过这些系统,用户可以轻松地从特定位置租用自行车,然后在另一个位置返回。目前,全球约有500多个自行车共享计划,其......
  • 高效Python-2-1 剖析(Profiling 性能分析)
    2从内置功能中获取最高性能本章包括剖析代码以发现速度和内存瓶颈更有效地利用现有的Python数据结构了解Python分配典型数据结构的内存成本使用懒编程技术处理大量数据有很多工具和库可以帮助我们编写更高效的Python。但是,在我们深入研究提高性能的所有外部选项之前,让我......
  • [转载]Vbox中自动挂载共享文件夹的读取
    转载自Ubuntu中文论坛本文只解决在使用共享文件夹时勾选自动挂载的选项自动挂载被勾选后,虚拟机会自动在目录/media下建立"sf_NAME"的挂载点,其中NAME为在Windows的Vbox中设置的共享文件夹的名称./media目录为Linux为了挂载外部存储设备而设立的目录问题就出在这个挂......
  • 9.1 共享文件
    内核用三个相关的数据结构来表示打开的文件:描述符表(descriptortable)。每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。文件表(filetable)。打开文件的集合是由一张文件表来表示的,所有的进程共享这张......
  • 使用UDP和RDP共享电脑屏幕和声音
    publicpartialclassForm1:Form{privateWasapiLoopbackCapturemic;//音频输入protectedRDPSession_rdpSession=null;publicForm1(){InitializeComponent();}staticThreadreceiveThrea......