首页 > 其他分享 >55 | 理解Disruptor(下):不需要换挡和踩刹车的CPU,有多快?

55 | 理解Disruptor(下):不需要换挡和踩刹车的CPU,有多快?

时间:2023-05-22 15:36:55浏览次数:55  
标签:Disruptor 操作数 55 long 队列 CPU 生产者


上一讲,我们学习了一个精妙的想法,Disruptor 通过缓存行填充,来利用好 CPU 的高速缓存。不知道你做完课后思考题之后,有没有体会到高速缓存在实践中带来的速度提升呢?


不过,利用 CPU 高速缓存,只是 Disruptor“快”的一个因素,那今天我们就来看一看 Disruptor 快的另一个因素,也就是“无锁”,而尽可能发挥 CPU 本身的高速处理性能。


缓慢的锁


Disruptor 作为一个高性能的生产者 - 消费者队列系统,一个核心的设计就是通过 RingBuffer 实现一个无锁队列。


上一讲里我们讲过,Java 里面的基础库里,就有像 LinkedBlockingQueue 这样的队列库。但是,这个队列库比起 Disruptor 里用的 RingBuffer 要慢上很多。慢的第一个原因我们说过,因为链表的数据在内存里面的布局对于高速缓存并不友好,而 RingBuffer 所使用的数组则不然。



55 | 理解Disruptor(下):不需要换挡和踩刹车的CPU,有多快?_加锁


LinkedBlockingQueue 慢,有另外一个重要的因素,那就是它对于锁的依赖。在生产者 - 消费者模式里,我们可能有多个消费者,同样也可能有多个生产者。多个生产者都要往队列的尾指针里面添加新的任务,就会产生多个线程的竞争。于是,在做这个事情的时候,生产者就需要拿到对于队列尾部的锁。同样地,在多个消费者去消费队列头的时候,也就产生竞争。同样消费者也要拿到锁。


那只有一个生产者,或者一个消费者,我们是不是就没有这个锁竞争的问题了呢?很遗憾,答案还是否定的。一般来说,在生产者 - 消费者模式下,消费者要比生产者快。不然的话,队列会产生积压,队列里面的任务会越堆越多。


一方面,你会发现越来越多的任务没有能够及时完成;另一方面,我们的内存也会放不下。虽然生产者 - 消费者模型下,我们都有一个队列来作为缓冲区,但是大部分情况下,这个缓冲区里面是空的。也就是说,即使只有一个生产者和一个消费者者,这个生产者指向的队列尾和消费者指向的队列头是同一个节点。于是,这两个生产者和消费者之间一样会产生锁竞争。


在 LinkedBlockingQueue 上,这个锁机制是通过 ReentrantLock 这个 Java 基础库来实现的。这个锁是一个用 Java 在 JVM 上直接实现的加锁机制,这个锁机制需要由 JVM 来进行裁决。这个锁的争夺,会把没有拿到锁的线程挂起等待,也就需要经过一次上下文切换(Context Switch)。


第 28 讲


我们可以按照 Disruptor 介绍资料里提到的 Benchmark,写一段代码来看看,是不是真是这样的。这里我放了一段 Java 代码,代码的逻辑很简单,就是把一个 long 类型的 counter,从 0 自增到 5 亿。一种方式是没有任何锁,另外一个方式是每次自增的时候都要去取一个锁。


你可以在自己的电脑上试试跑一下这个程序。在我这里,两个方式执行所需要的时间分别是 207 毫秒和 9603 毫秒,性能差出了将近 50 倍。


package
          
          
          
import
          
import
          
import
          
          
          
public            
           class
            
           LockBenchmark
          
          
          
public            
           static
            
           void
            
           runIncrement
           ()
          

            {
          
          
long            
           counter
            
           =
            
           0
          
long            
           max
            
           =
            
           500000000L
          
long            
           start
            
           =
          
while
          

            counter++;
          
          

            }
          
          
long            
           end
            
           =
          
"Time spent is "            + (end-start) + 
           "ms without lock"
          

            }
          
          
          
          
public            
           static
            
           void
            
           runIncrementWithLock
           ()
          

            {
          
          
Lock            
           lock
            
           =
            
           new
            
           ReentrantLock
          
long            
           counter
            
           =
            
           0
          
long            
           max
            
           =
            
           500000000L
          
long            
           start
            
           =
          
while
          
if
          

            counter++;
          
          

            lock.unlock();
          
          

            }
          
          

            }
          
          
long            
           end
            
           =
          
"Time spent is "            + (end-start) + 
           "ms with lock"
          

            }
          
          
          
          
public            
           static
            
           void
            
           main
           (String[] args)
          

            runIncrement();
          
          

            runIncrementWithLock();





加锁和不加锁自增 counter






Time spent is 207 ms without


Time spent is 9603 ms with





性能差出将近 10 倍


无锁的 RingBuffer


加锁很慢,所以 Disruptor 的解决方案就是“无锁”。这个“无锁”指的是没有操作系统层面的锁。实际上,Disruptor 还是利用了一个 CPU 硬件支持的指令,称之为 CAS(Compare And Swap,比较和交换)。在 Intel CPU 里面,这个对应的指令就是 cmpxchg。那么下面,我们就一起从 Disruptor 的源码,到具体的硬件指令来看看这是怎么一回事儿。


序号



55 | 理解Disruptor(下):不需要换挡和踩刹车的CPU,有多快?_计算机原理_02


在这个 RingBuffer 当中,进行生产者和消费者之间的资源协调,采用的是对比序号的方式。当生产者想要往队列里加入新数据的时候,它会把当前的生产者的 Sequence 的序号,加上需要加入的新数据的数量,然后和实际的消费者所在的位置进行对比,看看队列里是不是有足够的空间加入这些数据,而不会覆盖掉消费者还没有处理完的数据。


在 Sequence 的代码里面,就是通过 compareAndSet 这个方法,并且最终调用到了 UNSAFE.compareAndSwapLong,也就是直接使用了 CAS 指令。



public                       boolean
            
           compareAndSet
           (
           final
            
           long
            expectedValue, 
           final
            
           long
            newValue)
          

            {
          
          
return            UNSAFE.compareAndSwapLong(           this
          

            }
          
          
          
          
public                       long
            
           addAndGet
           (
           final
            
           long
            increment)
          

            {
          
          
long
          
long
          
          
          
do
          

            {
          
          

            currentValue = get();
          
          

            newValue = currentValue + increment;
          
          

            }
          
          
while
          
          
          
return


Sequence 源码中的 addAndGet,如果 CAS 的操作没有成功,它会不断忙等待地重试


一个 CPU 硬件支持的机器指令





[ax] (隐式参数,EAX累加器), [bx] (源操作数地址), [cx]





cmpxchg 指令,一共有三个操作数,第一个操作数不在指令里面出现,是一个隐式的操作数,也就是 EAX 累加寄存器里面的值。第二个操作数就是源操作数,并且指令会对比这个操作数和上面的累加寄存器里面的值。


如果值是相同的,那一方面,CPU 会把 ZF(也就是条件码寄存器里面零标志位的值)设置为 1,然后再把第三个操作数(也就是目标操作数),设置到源操作数的地址上。如果不相等的话,就会把源操作数里面的值,设置到累加器寄存器里面。


第 5 讲 、 第 6 讲






< = = [bx] THEN [ZF] = 1 , [bx] =


ELSE [ZF] = 0 , [ax] =





单个指令是原子的,这也就意味着在使用 CAS 操作的时候,我们不再需要单独进行加锁,直接调用就可以了。


没有了锁,CPU 这部高速跑车就像在赛道上行驶,不会遇到需要上下文切换这样的红灯而停下来。虽然会遇到像 CAS 这样复杂的机器指令,就好像赛道上会有 U 型弯一样,不过不用完全停下来等待,我们 CPU 运行起来仍然会快很多。


那么,CAS 操作到底会有多快呢?我们还是用一段 Java 代码来看一下。




package
          
          
          
import
          
import
          
import
          
          
          
public                       class
            
           LockBenchmark
          
          
          
public                       static
            
           void
            
           runIncrementAtomic
           ()
          

            {
          
          
AtomicLong                       counter
            
           =
            
           new
            
           AtomicLong
           (
           0
          
long                       max
            
           =
            
           500000000L
          
long                       start
            
           =
          
while
          

            }
          
          
long                       end
            
           =
          
"Time spent is "            + (end-start) +            "ms with cas"
          

            }
          
          
          
          
public                       static
            
           void
            
           main
           (String[] args)
          

            runIncrementAtomic();
          
          

            }
          
     
    
    
 

   
  
  
  
     
is            3867ms            with




和上面的 counter 自增一样,只不过这一次,自增我们采用了 AtomicLong 这个 Java 类。里面的 incrementAndGet 最终到了 CPU 指令层面,在实现的时候用的就是 CAS 操作。可以看到,它所花费的时间,虽然要比没有任何锁的操作慢上一个数量级,但是比起使用 ReentrantLock 这样的操作系统锁的机制,还是减少了一半以上的时间。


总结延伸


好了,咱们专栏的正文内容到今天就要结束了。今天最后一讲,我带着你一起看了 Disruptor 代码的一个核心设计,也就是它的 RingBuffer 是怎么做到无锁的。


Java 基础库里面的 BlockingQueue,都需要通过显示地加锁来保障生产者之间、消费者之间,乃至生产者和消费者之间,不会发生锁冲突的问题。


但是,加锁会大大拖慢我们的性能。在获取锁过程中,CPU 没有去执行计算的相关指令,而要等待操作系统或者 JVM 来进行锁竞争的裁决。而那些没有拿到锁而被挂起等待的线程,则需要进行上下文切换。这个上下文切换,会把挂起线程的寄存器里的数据放到线程的程序栈里面去。这也意味着,加载到高速缓存里面的数据也失效了,程序就变得更慢了。


Disruptor 里的 RingBuffer 采用了一个无锁的解决方案,通过 CAS 这样的操作,去进行序号的自增和对比,使得 CPU 不需要获取操作系统的锁。而是能够继续顺序地执行 CPU 指令。没有上下文切换、没有操作系统锁,自然程序就跑得快了。不过因为采用了 CAS 这样的忙等待(Busy-Wait)的方式,会使得我们的 CPU 始终满负荷运转,消耗更多的电,算是一个小小的缺点。


程序里面的 CAS 调用,映射到我们的 CPU 硬件层面,就是一个机器指令,这个指令就是 cmpxchg。可以看到,当想要追求最极致的性能的时候,我们会从应用层、贯穿到操作系统,乃至最后的 CPU 硬件,搞清楚从高级语言到系统调用,乃至最后的汇编指令,这整个过程是怎么执行代码的。而这个,也是学习组成原理这门专栏的意义所在。


标签:Disruptor,操作数,55,long,队列,CPU,生产者
From: https://blog.51cto.com/u_15202985/6324659

相关文章

  • 39 | MESI协议:如何让多核CPU的高速缓存保持一致?
    你平时用的电脑,应该都是多核的CPU。多核CPU有很多好处,其中最重要的一个就是,它使得我们在不能提升CPU的主频之后,找到了另一种提升CPU吞吐率的办法。不知道上一讲的内容你还记得多少?上一节,我们讲到,多核CPU里的每一个CPU核,都有独立的属于自己的L1Cache和L2Ca......
  • 03 | 通过你的CPU主频,我们来谈谈“性能”究竟是什么?
    00:10讲述:徐文浩大小:11.62M时长:12:41我们常常挂在嘴边的“性能”到底指的是什么呢?我们能不能给性能下一个明确的定义,然后来进行准确的比较呢?学习和研究计算机组成原理,就是在理解计算机是怎么运作的,以及为什么要这么运作......
  • windows系统下SNMP协议获取系统内存、CPU实例代码
    系统环境:win10注:win10及以下windows平台,在控制面板,程序中,添加snmp服务功能。可参照:https://jingyan.baidu.com/article/3d69c5515e56b3f0cf02d7bf.html为方便测试snmp协议,先关闭系统防火墙,后续可根据需要进行防护墙配置。工具资源下载:https://download.csdn.net/download/csdnyang......
  • linux 中 查看cpu架构
     001、uname-m[root@PC1~]#uname-mx86_64 002、arch[root@PC1~]#archx86_64 003、lscpu[root@PC1~]#lscpu ......
  • 2023-5-21 #55 渐行渐远迷路的我 看向了光年外璀璨星河
    358P5897[IOI2013]wombats线段树维护矩阵乘法,注意到有决策单调性,复杂度\(O(nC^2\logn)\),但是空间过大,我们递归到一个较小的区间时暴力计算即可,若阈值为\(k\),空间会整体除\(k\)。359P8275[USACO22OPEN]262144RevisitedP先考虑一个序列的问题:答案显然不超过最大值\(......
  • CPU通识课启发我的内容—从晶体管到生态宇宙
    做CPU的理由①中国信息化生态的高额利润被国外厂商赚取②国外高端CPU有严重供应链风险,难以杜绝后面,受制于人(就有点像寄人篱下)③信息安全:国外产品往往不提供设计资料和源代码。使用过程中常出现后门和漏洞,重要信息数据有被窃取、泄露的风险④缺少CPU影响企业生产产品(如工业控制),......
  • 微软推出Windows 11 Insider预览版22621.1255和22623.1255
    您好,WindowsInsider,今天我们将向Beta频道发布Windows11Insider预览版22621.1255和22623.1255(KB5022918)。Build22623.1255=推送新功能。Build22621.1255=默认情况下关闭新功能。提醒:以前在22622版本上的内部人员将通过启用包自动转移到22623版本。启用包人为地增加了新功能推出......
  • perf火焰图原生分析Linux cpu性能
    perfrecord-a-g-p16787--sleep30会生成perfdata在当前目录下:-rw-------1rootroot1068092May2118:11perf.datayum-yinstallgitgitclonehttps://github.com/brendangregg/FlameGraph.git生成火焰图perfscript-iperf.data&>perf.unfo......
  • 关于查看电脑cpu温度的文章
    大家好,我是末,是一个大学电脑计算机专业的学生,现已大三,对电脑代码知识方面有些研究,现在可以跟大家分享一下,本次来分享几个实用的代码小片段。获取CPU温度应用可以定时获取CPU的温度,比如程序异常崩溃时,我们可能需要分析多方面原因,CPU温度就是其中之一。代码:#include<stdio.h>#inclu......
  • CPU和显卡才是最抗热的?
    高温是电脑蓝屏和掉帧的罪魁祸首,虽然硬件有了保护不会因为高温烧坏,但当你的工作进行到一半时突然蓝屏,或是游戏中的关键时刻突然掉帧,你的内心肯定是崩溃的,那么电脑中的硬件温度应该控制在多少度呢?首先,你需要在电脑中安装aida64这款软件,直接去官网下载,免费的试用版也不影响测试,在......