太牛逼了!项目中用了 Disruptor 之后,性能提升了2.5倍

沙海
沙海
沙海
994
文章
2
评论
2021年4月12日05:22:24
评论
2 4984字阅读16分36秒
摘要

速读摘要

速读摘要

为了解决不同存储部件的速度不对等问题,让高速设备充分发挥性能,引入了多级缓存机制。一个处理器对应一个物理插槽,不同的处理器间通过QPI总线相连。此时Core-2就会很郁闷了,刚刚还能够从缓存中读取到对象bar,现在再读取却被告知缓存行失效,必须得去内存重新拉取,延缓了Core-2的执行效率。这里的V也不限定为long类型,其实只要对象的大小大于等于8个字节,通过前后各填充7个long型变量,就一定能够保证独占缓存行。

原文约 4314 | 图片 12 | 建议阅读 9 分钟 | 评价反馈

太牛逼了!项目中用了 Disruptor 之后,性能提升了2.5倍

点击关注 ? Java基基

点击上方“Java基基”,选择“设为星标”

做积极的人,而不是积极废人!

源码精品专栏

 

来源:jitwxs.cn/13836b16.html

大家好,我是基基。

太牛逼了!项目中用了 Disruptor 之后,性能提升了2.5倍

今儿我们来看看 Disruptor 组件。开始之前,我们先来一波科普。

存储设备往往是速度越快价格越昂贵,速度越快价格越低廉。在计算机中,CPU 的速度远高于主存的速度,而主存的速度又远高于磁盘的速度。为了解决不同存储部件的速度不对等问题,让高速设备充分发挥性能,引入了多级缓存机制。

为了解决内存和 CPU 的速度不匹配问题,相继引入了 L1 Cache、L2 Cache、L3 Cache,数字越小,容量越小,速度越快,位置越接近 CPU。

太牛逼了!项目中用了 Disruptor 之后,性能提升了2.5倍

现在的 CPU 都是由多个处理器,每个处理器由多个核心构成。一个处理器对应一个物理插槽,不同的处理器间通过 QPI 总线相连。一个处理器间的多核共享 L3 Cache。一个核包含寄存器、L1 Cache、L2 Cache,下图是Intel Sandy Bridge CPU架构:

太牛逼了!项目中用了 Disruptor 之后,性能提升了2.5倍

缓存行与伪共享

缓存中的数据并不是独立的进行存储的,它的最小存储单位是缓存行,缓存行的大小是2的整数幂个字节,最常见的缓存行大小是 64 字节。CPU 为了执行的高效,会在读取某个对象时,从内存上加载 64 的整数倍的长度,来补齐缓存行。

以 Java 的 long 类型为例,它是 8 个字节,假设我们存在一个长度为 8 的 long 数组 arr,那么CPU 在读取 arr[0] 时,首先查询缓存,缓存没有命中,缓存就会去内存中加载。由于缓存的最小存储单位是缓存行,64 字节,且数组的内存地址是连续的,则将 arr[0] 到 arr[7] 加载到缓存中。后续 CPU 查询 arr[6] 时候也可以直接命中缓存。

太牛逼了!项目中用了 Disruptor 之后,性能提升了2.5倍

现在假设多线程情况下,线程 A 的执行者 CPU Core-1 读取 arr[1],首先查询缓存,缓存没有命中,缓存就会去内存中加载。从内存中读取 arr[1] 起的连续的 64 个字节地址到缓存中,组成缓存行。由于从arr[1] 起,arr 的长度不足够 64 个字节,只够 56 个字节。假设最后 8 个字节内存地址上存储的是对象 bar,那么对象 bar 也会被一起加载到缓存行中。

太牛逼了!项目中用了 Disruptor 之后,性能提升了2.5倍

现在有另一个线程 B,线程 B 的执行者 CPU Core-2 去读取对象 bar,首先查询缓存,发现命中了,因为 Core-1 在读取 arr 数组的时候也顺带着把 bar 加载到了缓存中。

这就是缓存行共享,听起来不错,但是一旦牵扯到了写入操作就不妙了。

假设 Core-1 想要更新 arr[7] 的值,根据 CPU 的 MESI 协议,那么它所属的缓存行就会被标记为失效。因为它需要告诉其他的 Core,这个 arr[7] 的值已经被更新了,缓存已经不再准确了,你必须得重新去内存拉取。但是由于缓存的最小单元是缓存行,因此只能把 arr[7] 所在的一整行给标识为失效。

此时 Core-2 就会很郁闷了,刚刚还能够从缓存中读取到对象 bar,现在再读取却被告知缓存行失效,必须得去内存重新拉取,延缓了 Core-2 的执行效率。

这就是缓存伪共享问题,两个毫无关联的线程执行,一个线程却因为另一个线程的操作,导致缓存失效。这两个线程其实就是对同一缓存行产生了竞争,降低了并发性。

Disruptor 缓存行填充

Disruptor 为了解决伪共享问题,使用的方法是缓存行填充。这是一种以空间换时间的策略,主要思想就是通过往对象中填充无意义的变量,来保证整个对象独占缓存行。

举个例子,以 Disruptor 中的 Sequence 为例,在 volatile long value 的前后各放置了 7 个 long 型变量,确保 value 独占一个缓存行。

public class Sequence extends RhsPadding {    private static final long VALUE_OFFSET;    static {        VALUE_OFFSET = UNSAFE.objectFieldOffset(Value.class.getDeclaredField("value"));        ...    }    ...}class RhsPadding extends Value {    protected long p9, p10, p11, p12, p13, p14, p15;}class Value extends LhsPadding {    protected volatile long value;}class LhsPadding {    protected long p1, p2, p3, p4, p5, p6, p7;}

如下图所示,其中 V 就是 Value 类的 value,P 为 value 前后填充的无意义 long 型变量,U 为其它无关的变量。不论什么情况下,都能保证 V 不和其他无关的变量处于同一缓存行中,这样 V 就不会被其他无关的变量所影响。

太牛逼了!项目中用了 Disruptor 之后,性能提升了2.5倍

Padding 填充

这里的 V 也不限定为 long 类型,其实只要对象的大小大于等于8个字节,通过前后各填充 7 个 long 型变量,就一定能够保证独占缓存行。

此处以 Disruptor 的 RingBuffer 为例,最左边的 7 个 long 型变量被定义在顶级父类 RingBufferPad 中,最右边的 7 个 long 型变量被定义在 RingBuffer 的最后一行变量定义中,这样所有的需要独占的变量都被左右 long 型给包围,确保会独占缓存行。

public final class RingBuffer<E> extends RingBufferFields<E> implements Cursored, EventSequencer<E>, EventSink<E> {    public static final long INITIAL_CURSOR_VALUE = Sequence.INITIAL_VALUE;    protected long p1, p2, p3, p4, p5, p6, p7;    ...}abstract class RingBufferFields<E> extends RingBufferPad{    ...}abstract class RingBufferPad {    protected long p1, p2, p3, p4, p5, p6, p7;}

@Contended

在 JDK 1.8 中,提供了 @sun.misc.Contended 注解,使用该注解就可以让变量独占缓存行,不再需要手动填充了。注意,JVM 需要添加参数 -XX:-RestrictContended 才能开启此功能。

如果该注解被定义在了类上,表示该类的每个变量都会独占缓存行;如果被定义在了变量上,通过指定 groupName,相同的 groupName 会独占同一缓存行。

// 类前加上代表整个类的每个变量都会在单独的cache line中@sun.misc.Contendedpublic class ContendedData {    int value;    long modifyTime;    boolean flag;    long createTime;    char key;}// 同一 groupName 在同一缓存行public class ContendedGroupData {    @sun.misc.Contended("group1")    int value;    @sun.misc.Contended("group1")    long modifyTime;    @sun.misc.Contended("group2")    boolean flag;    @sun.misc.Contended("group3")    long createTime;    @sun.misc.Contended("group3")    char key;}

@Contended 在 JDK 源码中已经有所应用,以 Thread 类为例,为了保证多线程情况下随机数的操作不会产生伪共享,相关的变量被设置为同一 groupName。

public class Thread implements Runnable {    ...    // The following three initially uninitialized fields are exclusively    // managed by class java.util.concurrent.ThreadLocalRandom. These    // fields are used to build the high-performance PRNGs in the    // concurrent code, and we can not risk accidental false sharing.    // Hence, the fields are isolated with @Contended.    /** The current seed for a ThreadLocalRandom */    @sun.misc.Contended("tlr")    long threadLocalRandomSeed;    /** Probe hash value; nonzero if threadLocalRandomSeed initialized */    @sun.misc.Contended("tlr")    int threadLocalRandomProbe;    /** Secondary seed isolated from public ThreadLocalRandom sequence */    @sun.misc.Contended("tlr")    int threadLocalRandomSecondarySeed;    ...}

速度测试

将 volatile long value 封装为对象,四线程并行,每个线程循环 1 亿次,对 value 进行更新操作,测试缓存行对速度的影响。

CPU:AMD 3600 3.6 GHz,Memory:16 GB

太牛逼了!项目中用了 Disruptor 之后,性能提升了2.5倍

欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

太牛逼了!项目中用了 Disruptor 之后,性能提升了2.5倍

已在知识星球更新源码解析如下:

太牛逼了!项目中用了 Disruptor 之后,性能提升了2.5倍

太牛逼了!项目中用了 Disruptor 之后,性能提升了2.5倍

太牛逼了!项目中用了 Disruptor 之后,性能提升了2.5倍

太牛逼了!项目中用了 Disruptor 之后,性能提升了2.5倍

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 20 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。谢谢支持哟 (*^__^*)

阅读原文

继续阅读
weinxin
资源分享QQ群
本站是一个IT技术分享社区, 会经常分享资源和教程; 分享的时代, 请别再沉默!
沙海
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: