「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
局部性原理
程序在访问数据时,都趋于聚集在一片连续的区域中,这被称为局部性原理。
按时间和空间划分为两类:
时间局部性:如果一个数据正在被访问,那么近期它很可能再次被访问。
空间局部性:如果某一个位置的数据被访问,那么这个位置附近的数据很可能被访问。
针对局部性原理,CPU和操作系统都有具体的实现。
本文主要总结梳理CPU和操作系统的局部性原理在Java后端中的影响与意义。
CPU空间局部性
如下图是Java的内存模型
我们知道CPU为提高从内存中读数据的性能,有L1、L2、L3三个级别的高速缓存。
CPU利用局部性原理,在从内存读取数据项到缓存时,将该内存附近的数据块也一并读取到缓存中,这一过程称为预读,也叫共享。
缓存行cache line
CPU缓存是由很多个cache line为单元组成,每个cache line通常是64字节。
CPU每次从主存中拉取数据时,会把目标数据相邻的数据也存入同一个cache line内。
即读取连续空间的内存要比内存随机访问的性能要高,这一点用Java程序可以证明。
public static void main(String[] args) {
int[][] arr = new int[10000][10000];
int sum = 0;
long startTime = System.currentTimeMillis();
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[0].length; j++) {
sum += arr[i][j];
}
}
System.out.println("数组顺序访问耗时:" + (System.currentTimeMillis() - startTime) + "ms");
sum = 0;
startTime = System.currentTimeMillis();
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[0].length; j++) {
sum += arr[j][i];
}
}
System.out.println("数组非顺序访问耗时:" + (System.currentTimeMillis() - startTime) + "ms");
}
这是一段对二维数组循环读取的代码。
程序的上半部分是按数组的第二维开始顺序读取,即二维数组逐行按内存连续空间顺序访问。
下半部分则是按数组的第一维按列读取,不是顺序访问。
分别经过10000*10000次的数组访问后,其运行结果如下:
由此可见,对内存的顺序访问性能优于随机访问。
缓存行的伪共享
以ArrayBlockingQueue为例:
ArrayBlockingQueue有三个成员变量:
- takeIndex:需要被取走的元素下标
- putIndex:可被元素插入的位置的下标
- count:队列中元素的数量
这三个变量很容易放到一个缓存行中,但是它们之间修改没有太多关联。所以每次修改,都会使之前缓存的数据失效,从而不能完全达到共享的效果。
这种无法充分使用缓存行特性的现象,称为伪共享。
解决伪共享的方式一般有两种:
- padding:空间换时间,通过数据填充的方式,让频繁变更的数据尽量不在同一个缓存行中
- @Contented注解:Java中的这个注解通知JVM应当将字段放入不同的缓存行
磁盘空间局部性
在Java日常开发中,很多的中间件都需要跟磁盘文件打交道,这些磁盘数据的高性能访问也都依托于局部性原理,比如:
- MySql的日志文件
- MQ消息数据
我们知道MySql的数据最终都保存在磁盘中,为减少磁盘IO提高性能,InnoDB引擎底层依托BufferPool+redo log机制来提高mySql读写性能(具体可参考MySql原理总结)。而针对redo log、undo log、binlog的读写避免不了磁盘IO,那么这里就利用操作系统的PageCache机制,对磁盘数据顺序读写,使得磁盘IO的性能近乎于内存性能。
我们常说kafka和rocketMQ是高性能的消息中间件,其中一部分高性能就依托于对磁盘文件的顺序读写。比如commit log的顺序写入,kafka中partition、rockerMQ中consumerQueue中消息的顺序读写。同样的也是利用操作系统的PageCache机制。
PageCache
页缓存(PageCache)是OS对文件的缓存,用于加速对文件的读写。一般来说,程序对文件进行顺序读写的速度几乎接近于内存的读写速度,主要原因就是由于OS使用PageCache机制对读写访问操作进行了性能优化,将一部分的内存用作PageCache。
对于数据的写入,OS会先写入至Cache内,随后通过异步的方式由pdflush内核线程将Cache内的数据刷盘至物理磁盘上。
对于数据的读取,如果一次读取文件时出现未命中PageCache的情况,OS从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取。
而PageCache就是局部性原理的实现。
时间局部性
时间局部性可能在我们日常业务开发中体现得更明显。
类似LRU缓存都是其具体实现。
另外CPU的指令重排序也贴点边,比如对一个数据的访问计算,优先将于这数据有关的指令排在一起处理。
参考
gitHub:RocketMQ设计文档
美团技术团队:高性能队列Disruptor