了解存储结构对性能优化是很是关键的,不论是数据库,消息中间件,负载均衡器,api gateway等
性能优化的道理都是相通的,好比说Oracle性能优化,那么咱们也须要从Oracle内部的存储和体系结构出发,分析B*树,块缓存,JOIN算法,逻辑读,Latch/Lock,分析统计数据等,在分析逻辑读的时候须要考虑访问的顺序及存储的顺序,这里都涉及到如何最大化使用各层的Cachejava
本文主要介绍下cache的层次和java中能够优化或关注的地方ios
如今计算机体系里,根据读写的速度,能够分为如下层次,这里远程也能够根据读写速度再细分,好比Redis能够做为Mysql的上一级cache程序员
不一样的存储访问延时差异甚大,实现的细节也有很大的差异
好比下图的SRAM和DRAM算法
操做 | 延时 |
---|---|
execute typical instruction | 1/1,000,000,000 sec = 1 nanosec |
fetch from L1 cache memory | 0.5 nanosec |
branch misprediction | 5 nanosec |
fetch from L2 cache memory | 7 nanosec |
Mutex lock/unlock | 25 nanosec |
fetch from main memory | 100 nanosec |
send 2K bytes over 1Gbps network | 20,000 nanosec |
read 1MB sequentially from memory | 250,000 nanosec |
fetch from new disk location (seek) | 8,000,000 nanosec |
read 1MB sequentially from disk | 20,000,000 nanosec |
send packet US to Europe and back | 150 milliseconds = 150,000,000 nanosec |
如何利用好每个层次的cache,对系统的性能相当重要,好比操做系统的Page Cache, Buffer Cache , Oracle的block cache,好比咱们经常使用的java on/off-heap cache,Jedis/Memcached等。
由于篇幅有限,本文主要挑L0-L4进行具体介绍,以及咱们设计程序的时候须要考虑到哪些问题以追求极致的性能。sql
L0-L4会涉及到JVM的内存模型,你们能够先不关心这个,JMM是另一个话题(涉及cpu 指令流水,store buffer,invalid message queue,memory barrier等),这里不作介绍,下次找时间完整的写一篇,顺便学学JCStressTest数据库
寄存器是存储结构里的L0缓存,寄存器速度是最快的,毕竟是直接跟ALU(Arithmetric Logic Uint in CPU)打交道的,不快能行吗api
下图X86-64会有16个寄存器(更多的寄存器,能够将一部分函数调用的参数直接取寄存器,节约了栈上分配及访问的时间),IA32只有8个数组
寄存器的优化主要在编译器或/JIT层面,好比X86-64有16个寄存器,能够将一部分函数调用的参数直接取寄存器,节约了栈上分配及访问的时间等。
寄存器java程序员不用怎么care缓存
首先看为啥要有L1-L3 cache,如图所示,cpu的发展速度要远高于DRAM,当出现数量级的差别的时候,就须要中间加一层cache,来缓冲这个数量级的差别带来的巨大影响,这个也适用于上面的存储层次性能优化
cpu cache是一个复杂的体系,这里先介绍基本的层次结构,cache的映射方式,一致性协议等略过
你们先看下cpu cache的总体结构
PS: QPI(quick path interconnect)实现芯片之间的快速互联,不占用内存总线带宽
Cache Line 是CPU Cache的最小缓存单位,通常大小是64个字节
完整的了解看 https://en.wikipedia.org/wiki...
好了,回到性能优化的主题,咱们java程序员,如何作到高效的使用CPU cache呢,或则,咱们须要关心哪些地方会对性能产生较大影响。
若是某一个请求,有多个core处理,那么请求相关的cache line须要在多个core之间"copy",其实这里也有一个zero copy的概念,就是在多个core之间的cache line zero copy(本身发明一个。。。)
为了达到这个目的,有下面几个须要注意的地方
将请求绑定到某一个线程/进程
利用OS的soft cpu affinity或手动绑定,将某一个线程/进程绑定到固定的core
网络io的tx/rx亲缘性以及收发的core和处理的core的亲缘性
java进程内部,好比EventLoop的亲缘性
这里核心的思路是解决各类亲缘性,如cpu 亲缘性, io 亲缘性, 应用的请求亲缘性,netty的EventLoop亲缘性, 目的仍是让请求或某个切面的请求尽可能在同一个core处理,以最大化的利用cache,并且这个切面的一些共享数据均可以不用加锁,最大化系统的并行程度
咱们看看Google Maglev中是如何处理的
Maglev 是google的负载均衡器(相似于LVS,可是Maglev实现的更底层)
Maglev中根据链接的五元组(这里除了src ip,port dst ip,port外,还有protocal version)将packet hash到某一个packet thread处理(packet thread跟core绑定),而后再根据packet thread本身的缓存映射到service ip(假设以前已经映射过),这里的优点是
packet不会在多个core之间交互,zero cache line copy
packet能够无锁的使用或更新到service ip的映射
咱们在设计数据结构和算法时,除了算法理论的时间和空间复杂度,还要考虑集合是否缓存友好,好比ArrayList和LinkedList这两种数据结构,不少人认为LinkedList适合插入节点的场景,由于ArrayList须要arraycopy,实际上是不必定的
下面是个人JMH测试数据(mbp i7 2.5G 单线程,java 1.8.0_65)
@BenchmarkMode(Mode.Throughput) @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 2, time = 500, timeUnit = TimeUnit.MILLISECONDS) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Fork(1) @State(Scope.Benchmark) @Threads(1) public class LinkedListTest { private int size = 10000; @Param({"1", "500", "1000", "5000"}) private int offset; @Param({"true", "false"}) private boolean arrayScanIndex; LinkedList linkedList = new LinkedList(); ArrayList arrayList = new ArrayList(size + 1); public LinkedListTest() { for (int i = 0; i < size; i++) { linkedList.add(new Object()); } for (int i = 0; i < size; i++) { arrayList.add(new Object()); } } @Benchmark public void testLinkedList() { linkedList.add(offset, new Object()); linkedList.remove(offset); } @Benchmark public void tetArrayList() { if (arrayScanIndex) { for (int i = 0; i < offset; i++) { arrayList.get(i); } } arrayList.add(offset, new Object()); if (arrayScanIndex) { for (int i = 0; i < offset; i++) { arrayList.get(i); } } arrayList.remove(offset); } }
上面开不开arrayScanIndex,对ArrayList性能基本没有影响,由于一个cache line能够存多个array的节点对象,大体估下64/4=16,好比须要遍历5000,那么5000/16=312个cache line的扫描,并且循环调用能够反复使用这些cache line,另外,ArrayList的elementData数组元素必定是连续分配的,因此arraycopy的时候能够最大化利用cache line
而LinkedList可是由于他的Node节点占用40个字节,item这里占用16个字节,那么遍历5000个节点,须要5000*56/64=4375个cache line(粗略估计),根据测试结果来看,前面的cache line已被换出,没法循环使用
只有在index很是小的时候,LinkedList才有优点,另外,ArrayList比LinkedList最坏状况好得多
这个例子不是说让你们使用ArrayList,由于LinkedList能够用辅助结构来加快index速度,而是说明一个问题,算法以外,考虑cache友好
在保持合理的抽象层度的同时,须要尽量下降under lying数据的寻址次数,从而减小cache的淘汰,提升cache hit
咱们有时候写java对象,一层套一层,5,6层不算啥,a->b->c->d->data
咱们用pointer chasing 来表示这种现象
下面咱们来看两个场景的测试结果
先设计几个测试的类,分别是Level1,Level2,Level3,Level4,每一个类在heap中占24个字节(64位机器)
//以Level3举例 public class Level3 { public Level2 level2 = new Level2(); public int get(){ return level2.level1.x; } }
@BenchmarkMode(Mode.Throughput) @Warmup(iterations = 2, time = 500, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 2, time = 500, timeUnit = TimeUnit.MILLISECONDS) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Fork(1) @State(Scope.Benchmark) @Threads(1) public class PointerChasingTest { private int size = 10000; private int[] list0 = new int[size]; private Level1[] list1 = new Level1[size]; private Level2[] list2 = new Level2[size]; private Level3[] list3 = new Level3[size]; private Level4[] list4 = new Level4[size]; public PointerChasingTest() { for (int i = 0; i < size; i++) { list0[i] = i; } for (int i = 0; i < size; i++) { list1[i] = new Level1(); } for (int i = 0; i < size; i++) { list2[i] = new Level2(); } for (int i = 0; i < size; i++) { list3[i] = new Level3(); } for (int i = 0; i < size; i++) { list4[i] = new Level4(); } } @Benchmark public void testLevel0() { for (int i = 0; i < size; i++) { if (list0[i] == i) { } ; } } @Benchmark public void testLevel1() { for (int i = 0; i < size; i++) { list1[i].get(); } } @Benchmark public void testLevel2() { for (int i = 0; i < size; i++) { list2[i].get(); } } @Benchmark public void testLevel3() { for (int i = 0; i < size; i++) { list3[i].get(); } } @Benchmark public void testLevel4() { for (int i = 0; i < size; i++) { list4[i].get(); } } }
从测试结果能够看出,pointer chasing越深,性能越差
另外,原始类型比Object性能好几个数量级,一个是原始类型没有pointer chasing,另外一个是一个cache line能够存储的int要远远多余Object,Object在JVM中是臃肿的
大体画了下pointer chasing的内存分布图
有没有即有ArrayList这样的面向Object的集合抽象,又有原始类型的性能?
有,java project Valhalla
This aims to “reboot the layout of data in memory” in order to reduce the amount of memory used fetching objects from memory compared to, for example, arithmetic calculations. Not all classes need mutability or polymorphism. To do this, the project explores value types, generic specialisation and enhanced volatiles, and more.
Value types would provide JVM infrastructure to work with immutable, reference-free objects. This will be essential when high performance is required, and pairs of numbers need to be returned. Using primitives avoids allocation, but an object to wrap around the pair gives the benefit of abstraction. This project looks to open the door to user-defined abstract data types that perform like primitives
咱们知道,java对象在heap中是很臃肿的(全部才会用公司在保持api不变的同时,直接读写本身的raw data...),过于臃肿的对象,势必须要更多的cache line,产生更多的cache 淘汰
简单对比了下面两个对象的效率,后者快2倍多,FatModel除了占用更多的内存,须要扫描更多的cache line
public class FatModel { long a, b, c, d, e, f; public long get() { return a & b & c & d & e & f; } } public class ThinModel { byte a, b, c, d, e, f; public byte get() { return (byte) (a & b & c & d & e & f); } }
//todo 测试二进制raw对象的query 性能
这里须要注意false sharing的场景,见下面章节
在设计无锁高并发的数据结构结构时,都会用到CAS或volatile,为了支持更高的并行度,须要将CAS的变量细化成数组,分配给不一样的core,每个CAS变量负责一个区域或一个切面,那么在不一样切面的请求,能够独立的进行CAS并发控制。
然额,由于JVM对数组元素对象倾向于连续分配,会致使多个对象在同一个cache line, 致使不一样切面的请求,其实是对同一个cache line竞争,这种状况就是False Sharing
不论是False Sharing仍是别的缘由,多个core对同一个cache line的争用(如lock xcmchg指令)会致使
对性能会产生较大影响
在我本机JMH 2*core线程测试AtomicLong和LongAdder,后者性能是前者10x,固然内存占用也更多)
下图解释了False Sharing为何会致使cache contention
解决Flase Sharing: 在对象中加cache line padding,使操做的对象在不一样的cache line,从而减小cache contention
不少开源的高性能无锁结构都有这方面的处理,不如Disruptor,或则JDK自带的LongAdder
咱们测试一下
@BenchmarkMode(Mode.Throughput) @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Fork(1) @State(Scope.Benchmark) @Threads(16) public class FalseSharingTest { private int threads = 16; private FalseSharing[] counters1 = new FalseSharing[threads]; private FalseSharingPadding[] counters2 = new FalseSharingPadding[threads]; public FalseSharingTest(){ for(int i = 0 ; i < threads ; i++){ counters1[i] = new FalseSharing(); } for(int i = 0 ; i < threads ; i++){ counters2[i] = new FalseSharingPadding(); } } @Benchmark public void testFalseSharing(ThreadParams params){ counters1[params.getThreadIndex()%threads].value=2; } @Benchmark public void testFalseSharingPadding(ThreadParams params){ counters2[params.getThreadIndex()%threads].value=2; } }
老生常谈了,context switch会进行model switch(user->kernel),再进行线程切换
Context Switch在OS里实现的比较heavy,自己切换的效率也比coroutine切换低不少
另外,频繁context switch会致使cache hit降低(多个线程频繁交互的使用cache)
若是线程须要充分利用cache,最好是non-blocking,下降csw,而后持有cpu尽可能多的时间批量干活
内存的话题太大,挑几个介绍下
首先,你们可能日常常常听到一些词,用户态啊,内核态啊,zero copy啊,可是又有点疑惑,底下究竟是怎么搞的
咱们先从虚拟地址,物理地址,进程的地址空间提及
todo:待补充细节
简单来讲,cache 和 buffer 定位以下
The page cache caches pages of files to optimize file I/O. The buffer cache caches disk blocks to optimize block I/O.
page cache
file cache,mmap,direct buffer…buffer cache
metadata(permission…) , raw io , 其余非文件的运行时数据
2.4版本的内核以前,文件的内容也会在buffer存储,也就是须要存储2次,2.4版本以后,buffer不会再存储再Cache中的内容
//todo 补充更细粒度的内存视图
Layer | Unit | Typical Unit Size |
---|---|---|
User Space System Calls | read() , write() | |
Virtual File System Switch (VFS) | Block | 4096 Bytes |
Page Cache | Page | Normal:4k Huge: |
Filesystem (For example ext3) | Blocks | 4096 Bytes (Can be set at FS creation) |
Generic Block Layer | Page Frames / Block IO Operations (bio) | |
I/O Scheduler Layer | bios per block device (Which this layer may combine) | |
Block Device Driver | Segment | 512 Bytes |
Hard Disk | Sector | 512 Bytes |
咱们主要关心的是Page Cache,Buffer Cache对咱们来讲不须要重点去关注
1.Zero Copy
Item | Value |
---|---|
mmap | 读写文件,不须要再从user区(好比java heap)复制一份到kernal区再进行write到page cache,user直接写page cache |
direct buffer | 针对网络IO,减小了一次user和kernal space之间的copy,其实这里也不能叫zero copy,就是减小了一次copy |
//todo mmap性能对比