Cache Line对数据读写性能的影响

   日期:2020-10-08     浏览:100    评论:0    
核心提示:文章目录多级缓存-填补内存读写速度与CPU计算速度的鸿沟局部性原理与Cache Line伪共享对齐填充@Contended备注尾巴对于一个程序来说,几乎所有的计算任务都不可能仅通过CPU的计算就可以完成,它至少要和内存打交道:读取运算数据、写入运算结果。现代CPU的算力已经十分强大,相比之下存储设备的IO读写速度却发展的十分缓慢。通常情况下,内存每完成一次读写操作,CPU已经可以进行上百次的运算,为了填补两者速度上的鸿沟,现代CPU不得不加入一层或多层读写速度接近于CPU处理速度的高速缓存Cache。C

文章目录

  • 多级缓存-填补内存读写速度与CPU计算速度的鸿沟
  • 局部性原理与Cache Line
  • 伪共享
    • 对齐填充
    • @Contended
    • 备注
  • 尾巴

对于一个程序来说,几乎所有的计算任务都不可能仅通过CPU的计算就可以完成,它至少要和内存打交道:读取运算数据、写入运算结果。

现代CPU的算力已经十分强大,相比之下存储设备的IO读写速度却发展的十分缓慢。通常情况下,内存每完成一次读写操作,CPU已经可以进行上百次的运算,为了填补两者速度上的鸿沟,现代CPU不得不加入一层或多层读写速度接近于CPU处理速度的高速缓存Cache。CPU会将计算需要的数据读取到缓存中,让计算可以快速进行,当计算结束后,再将计算结果统一写回到主存中,这样CPU就不需要缓慢的等待内存读写了。

多级缓存-填补内存读写速度与CPU计算速度的鸿沟

笔者画了一个简图,大概表示现代CPU的缓存架构,如下:

各级缓存的读写速度如下:

缓存 时钟周期(大约) 时间(大约)
主存 - 80ns
L3 40 15ns
L2 10 3ns
L1 3~4 1ns
寄存器 1 -

越靠近CPU的缓存读写速度越快,相应的 容量越小、且成本越高。
当CPU需要读取数据时,首先从最近的缓存开始找,找不到就逐层往上寻找,如果命中缓存,就无需再从主存中去读取,直接拿来计算。反之从主存中加载数据并依次写入多级缓存中,下次就可以直接从缓存中读数据了。

局部性原理与Cache Line

CPU访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中。

  • 当我们需要从内存中读取一个int变量i的值时,CPU真的只会仅仅将这个4字节的i加载到缓存吗?
    答案是:NO!!!

当我们去读取一个4字节的int变量时,计算机认为程序接下来很大概率会访问相邻的数据,于是会把相邻的数据给一块儿加载到缓存中,下次再读取时,就不用访问主存了,直接从缓存中读取就可以了,减少了CPU访问主存的次数,提高缓存的命中率。

说白了,CPU读取数据时,总是会一个块一个块的读,哪怕你需要的仅仅是1个字节的数据,这个【块】就被称为【Cache Line】。
不同的CPU,Cache Line大小是不一样的,Intel的CPU大部分都是64字节。

多级缓存就是由若干个Cache Line组成的,CPU每次从主存中拉取数据时,会把相邻的数据也存入同一个Cache Line。

如下测试代码,各自读取一千万次数据,Cache Line失效的耗时11ms,能很好的利用Cache Line特性的只需要3ms。

public class CacheLine { 
	static int length = 8 * 10000000;
	static long[] arr = new long[length];

	public static void main(String[] args) { 
		long temp;// 无特殊含义,读取出来的数据赋值

		// 1.每次读取都跳8个,下次读取的数据一定不在上次读取的Cache Line中,缓存全部未命中
		long start = System.currentTimeMillis();
		for (int i = 0; i < length; i += 8) { 
			temp = arr[i];
		}
		long end = System.currentTimeMillis();
		System.out.println(end - start);// 11ms

		// 2.顺序读取,Cache Line生效,只读前8分之1的数据
		start = System.currentTimeMillis();
		for (int i = 0; i < length / 8; i++) { 
			temp = arr[i];
		}
		end = System.currentTimeMillis();
		System.out.println(end - start);// 3ms
	}
}

伪共享

当多个线程去同时读写共享变量时,由于缓存一致性协议,只要Cache Line中任一数据失效,整个Cache Line就会被置为失效。这就会导致本来相互不影响的数据,由于被分配在同一个Cache Line中,双方在写数据时,导致对方的Cache Line不断失效,无法利用Cache Line缓存特性的现象就被称为【伪共享】。

如下代码,启动两个线程,分别修改共享变量a和b,由于a个b一共占用16字节,可以被分配进同一个Cache Line中,本来互相不影响的两个线程修改数据,但是由于a个b被分配到同一个Cache Line中,导致对方的Cache Line不断失效,不断的重新发起load指令重主存中重新加载数据,降低程序的性能。

public class FalseShare { 
	static volatile long a;
	static volatile long b;

	public static void main(String[] args) throws InterruptedException { 
		CountDownLatch cdl = new CountDownLatch(2);

		long t1 = System.currentTimeMillis();
		new Thread(()->{ 
			for (long i = 0; i < 1_0000_0000L; i++) { 
				// 线程只改a
				FalseShare.a = i;
			}
			cdl.countDown();
		}).start();
		
		new Thread(()->{ 
			for (long i = 0; i < 1_0000_0000L; i++) { 
				// 线程只改b
				FalseShare.b = i;
			}
			cdl.countDown();
		}).start();
		cdl.await();
		long t2 = System.currentTimeMillis();
		System.err.println(t2 - t1);
	}
}

程序运行结果:耗时2782ms。

对齐填充

要想解决上面的伪共享问题也很简单,既然一个Cache Line存放64字节的数据,只要在a和b变量之间填充7个无意义的Long变量,占满64字节,这样a和b就无法被分配进同一个Cache Line中,线程之间修改数据互不影响,就没有上面的问题了。

解决方法如下:

程序运行结果:耗时752ms。

@Contended

Cache Line对齐其实是一种比较low的解决办法,因为你无法判断你写的程序会被放到哪种CPU上运行,不同的CPU它的Cache Line大小是不一样的,如果超过了64字节,填充7个long变量就没有效果了,还有没有更好的解决办法呢???

JDK8引入了一个新的注解@Contended,被它修饰的变量,会被存放到一个单独的Cache Line中,不会和其他变量共享Cache Line。

修改后如下:

程序运行结果:耗时744ms,Cache Line是生效的。

备注

JDK8的老版本中@Contended默认是禁用的,需要手动开启:-XX:-RestrictContended。笔者的JDK版本为【1.8.0_191】-XX:-RestrictContended参数已经没有用了,默认都会开启对@Contended注解的支持。

尾巴

理解硬件设计对编写高性能程序是有必要的!!!

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服