一. 引子
在多線程環境中,經常會有一些計數操作,用來統計線上服務的一些qps、平均延時、error等。為了完成這些統計,可以實現一個多線程環境下的計數器類庫,方便記錄和查看用戶程序中的各類數值。在實現這個計數器類庫時,可以利用thread local存儲來避免cache bouncing,從而提高效率。注意,這種實現方式的本質是把寫時的競爭轉移到了讀:讀得合並所有寫過的線程中的數據,而不可避免地變慢了。當你讀寫都很頻繁並得基於數值做一些邏輯判斷時,你不應該用前述的實現方式。那么,cache bouncing是什么?下面詳細說明一下。
二. 什么是cache bouncing?
為了以較低的成本大幅提高性能,現代CPU都有cache。cpu cache已經發展到了三級緩存結構,基本上現在買的個人電腦都是L3結構。其中L1和L2cache為每個核獨有,L3則所有核共享。為了保證所有的核看到正確的內存數據,一個核在寫入自己的L1 cache后,CPU會執行Cache一致性算法把對應的cacheline(一般是64字節)同步到其他核。這個過程並不很快,是微秒級的,相比之下寫入L1 cache只需要若干納秒。當很多線程在頻繁修改某個字段時,這個字段所在的cacheline被不停地同步到不同的核上,就像在核間彈來彈去,這個現象就叫做cache bouncing。由於實現cache一致性往往有硬件鎖,cache bouncing是一種隱式的的全局競爭。
cache bouncing使訪問頻繁修改的變量的開銷陡增,甚至還會使訪問同一個cacheline中不常修改的變量也變慢,這個現象是false sharing。按cacheline對齊能避免false sharing,但在某些情況下,我們甚至還能避免修改“必須”修改的變量。當很多線程都在累加一個計數器時,我們讓每個線程累加私有的變量而不參與全局競爭,在讀取時我們累加所有線程的私有變量。雖然讀比之前慢多了,但由於這類計數器的讀多為低頻展現,慢點無所謂。而寫就快多了,從微秒到納秒,幾百倍的差距。
三. cache
1. cache的意義
為什么需要CPU cache?因為CPU的頻率太快了,快到主存跟不上,這樣在處理器時鍾周期內,CPU常常需要等待主存,浪費資源。所以cache的出現,是為了緩解CPU和內存之間速度的不匹配問題(結構:cpu -> cache -> memory)。
CPU cache有什么意義?cache的容量遠遠小於主存,因此出現cache miss在所難免,既然cache不能包含CPU所需要的所有數據,那么cache的存在真的有意義嗎?當然是有意義的——局部性原理。
A. 時間局部性:如果某個數據被訪問,那么在不久的將來它很可能被再次訪問;
B. 空間局部性:如果某個數據被訪問,那么與它相鄰的數據很快也可能被訪問;
2. cache和寄存器
存儲器的三個性能指標——速度、容量和每位價格——導致了計算機組成中存儲器的多級層次結構,其中主要是緩存和主存、主存和磁盤的結構。那么在主存之上,cache和寄存器之間的關系是?
舉個例子,當你在思考一個問題的時候,寄存器存放的是你當前正在思考的內容,cache存放的是與該問題相關的記憶,主存則存放無論與該問題是否有關的所有記憶,所以,寄存器存放的是當前CPU執行的數據,而cache則緩存與該數據相關的部分數據,因此只要保證了cache的一致性,那么寄存器拿到的數據也必然具備一致性。
四. CPU cache結構
1. 單核CPU cache結構
在單核CPU結構中,為了緩解CPU指令流水中cycle沖突,L1分成了指令(L1P)和數據(L1D)兩部分,而L2則是指令和數據共存。
2. 多核CPU cache結構
多核CPU的結構與單核相似,但是多了所有CPU共享的L3三級緩存。在多核CPU的結構中,L1和L2是CPU私有的,L3則是所有CPU核心共享的。
五. MESI(緩存一致性)
cache的寫操作方式可以追溯到大學教程《計算機組成原理》一書。
A. write through(寫通):每次CPU修改了cache中的內容,立即更新到內存,也就意味着每次CPU寫共享數據,都會導致總線事務,因此這種方式常常會引起總線事務的競爭,高一致性,但是效率非常低;
B. write back(寫回):每次CPU修改了cache中的數據,不會立即更新到內存,而是等到cache line在某一個必須或合適的時機才會更新到內存中;
無論是寫通還是寫回,在多線程環境下都需要處理緩存cache一致性問題。為了保證緩存一致性,處理器又提供了寫失效(write invalidate)和寫更新(write update)兩個操作來保證cache一致性。
寫失效:當一個CPU修改了數據,如果其他CPU有該數據,則通知其為無效;
寫更新:當一個CPU修改了數據,如果其他CPU有該數據,則通知其跟新數據;
寫更新會導致大量的更新操作,因此在MESI協議中,采取的是寫失效(即MESI中的I:ivalid,如果采用的是寫更新,那么就不是MESI協議了,而是MESU協議)。
2. cache line
cache line是cache與內存數據交換的最小單位,根據操作系統一般是32byte或64byte。在MESI協議中,狀態可以是M、E、S、I,地址則是cache line中映射的內存地址,數據則是從內存中讀取的數據。
工作方式:當CPU從cache中讀取數據的時候,會比較地址是否相同,如果相同則檢查cache line的狀態,再決定該數據是否有效,無效則從主存中獲取數據,發起一次RR(remote read);
工作效率:當CPU能夠從cache中拿到有效數據的時候,消耗幾個CPU cycle,如果發生cache miss,則會消耗幾十上百個CPU cycle;
cache的工作原理以及在主板上的結構如下兩圖所示:
3. 狀態介紹
MESI協議將cache line的狀態分成modify、exclusive、shared、invalid,分別是修改、獨占、共享和失效。
modify:當前CPU cache擁有最新數據(最新的cache line),其他CPU擁有失效數據(cache line的狀態是invalid),雖然當前CPU中的數據和主存是不一致的,但是以當前CPU的數據為准;
exclusive:只有當前CPU中有數據,其他CPU中沒有改數據,當前CPU的數據和主存中的數據是一致的;
shared:當前CPU和其他CPU中都有共同數據,並且和主存中的數據一致;
invalid:當前CPU中的數據失效,數據應該從主存中獲取,其他CPU中可能有數據也可能無數據,當前CPU中的數據和主存被認為是不一致的;
對於invalid而言,在MESI協議中采取的是寫失效(write invalidate)。
4. cache操作
MESI協議中,每個cache的控制器不僅知道自己的操作(local read和local write),通過監聽也知道其他CPU中cache的操作(remote read和remote write)。對於自己本地緩存有的數據,CPU僅需要發起local操作,否則發起remote操作,從主存中讀取數據,cache控制器通過總線監聽,僅能夠知道其他CPU發起的remote操作,但是如果local操作會導致數據不一致性,cache控制器會通知其他CPU的cache控制器修改狀態。
local read(LR):讀本地cache中的數據;
local write(LW):將數據寫到本地cache;
remote read(RR):讀取內存中的數據;
remote write(RW):將數據寫通到主存;
5. 狀態轉換和cache操作
如上文內容所述,MESI協議中cache line數據狀態有4種,引起數據狀態轉換的CPU cache操作也有4種,因此要理解MESI協議,就要將這16種狀態轉換的情況討論清楚。
MESI協議為了保證多個CPU cache中共享數據的一致性,定義了cache line的四種狀態,而CPU對cache的4種操作可能會產生不一致狀態,因此cache控制器監聽到本地操作和遠程操作的時候,需要對地址一致的cache line狀態做出一定的修改,從而保證數據在多個cache之間流轉的一致性。
參考資料:
http://blog.csdn.net/reliveit/article/details/50450136