Disruptor高性能之道-缓存行填充
在上一篇文章中,我们介绍了Disruptor实现高性能的方式之一:环形数组RingBuffer。
本文我们介绍Disruptor高性能的另一个实现机制为 「“缓存行填充”」,它解决了CPU访问内存变量的“伪共享”问题。
什么是伪共享?❝在解释什么是伪共享之前,先了解下数据在缓存中是如何存储的。
❞
我们都知道,计算机为了解决CPU与主存之间速度差的问题,引入了多级缓存机制。
事实上,数据在CPU缓存(多级cache)中并非是单独存储的,而是按行存储的。其中每一行成为一个缓存行。
缓存行是CPU的Cache与主内存进行数据交换的基本单位,每个缓存行的大小一般为2的N次方字节。(「在32位计算机中为32字节,64位计算机中为64字节。」)可以想到,如果计算机为128位,则缓存行大小就是128字节。
在Java中,一个long型变量为8字节,也就是说在64位计算机中,每行可存放8个long型变量。
❝当CPU访问某个变量的时,如果CPU Cache中存在该变量,则直接获取。若不存在则去主内存获取该变量。由于缓存行机制的存在,因此会将该变量所在内存区域为一个缓存行大小的内存复制到CPU Cache中。
❞
此时有可能会在一行缓存行中加载多个变量,如图中不同的颜色对应不同的long型变量。
❝试想,如果多个内核的线程都操作了同一缓存行的数据,如图所示。CPU1读取并修改了缓存行中的变量D,了解volatile的同学都知道,当CPU Cache中的变量发生变更,会通过缓存一致性协议通知其他CPU失效当前缓存行,重新从主内存中加载当前行的值。
❞
图中,CPU1修改了缓存行中的变量D,CPU2也在读取该缓存行的值。根据缓存一致性协议,CPU2中的缓存行会失效,因为它操作的缓存行中的变量D的值已经不是最新值了。
这是因为CPU是以缓存行为单位进行数据的读写操作的。
这就是伪共享。
为什么是“伪”共享呢?
❝看起来CPU1 与 CPU2 共享了同一个缓存行,但是由于CPU以缓存行为单位进行读写操作,无论CPU1 与 CPU2中的任何一位修改了缓存行中的值,都需要通知其他CPU对失效该缓存行。也就是说当线程对缓存进行了写操作,则当前线程所在内核就需要失效其他内核的缓存行,并重新加载主内存。
❞
这是一种缓存未命中的情况,当发生这样的情况,缓存本身的意义就被削弱了,因为CPU始终需要从主内存加载数据,而根本命中不了CPU Cache中的缓存。
❝Disruptor是如何进行缓存行填充的?所谓的“伪”共享,就可以理解成是一种 “错误”的共享,这种共享如果不发生,则多核CPU操作缓存行互不影响,每个核心都只关心自己操作的变量,而不会因为读写自己关心的变量而影响到其他CPU对变量的读写。
❞
❝Disruptor解决伪共享的方式为:使用缓存行填充。
❞
上文我们提到,由于多核CPU同时读写统一缓存行中的数据,导致了CPU Cache命中失败的伪共享问题。
那么只需要避免多核CPU同时操作统一缓存行,不就可以解决这个问题了么?
事实上,Disruptor正是这么做的。
❝Disruptor为Sequence中的value(volatile修饰)进行了缓存行填充,保证每个sequence只在一个缓存行中存在,避免了其他的变量对sequence的干扰。
❞
classLhsPadding{protectedlongp1,p2,p3,p4,p5,p6,p7;//缓存行填充}classValueextendsLhsPadding{protectedvolatilelongvalue;}classRhsPaddingextendsValue{protectedlongp9,p10,p11,p12,p13,p14,p15;//缓存行填充}/***其他的缓存行填充机制Concurrentsequenceclassusedfortrackingtheprogressof*theringbufferandeventprocessors.Supportanumber*ofconcurrentoperationsincludingCASandorderwrites.**
Alsoattemptstobemoreefficientwithregardstofalse*sharingbyaddingpaddingaroundthevolatilefield.*/publicclassSequenceextendsRhsPadding{staticfinallongINITIAL_VALUE=-1L;privatestaticfinalUnsafeUNSAFE;privatestaticfinallongVALUE_OFFSET;static{UNSAFE=Util.getUnsafe();try{VALUE_OFFSET=UNSAFE.objectFieldOffset(Value.class.getDeclaredField("value"));}catch(finalExceptione){thrownewRuntimeException(e);}}
JDK1.8 提供了注解 「@Contended」 用于解决伪共享问题,需要注意的是,如果业务代码需要使用该注解,要添加JVM参数
❝-XX:-RestrictContended。
❞
默认填充宽度为128,若需要自定义填充宽度,则设置
❝-XX:ContendedPaddingWidth
❞
具体的使用方式为:
@sun.misc.ContendedpublicfinalstaticclassValue{publicvolatilelongvalue=0L;}参考资料《Java并发编程之美》并发编程网:剖析Disruptor:为什么会这么快?(二)神奇的缓存行填充
相关阅读
-
世界热推荐:今晚7:00直播丨下一个突破...
今晚19:00,Cocos视频号直播马上点击【预约】啦↓↓↓在运营了三年... -
NFT周刊|Magic Eden宣布支持Polygon网...
Block-986在NFT这样的市场,每周都会有相当多项目起起伏伏。在过去... -
环球今亮点!头条观察 | DeFi的兴衰与...
在比特币得到机构关注之后,许多财务专家预测世界将因为加密货币的... -
重新审视合作,体育Crypto的可靠关系才能双赢
Block-987即使在体育Crypto领域,人们的目光仍然集中在FTX上。随着... -
简讯:前端单元测试,更进一步
前端测试@2022如果从2014年Jest的第一个版本发布开始计算,前端开发... -
焦点热讯:刘强东这波操作秀
近日,刘强东发布京东全员信,信中提到:自2023年1月1日起,逐步为...