Disruptor高性能之道-缓存行填充
共 2749字,需浏览 6分钟
·
2022-03-02 01:44
在上一篇文章中,我们介绍了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中的缓存。
❝所谓的“伪”共享,就可以理解成是一种 “错误”的共享,这种共享如果不发生,则多核CPU操作缓存行互不影响,每个核心都只关心自己操作的变量,而不会因为读写自己关心的变量而影响到其他CPU对变量的读写。
❞
Disruptor是如何进行缓存行填充的?
❝Disruptor解决伪共享的方式为:使用缓存行填充。
❞
上文我们提到,由于多核CPU同时读写统一缓存行中的数据,导致了CPU Cache命中失败的伪共享问题。
那么只需要避免多核CPU同时操作统一缓存行,不就可以解决这个问题了么?
事实上,Disruptor正是这么做的。
❝Disruptor为Sequence中的value(volatile修饰)进行了缓存行填充,保证每个sequence只在一个缓存行中存在,避免了其他的变量对sequence的干扰。
❞
class LhsPadding
{
protected long p1, p2, p3, p4, p5, p6, p7; // 缓存行填充
}
class Value extends LhsPadding
{
protected volatile long value;
}
class RhsPadding extends Value
{
protected long p9, p10, p11, p12, p13, p14, p15; // 缓存行填充
}
/**
* Concurrent sequence class used for tracking the progress of
* the ring buffer and event processors. Support a number
* of concurrent operations including CAS and order writes.
*
*
Also attempts to be more efficient with regards to false
* sharing by adding padding around the volatile field.
*/
public class Sequence extends RhsPadding
{
static final long INITIAL_VALUE = -1L;
private static final Unsafe UNSAFE;
private static final long VALUE_OFFSET;
static
{
UNSAFE = Util.getUnsafe();
try
{
VALUE_OFFSET = UNSAFE.objectFieldOffset(Value.class.getDeclaredField("value"));
}
catch (final Exception e)
{
throw new RuntimeException(e);
}
}
其他的缓存行填充机制
JDK1.8 提供了注解 「@Contended」 用于解决伪共享问题,需要注意的是,如果业务代码需要使用该注解,要添加JVM参数
❝-XX:-RestrictContended。
❞
默认填充宽度为128,若需要自定义填充宽度,则设置
❝-XX:ContendedPaddingWidth
❞
具体的使用方式为:
@sun.misc.Contended
public final static class Value {
public volatile long value = 0L;
}
参考资料
《Java并发编程之美》 并发编程网:剖析Disruptor:为什么会这么快?(二)神奇的缓存行填充