瞭解存儲結構對性能優化是很是關鍵的,不論是數據庫,消息中間件,負載均衡器,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性能對比