硬件级别的缓存对程序的影响
JVM-合并写(write combining)
当cpu修改了某值后会把数据线存入L1中,这个时候可能没有命中,则会一往下查找,会写入到L2中,此时,由于往L2中写的时候需要大量的时间,同时这个变量还可能继续被修改,此时会用到合并写的技术,所谓合并写就是把两次的写的结果一次进行写出。
这里会用到合并写的地址空间(WCBuffer),在64位的操作系统中一共4个字节,也就是如果我们的修改在4个字节以内,则会使用到合并写的技术,如果超过4个字节,则无法使用到合写的红利。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| final class WriteCombining {
private static final int ITERATIONS = Integer.MAX_VALUE; private static final int ITEMS = 1 << 24; private static final int MASK = ITEMS - 1;
private static final byte[] arrayA = new byte[ITEMS]; private static final byte[] arrayB = new byte[ITEMS]; private static final byte[] arrayC = new byte[ITEMS]; private static final byte[] arrayD = new byte[ITEMS]; private static final byte[] arrayE = new byte[ITEMS]; private static final byte[] arrayF = new byte[ITEMS];
public static void main(final String[] args) {
for (int i = 1; i <= 3; i++) { System.out.println(i + " SingleLoop duration (ns) = " + runCaseOne()/10000); System.out.println(i + " SplitLoop duration (ns) = " + runCaseTwo()/10000); } }
public static long runCaseOne() { long start = System.nanoTime(); int i = ITERATIONS;
while (--i != 0) { int slot = i & MASK; byte b = (byte) i; arrayA[slot] = b; arrayB[slot] = b; arrayC[slot] = b; arrayD[slot] = b; arrayE[slot] = b; arrayF[slot] = b; } return System.nanoTime() - start; }
public static long runCaseTwo() { long start = System.nanoTime(); int i = ITERATIONS; while (--i != 0) { int slot = i & MASK; byte b = (byte) i; arrayA[slot] = b; arrayB[slot] = b; arrayC[slot] = b; } i = ITERATIONS; while (--i != 0) { int slot = i & MASK; byte b = (byte) i; arrayD[slot] = b; arrayE[slot] = b; arrayF[slot] = b; } return System.nanoTime() - start; }
|
1 2
| 1 SingleLoop duration (ns) = 428328 1 SplitLoop duration (ns) = 415759
|
runCaseTwo
runCaseTwo写的过程是一次四个字节写入,注意b占一个字节
runCaseOne
runCaseOne写的过程是第一次循环下来写入四个字节,然后将剩余的三个字节放入到wc缓存中,等待第二次while的进入,第二次循环进入后在分割出一个字节放入wc中写入告诉缓存,一次类推,所以它比较慢。
JVM-Cache Line、缓存对齐、伪共享
由于寄存器的速度是非常快的,是内存的100被,是硬盘的10的六次方倍。
所以cpu读取数据,吸纳从寄存器中读取,如果无,则一次L1\L2\L3读取。
cache line 缓存行
那么系统读取数据是需要什么读取什么吗?当然是的,但是由于缓存行的存在,他会都去更多的数据。比如读取一个int类型4个字节的数据,他会把这四个字节后面的60个字节都读进去,即每次读64个字节的数据。这就是缓存行cache line
场景:
在多核cpu读取数据的时候:
core1 读取了x=1的数据,不好意思,由于cache line的存在,他需要把后面的y=2联通后面的数据一读进core1中;
core2 读取了y=2的数据,不好意思,由于cache line的存在,他需要把后面的x=1联通后面的数据一读进core2中;
此时core1 对x=1做了运算是的x=11
此时core2 对y=2做了运算是的x=22
下你在在core1和core2中的数据如下:
我们发现出现了数据不一致的问题。关于数据不一致问题,这就是伪共享的问题
。
缓存对齐可以提高效率
知道了cache line的存在,我们在写代码的时候可以利用缓存对齐(就是每次64字节)的方式去提供效率,真的可以吗?
如果每次不是64个字节,需要等待到了64个字节才会写入缓存哦!中间有等待的时间,是的效率下降。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public class One { private static class T { public volatile long x = 0L; }
public static T[] arr = new T[2];
static { arr[0] = new T(); arr[1] = new T(); }
public static void main(String[] args) throws Exception { Thread t1 = new Thread(()->{ for (long i = 0; i < 1000_0000L; i++) { arr[0].x = i; } });
Thread t2 = new Thread(()->{ for (long i = 0; i < 1000_0000L; i++) { arr[1].x = i; } });
final long start = System.nanoTime(); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println((System.nanoTime() - start)/100_0000); } }
|
缓存对齐时效率高,不存在伪共享
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public class Two { private static class Padding { public volatile long p1, p2, p3, p4, p5, p6, p7; }
private static class T extends Padding { public volatile long x = 0L; }
public static T[] arr = new T[2];
static { arr[0] = new T(); arr[1] = new T(); }
public static void main(String[] args) throws Exception { Thread t1 = new Thread(()->{ for (long i = 0; i < 1000_0000L; i++) { arr[0].x = i; } });
Thread t2 = new Thread(()->{ for (long i = 0; i < 1000_0000L; i++) { arr[1].x = i; } });
final long start = System.nanoTime(); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println((System.nanoTime() - start)/100_0000); } }
|
伪共享问题引出数据一致性问题
数据一致性问题解决方案
针对一致性的问题有两种解决方案:
总线锁:在L3和L2直接加锁,拿到锁才能处理下面工作。
一致性协议MESI(缓存一致性协议)