存储器的层次结构:
存储器是分层次的,离CPU越近的存储器,速度越快,每字节的成本越高,同时容量也越小。寄存器速度最快,离CPU最近,成本最高,所以个数容量有限,其次是高速缓存(缓存也是分级,有L1,L2,L3等缓存),再次是主存(普通内存),然后是本地磁盘,最次是远程文件存储。
缓存 | Register | L1 cache | L2 cache | L3 cache | Main Memory | 硬盘 |
---|---|---|---|---|---|---|
访问时间 | < 1ns | 约1ns | 约3ns | 约15ns | 约80ns | 约2ms |
典型容量 | 几十~几百B | 几十~几百KB | 几百KB | 几百KB~几MB | 几GB | 几百GB~几TB |
Cache Line:
高速缓存其实就是一组称之为cache line的固定大小的数据块,其大小是以突发读或者突发写周期的大小为基础的。即使处理器只存取一个字节的存储器,高速缓存控制器也启动整个存取器访问周期并请求整个数据块。缓存行第一个字节的地址总是突发周期尺寸的倍数。缓存行的起始位置总是与突发周期的开头保持一致。当从内存中取单元到cache中时,会一次取一个cache line大小的内存区域到cache中,然后存进相应的cache line中。
如图,两颗CPU(核)读取数据,如CPU1读X,CPU2读Y,Y与X处于同一个cache line,CPU1和CPU2会将X和Y所在的cache line全部读进来,ALU与Register在读取数据时,依次从L1、L2、L3、Main Memory中找数据,找到后按照相反的顺序依次缓存数据,该过程会有损失,但是使用缓存的损失更低。如果两颗CPU中间的数据需要保持一致性,如CPU1中X被修改,就必须告知CPU2中X已经过时,需要重新从内存中读取,CPU内部的数据同步过程以cache line为单位。这个协议称为缓存一致性协议,Intel采用的缓存一致性协议称为MESI Cache
一致性协议。MESI 是指4中状态的首字母。每个Cache line有4个状态,可用2个bit表示,它们分别是:
状态 | 描述 | 监听任务 |
---|---|---|
Modify(修改) | 该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 | 缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。 |
Exclusive(独享、互斥) | 该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。 | 缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。 |
Shared(共享) | 该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。 | 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。 |
Invalid(无效) | 该Cache line无效。 | 无 |
MESI Cache一致性协议是缓存锁的实现之一,还有MSI、MOSI、Synapse、Firefly、Dragon。有些无法被缓存的数据,或者跨越多个cache line的数据,依然必须使用总线锁,效率较低。下表示意了,当一个cache line调整状态的时候,另外一个cache line 需要调整的状态。
Modify | Exclusive | Shared | Invalid | |
---|---|---|---|---|
Modify | X | X | X | √ |
Exclusve | X | X | X | √ |
Shared | X | X | √ | √ |
Invalid | √ | √ | √ | √ |
cache line越大,局部性空间效率越高,但读取时间慢;cache line越小,局部性空间效率越低,但读取时间快;取一个折中值,目前多用64字节。下列程序展示修改的两个数在不在同一个cache line情况下的时间消耗。
// 在同一cache line
public class SameCacheLine {
public static volatile long[] array = new long[2];
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 10_0000_0000L ; i++) {
array[0] = i;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10_0000_0000L ; i++) {
array[1] = i;
}
});
final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime()-start)/100_0000);
}
}
// 输出:4366
----------------------------------------------------------------------------
// 不在同一cache line
public class DifferentCacheLine {
public static volatile long[] array = new long[16];
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 10_0000_0000L ; i++) {
array[0] = i;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10_0000_0000L ; i++) {
array[8] = i;
}
});
final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime()-start)/100_0000);
}
}
// 输出:2276
cache line对齐:
对于有些特别敏感的数字,会存在线程高竞争的访问,为了保证不发生伪共享,可以使用缓存航对齐的编程方式。在著名的框架Disruptor中就应用了cache line对齐,如下所示:
public long p1, p2, p3, p4, p5, p6, p7; // cache line padding
private volatile long cursor = INITIAL_CURSOR_VALUE;
public long p8, p9, p10, p11, p12, p13, p14; // cache line padding
JDK7中,很多采用long padding提高效率;JDK8,加入了@Contended
注解,被注解的类属性不与该类的其他属性处于同一cache line,需要加上:JVM -XX:-RestrictContended
。因为前后加上7个long类型进行padding只对Inter的CPU有效,通过使用@Contended
注解,虚拟机可以自动匹配CPU实现cache line对齐。
public class ContendedCacheLine {
@Contended
volatile long x;
@Contended
volatile long y;
public static void main(String[] args) throws InterruptedException {
ContendedCacheLine cacheLine = new ContendedCacheLine();
Thread t1 = new Thread(()->{
for (int i = 0; i < 10_0000_0000L ; i++) {
cacheLine.x = i;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10_0000_0000L ; i++) {
cacheLine.y = i;
}
});
final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime()-start)/100_0000);
}
}
// 输出:
加上@Contended:5399
不加@Contended: 28908