伪共享之遭遇&解决

531 阅读3分钟

伪共享之遭遇&解决

前言

上一篇我们看了什么是缓存行,什么是MESI协议。这一篇我们来看一下伪共享。

遭遇伪共享

之前我们已经说过了,CPU的缓存是以cache line作为单位存储的,那么我们考虑一种情况,如果多个线程在操作同一个实例的不同成员变量,但是这两个变量存在于同一个缓存行里,会发生什么?

会发生两个线程要不停地去获取这个缓存行的所有权,一个使用另外一个就只能等。这是效率非常慢的。这种情况下,就是发生了伪共享。

爬一张Disruptor的图:

|center|

一个运行在处理器core1上的线程想要更新变量X的值,同时另外一个运行在处理器core2上的线程想要更新变量Y的值。但是,这两个频繁改动的变量都处于同一条缓存行。两个线程就会轮番发送RFO消息,占得此缓存行的拥有权。当core1取得了拥有权开始更新X,则core2对应的缓存行需要设为I状态(关于MESI四种状态,看我的上一篇文章)。当core2取得了拥有权开始更新Y,则 core1 对应的缓存行需要设为I状态。

再举个JAVA开发中的栗子~

比如我自己写个阻塞队列。头结点如果和尾节点在同一个缓存行里,就可能会伪共享。在多个线程频繁的offer/take的时候,就会一定程度上的影响效率。

解决伪共享问题

这里同样看一下Disruptor的解决方式。

看下RingBuffer的继承关系:

|center|

看下RingBufferPad

abstract class RingBufferPad
{
//仅仅填充使用
protected long p1, p2, p3, p4, p5, p6, p7;
}

再看看RingBuffer

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;

//sth...
}

依据JVM对象继承关系中父类属性与子类属性,内存地址连续排列布局,RingBufferPadprotected long p1,p2,p3,p4,p5,p6,p7;作为缓存前置填充,RingBuffer中的protected long p1,p2,p3,p4,p5,p6,p7;作为缓存后置填充。这样任意线程访问RingBuffer时,RingBuffer放在父类RingBufferFields的属性,都是独占一行cache line不会产生伪共享问题。如图,RingBuffer的操作字段在RingBufferFields中,使用rbf标识:

|center|

按照一行缓存64字节计算,前后填充56字节(7个long),中间大于等于8字节的内容都能独占一行cache line,而RingBufferFields是大于8字节的:

abstract class RingBufferFields<E> extends RingBufferPad
{
private static final int BUFFER_PAD;
private static final long REF_ARRAY_BASE;
private static final int REF_ELEMENT_SHIFT;
private static final Unsafe UNSAFE = Util.getUnsafe();
private final long indexMask;
//这里是真正存放事件的地方
private final Object[] entries;
protected final int bufferSize;
protected final Sequencer sequencer;

//sth...
}

注意
由于某些 Java 编译器的优化策略,那些没有使用到的补齐数据可能会在编译期间被优化掉,我们可以在程序中加入一些代码防止被编译优化。如下:

public static long preventFromOptimization(VolatileLong v) {  
return v.p1 + v.p2 + v.p3 + v.p4 + v.p5 + v.p6;
}

如何看待伪共享

伪共享是很隐蔽的,我们暂时无法从系统层面上通过工具来探测伪共享事件。其次,不同类型的计算机具有不同的微架构(如 32 位系统和 64 位系统的 java 对象所占自己数就不一样),如果设计到跨平台的设计,那就更难以把握了,一个确切的填充方案只适用于一个特定的操作系统。还有,缓存的资源是有限的,如果填充会浪费珍贵的 cache 资源,并不适合大范围应用。

综上所述,并不是每个系统都适合花大量精力去解决潜在的伪共享问题。如果真的需要解决,那么一定要做充分的测试,保证你的填充是有效地而不是白白浪费了缓存。