转载于
https://www.cnblogs.com/tomakemyself/p/14086547.html
前言现代计算机通常由CPU,以及主板、内存、硬盘等主要硬件结构组成,而决定计算机性能的最核心部件是CPU 内存,CPU负责处理程序指令,内存负责存储指令执行结果。在这个工作机制当中CPU的读写效率其实是远远高于内存的,为提升执行效率减少CPU与内存的交互,一般在CPU上设计了缓存结构,常见的为三级缓存结构:
- L1 Cache,分为数据缓存和指令缓存,逻辑核独占
- L2 Cache,物理核独占,逻辑核共享
- L3 Cache,所有物理核共享
下图为CPU-Core(TM)I7-10510U型号缓存结构
存储器存储空间大小:内存>L3>L2>L1>寄存器。
存储器速度快慢排序:寄存器>L1>L2>L3>内存。
缓存行大小[root@192 ~]# getconf -a|grep CACHE
LEVEL1_ICACHE_SIZE 32768 #L1缓存大小
LEVEL1_ICACHE_ASSOC 8 #L1缓存行大小
LEVEL1_ICACHE_LINESIZE 64
LEVEL1_DCACHE_SIZE 32768
LEVEL1_DCACHE_ASSOC 8
LEVEL1_DCACHE_LINESIZE 64
LEVEL2_CACHE_SIZE 262144 #L2缓存大小
LEVEL2_CACHE_ASSOC 4
LEVEL2_CACHE_LINESIZE 64 #L2缓存行大小
LEVEL3_CACHE_SIZE 8388608 #L3缓存大小
LEVEL3_CACHE_ASSOC 16
LEVEL3_CACHE_LINESIZE 64 #L3缓存行大小
LEVEL4_CACHE_SIZE 0
LEVEL4_CACHE_ASSOC 0
LEVEL4_CACHE_LINESIZE 0
[root@192 ~]# cat /proc/cpuinfo |grep -i cache
cache size : 8192 KB
cache_alignment : 64
cache size : 8192 KB
cache_alignment : 64
JAVA程序毫无疑问也必须是运行在硬件机器之上,如何利用底层硬件工作原理,提升性能也必然是我们需要考虑的,笔者今天以无锁并发高性能框架Disruptor为例分析如何高效的利用CPU缓存。
Who is Disruptor? Disruptor是一个开源框架,研发的初衷是为了解决高并发下队列锁的问题,最早由LMAX(一种新型零售金融交易平台)提出并使用,能够在无锁的情况下实现队列的并发操作,并号称能够在一个线程里每秒处理6百万笔订单。
缓存行填充下方示例为Disruptor框架的内部代码:
abstract class RingBufferPad
{
protected long p1, p2, p3, p4, p5, p6, p7;
}
分析:
- 变量p1~p7本身没有实际意义,只能用于缓存行填充,为了尽可能地用上CPU Cache!
- 访问CPU里的L1 Cache或者L2 Cache、L3 Cache,访问延时是内存的1/15乃至1/100(内存的访问速度,是远远慢于CPU Cache的)因此,为了追求极限性能,需要尽可能地从CPU Cache里面读取数据
- CPU Cache装载内存里面的数据,不是一个个字段加载的,而是加载一整个缓存行64位的Intel CPU,缓存行通常是64 Bytes,一个long类型的数据需要8 Bytes,因此会一下子加载8个long类型的数据
- 遍历数组元素速度很快,后面连续7次的数据访问都会命中CPU Cache,不需要重新从内存里面去读取数据
p1-p7仅用来填充缓存行,我们根本用不到它,但是我们为什么要填充满一个缓存行呢?
- CPU在加载数据的时候,会把这个数据从内存加载到CPU Cache里面
- 此时,CPU Cache里面除了这个数据,还会加载这个数据前后定义的其他变量释义:在高并发场景下,假定并发访问变量p0,在p0后定义的其它变量也一并会被缓存load
- Disruptor是一个多线程的服务器框架,在这个数据前后定义的其他变量,可能会被多个不同的线程去更新数据,读取数据这些写入和读取请求,可能会来自于不同的CPU Core为了保证数据的同步更新,不得不把CPU Cache里面的数据,重新写回到内存里面或者重新从内存里面加载CPU Cache的写回和加载,都是以整个Cache Line作为单位的
- 如果常量的缓存失效,当再次读取这个值的时候,需要重新从内存读取,读取速度会大大变慢
abstract class RingBufferPad
{
protected long p1, p2, p3, p4, p5, p6, p7;
}
abstract class RingBufferFields<E> extends RingBufferPad
{
...
private final long indexMask;
private final Object[] entries;
protected final int bufferSize;
protected final Sequencer sequencer;
...
}
public final class RingBuffer<E> extends RingBufferFields<E> implements Cursored, EventSequencer<E>, EventSink<E>
{
...
protected long p1, p2, p3, p4, p5, p6, p7;
...
}
- Disruptor在RingBufferFields里面定义的变量前后分别定义了7个long类型的变量前面7个继承自RingBufferPad,后面7个直接定义在RingBuffer类中这14个变量没有任何实际用途,既不会去读,也不会去写
- RingBufferFields里面定义的变量都是final的,第一次写入之后就不会再进行修改一旦被加载到CPU Cache之后,只要被频繁地读取访问,就不会被换出CPU Cache无论在内存的什么位置,这些变量所在的Cache Line都不会有任何写更新的请求
- Disruptor整个框架是一个高速的生产者-消费者模型下的队列生产者不停地往队列里面生产新的需要处理的任务消费者不停地从队列里面处理掉这些任务
- 要实现一个队列,最合适的数据结构应该是链表,如Java中的LinkedBlockingQueue
- Disruptor并没有使用LinkedBlockingQueue,而是使用了RingBuffer的数据结构RingBuffer的底层实现是一个固定长度的数组比起链表形式的实现,数组的数据在内存里面会存在空间局部性数组的连续多个元素会一并加载到CPU Cache里面,所以访问遍历的速度会更快链表里面的各个节点的数据,多半不会出现在相邻的内存空间数据的遍历访问还有一个很大的优势,就是CPU层面的分支预测会很准确可以更有效地利用CPU里面的多级流水线