前言 现代处理器的缓存用法对于成功的高性能操作至关重要。目前来说,处理器在处理缓存中的数据和指令非常高效,相反的,当缓存缺失(cachemiss)时则非常低效。接下来了解下神奇的缓存行填充。 首先看什么是内存共享 (计算机基础)计算机早就支持多核,软件也越来越多的支持多核运行,其实也可以叫做多处理运行。一个处理器对应一个物理插槽。其中一个插槽对应一个L3 Cache,一个槽包含多个cpu。一个cpu包含寄存器、L1 Cache、L2Cache,如下图所示: 计算机CPU与缓存示意图 其中越靠近cpu则,速度越快,容量则越小。其中L1和L2是只能给一个cpu进行共享,但是L3是可以给同一个槽内的cpu共享,而主内存,是可以给所有的cpu共享,这就是内存的共享。 其中cpu执行运算的流程是这样:首先回去L1里面查找对应数据,如果没有则去L2、L3,如果都没有,则就会去主内存中去拿,走的路越长,则耗费时间越久,性能就会越低。 Martin和Mike的 QCon presentation 演讲中给出了一些缓存未命中的消耗数据: 从CPU到 大约需要的CPU周期 大约需要的时间 主存 约60-80纳秒 QPI 总线传输(between sockets, not drawn) 约20ns L3 cache 约40-45 cycles 约15ns L2 cache 约10 cycles 约3ns L1 cache 约3-4 cycles 约1ns 寄存器 1 cycle 如果你的目标是让端到端的延迟只有10毫秒,而其中花80纳秒去主存拿一些未命中数据的过程将占很重的一块。 缓存行 刚刚说的缓存失效其实指的是Cache line的失效,也就是缓存行,Cache是由很多个Cache line 组成的,每个缓存行大小是32~128字节(通常是64字节)。我们这里假设缓存行是64字节,而java的一个Long类型是8字节,这样的话一个缓存行就可以存8个Long类型的变量,如下图所示: 一个缓存对应的缓存行的结构图 cpu 每次从主内存中获取数据的时候都会将相邻的数据存入到同一个缓存行中。假设我们访问一个Long内存对应的数组的时候,如果其中一个被加载到内存中,那么对应的后面的7个数据也会被加载到对应的缓存行中,这样就会非常快的访问数据。 伪共享 刚我们说了缓存的失效其实就是缓存行的失效,缓存行失效的原理是什么,这里又涉及到一个MESI协议(缓存一致性协议),我们这里不介绍这个,感兴趣的可以查资料研究一下,首先我们用Disruptor中很经典的讲解伪共享的图来讲解下: 伪共享示意图 上图中显示的是一个槽的情况,里面是多个cpu, 如果cpu1上面的线程更新了变量X,根据MESI协议,那么变量X对应的所有缓存行都会失效,这个时候如果cpu2中的线程进行读取变量Y,发现缓存行失效,就会按照缓存查找策略,往上查找,如果cpu1对应的线程更新变量X后又访问了变量X,那么左侧的L1、L2和槽内的L3 缓存行都会得到生效。这个时候cpu2线程可以在L3 Cache 中得到生效的数据,否则的话(即cpu1对应的线程更新X后没有访问X)cpu2的线程就只能从主内存中获取数据,对性能就会造成很大的影响,这就是伪共享。 表面上 X 和 Y 都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享。 简言之: 无法充分使用缓存行特性的现象称为伪共享。 Disruptor如何来解决伪共享 class LhsPadding { protected long p1, p2, p3, p4, p5, p6, p7; } class Value extendsLhsPadding { protected volatile long value; } class RhsPadding extendsValue { protected long p9, p10, p11, p12, p13, p14,p15; } /** *Concurrent sequence class used fortracking the progress of * the ring buffer and event processors. Support a number * of concurrent operations including CAS andorder writes. * *
Also attempts to be more efficientwith regards to false * sharing by adding padding around thevolatile field. */ public class Sequenceextends 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); } } /** * Create a sequence initialised to -1. */ public Sequence() { this(INITIAL_VALUE); } 通过Sequence的一系列的继承关系可以看到,它真正的用来计数的域是value,在value的前后各有7个long型的填充值,(缓存行大小是按128字节处理的 8*16=128)这些值在这里的作用是做cpu cache line填充,防止发生伪共享。这种做法其实是以空间换时间的思想。 小贴士: 在jdk1.8中,有专门的注解@Contended来避免伪共享,更优雅地解决问题。 小结 1、理解内存共享、缓存行、伪共享的概念 2、如何来避免伪共享 3、避免伪共享可提高性能