緩存
在現(xiàn)代的 CPU(大多數(shù))上赚瘦,所有的內(nèi)存訪問都需要通過層層的緩存來進行悲酷。CPU 的讀 / 寫(以及取指令)單元正常情況下甚至都不能直接訪問內(nèi)存——這是物理結構決定的顿肺;CPU 都沒有管腳直接連到內(nèi)存。相反,CPU 和一級緩存(L1 Cache)通訊凭戴,而一級緩存才能和內(nèi)存通訊档痪。大約二十年前涉枫,一級緩存可以直接和內(nèi)存?zhèn)鬏敂?shù)據(jù)笛坦。如今嫂易,更多級別的緩存加入到設計中,一級緩存已經(jīng)不能直接和內(nèi)存通訊了秩冈,它和二級緩存通訊——而二級緩存才能和內(nèi)存通訊芬失。或者還可能有三級緩存。
緩存是分“段”(line)的消别,一個段對應一塊存儲空間怀读,大小是 32、64或128字節(jié)菜枷,每個緩存段知道自己對應什么范圍的物理內(nèi)存地址苍糠。
當 CPU 看到一條讀內(nèi)存的指令時,它會把內(nèi)存地址傳遞給一級數(shù)據(jù)緩存啤誊。一級數(shù)據(jù)緩存會檢查它是否有這個內(nèi)存地址對應的緩存段岳瞭。如果沒有拥娄,它會把整個緩存段從內(nèi)存(或者從更高一級的緩存,如果有的話)中加載進來瞳筏。是的稚瘾,一次加載整個緩存段,這是基于這樣一個假設:內(nèi)存訪問傾向于本地化(localized)姚炕,如果我們當前需要某個地址的數(shù)據(jù)摊欠,那么很可能我們馬上要訪問它的鄰近地址。一旦緩存段被加載到緩存中柱宦,讀指令就可以正常進行讀取些椒。
如果我們只處理讀操作,那么事情會很簡單掸刊,因為所有級別的緩存都遵守以下規(guī)律——在任意時刻免糕,任意級別緩存中的緩存段的內(nèi)容,等同于它對應的內(nèi)存中的內(nèi)容忧侧。石窑。
一旦我們允許寫操作,事情就變得復雜一點了蚓炬。這里有兩種基本的寫模式:直寫(write-through)和回寫(write-back)松逊。直寫更簡單一點:我們透過本級緩存,直接把數(shù)據(jù)寫到下一級緩存(或直接到內(nèi)存)中试吁,如果對應的段被緩存了棺棵,我們同時更新緩存中的內(nèi)容(甚至直接丟棄),就這么簡單熄捍。這也遵守前面的定律:緩存中的段永遠和它對應的內(nèi)存內(nèi)容匹配烛恤。
回寫模式就有點復雜了。緩存不會立即把寫操作傳遞到下一級余耽,而是僅修改本級緩存中的數(shù)據(jù)缚柏,并且把對應的緩存段標記為“臟”段。臟段會觸發(fā)回寫碟贾,也就是把里面的內(nèi)容寫到對應的內(nèi)存或下一級緩存中币喧。回寫后袱耽,臟段又變“干凈”了杀餐。當一個臟段被丟棄的時候,總是先要進行一次回寫朱巨∈非蹋回寫所遵循的規(guī)律有點不同。當所有的臟段被回寫后,任意級別緩存中的緩存段的內(nèi)容琼讽,等同于它對應的內(nèi)存中的內(nèi)容必峰。
換句話說,回寫模式的定律中钻蹬,我們?nèi)サ袅恕霸谌我鈺r刻”這個修飾語吼蚁,代之以弱化一點的條件:要么緩存段的內(nèi)容和內(nèi)存一致(如果緩存段是干凈的話),要么緩存段中的內(nèi)容最終要回寫到內(nèi)存中(對于臟緩存段來說)问欠。
直接模式更簡單肝匆,但是回寫模式有它的優(yōu)勢:它能過濾掉對同一地址的反復寫操作,并且溅潜,如果大多數(shù)緩存段都在回寫模式下工作术唬,那么系統(tǒng)經(jīng)承椒可以一下子寫一大片內(nèi)存滚澜,而不是分成小塊來寫,前者的效率更高设捐。
一致性協(xié)議
只要系統(tǒng)只有一個 CPU 核在工作,一切都沒問題塘淑。如果有多個核萝招,每個核又都有自己的緩存,那么我們就遇到問題了存捺,因為如果一個 CPU 緩存了某塊內(nèi)存槐沼,那么在其他 CPU 修改這塊內(nèi)存的時候,我們希望得到通知捌治。系統(tǒng)的內(nèi)存在各個 CPU 之間無法做到與生俱來的同步岗钩,我們需要一個大家都能遵守的方法來達到同步的目的。
注意肖油,這個問題的根源是我們擁有多組緩存兼吓,而不是多個 CPU 核。我們也可以這樣解決問題森枪,讓多個 CPU 核共用一組緩存:也就是說只有一塊一級緩存视搏,所有處理器都必須共用它。在每一個指令周期县袱,只有一個幸運的 CPU 能通過一級緩存做內(nèi)存操作浑娜,運行它的指令。
這本身沒問題式散。唯一的問題就是太慢了筋遭,因為這下處理器的時間都花在排隊等待使用一級緩存了
我們知道了只有一組緩存也能工作,只是太慢了,接下來最好就是能做到:使用多組緩存宛畦,但使它們的行為看起來就像只有一組緩存那樣瘸洛。緩存一致性協(xié)議就是為了做到這一點而設計的。就像名稱所暗示的那樣次和,這類協(xié)議就是要使多組緩存的內(nèi)容保持一致反肋。
緩存一致性協(xié)議有多種,但是日常處理的大多數(shù)計算機設備使用的都屬于“窺探(snooping)”協(xié)議踏施。
還有一種叫“基于目錄的(directory-based)”協(xié)議石蔗,這種協(xié)議的延遲性較大,但是在擁有很多個處理器的系統(tǒng)中畅形,它有更好的可擴展性养距。
窺探”背后的基本思想是,所有內(nèi)存?zhèn)鬏敹及l(fā)生在一條共享的總線上日熬,而所有的處理器都能看到這條總線:緩存本身是獨立的棍厌,但是內(nèi)存是共享資源,所有的內(nèi)存訪問都要經(jīng)過仲裁(arbitrate):同一個指令周期中竖席,只有一個緩存可以讀寫內(nèi)存耘纱。窺探協(xié)議的思想是,緩存不僅僅在做內(nèi)存?zhèn)鬏數(shù)臅r候才和總線打交道毕荐,而是不停地在窺探總線上發(fā)生的數(shù)據(jù)交換束析,跟蹤其他緩存在做什么。所以當一個緩存代表它所屬的處理器去讀寫內(nèi)存時憎亚,其他處理器都會得到通知员寇,它們以此來使自己的緩存保持同步。只要某個處理器一寫內(nèi)存第美,其他處理器馬上就知道這塊內(nèi)存在它們自己的緩存中對應的段已經(jīng)失效蝶锋。
在直寫模式下,這是很直接的斋日,因為寫操作一旦發(fā)生牲览,它的效果馬上會被“公布”出去。但是如果混著回寫模式恶守,就有問題了第献。因為有可能在寫指令執(zhí)行過后很久,數(shù)據(jù)才會被真正回寫到物理內(nèi)存中——在這段時間內(nèi)兔港,其他處理器的緩存也可能會傻乎乎地去寫同一塊內(nèi)存地址庸毫,導致沖突。在回寫模型中衫樊,簡單把內(nèi)存寫操作的信息廣播給其他處理器是不夠的飒赃,我們需要做的是利花,在修改本地緩存之前,就要告知其他處理器载佳。
MESI以及衍生協(xié)議
MESI 是四種緩存段狀態(tài)的首字母縮寫炒事,任何多核系統(tǒng)中的緩存段都處于這四種狀態(tài)之一。
狀態(tài) | 說明 | 監(jiān)聽任務 |
---|---|---|
Invalid | 失效緩存段蔫慧,要么已經(jīng)不在緩存中挠乳,要么它的內(nèi)容已經(jīng)過時。為了達到緩存的目的姑躲,這種狀態(tài)的段將會被忽略睡扬。一旦緩存段被標記為失效,那效果就等同于它從來沒被加載到緩存中黍析。 | 緩存行必須時刻監(jiān)聽所有試圖讀該緩存行相對就主存的操作卖怜,這種操作必須在緩存將該緩存行寫回主存并將狀態(tài)變成S(共享)狀態(tài)之前被延遲執(zhí)行。 |
Shared | 共享緩存段阐枣,它是和主內(nèi)存內(nèi)容保持一致的一份拷貝马靠,在這種狀態(tài)下的緩存段只能被讀取,不能被寫入侮繁。多組緩存可以同時擁有針對同一內(nèi)存地址的共享緩存段,這就是名稱的由來锁孟。 | 緩存行也必須監(jiān)聽其它緩存讀主存中該緩存行的操作,一旦有這種操作圆恤,該緩存行需要變成S(共享)狀態(tài)。 |
Exclusive | 獨占緩存段腔稀,和 S 狀態(tài)一樣,也是和主內(nèi)存內(nèi)容保持一致的一份拷貝淡喜。區(qū)別在于炼团,如果一個處理器持有了某個 E 狀態(tài)的緩存段瘟芝,那其他處理器就不能同時持有它颈抚,所以叫“獨占”贩汉。這意味著,如果其他處理器原本也持有同一緩存段锚赤,那么它會馬上變成“失效”狀態(tài)。 | 緩存行也必須監(jiān)聽其它緩存使該緩存行無效或者獨享該緩存行的請求,并將該緩存行變成無效(Invalid)。 |
Modified | 已修改緩存段,屬于臟段构资,它們已經(jīng)被所屬的處理器修改了。如果一個段處于已修改狀態(tài)葵姥,那么它在其他處理器緩存中的拷貝馬上會變成失效狀態(tài),這個規(guī)律和 E 狀態(tài)一樣允乐。此外,已修改緩存段如果被丟棄或標記為失效厦滤,那么先要把它的內(nèi)容回寫到內(nèi)存中——這和回寫模式下常規(guī)的臟段處理方式一樣趟咆。 | 無 |
從CPU讀寫角度來說:
- CPU讀請求:緩存處于M、E值纱、S狀態(tài)都可以被讀取鳞贷,I狀態(tài)CPU只能從主存中讀取數(shù)據(jù)
- CPU寫請求:緩存處于M、E狀態(tài)才可以被寫虐唠。對于S狀態(tài)的寫搀愧,需要將其他CPU中緩存行置為無效才可寫
上圖的切換解釋:
MESI優(yōu)化和他們引入的問題
緩存的一致性消息傳遞是要時間的,這就使其切換時會產(chǎn)生延遲凿滤。當一個緩存被切換狀態(tài)時其他緩存收到消息完成各自的切換并且發(fā)出回應消息這么一長串的時間中CPU都會等待所有緩存響應完成妈橄。可能出現(xiàn)的阻塞都會導致各種各樣的性能問題和穩(wěn)定性問題翁脆。
比如你需要修改本地緩存中的一條信息,那么你必須將I(無效)狀態(tài)通知到其他擁有該緩存數(shù)據(jù)的CPU緩存中鼻种,并且等待確認反番。等待確認的過程會阻塞處理器,這會降低處理器的性能叉钥。因為這個等待遠遠比一個指令的執(zhí)行時間長的多罢缸。
為了避免這種CPU運算能力的浪費,Store Bufferes被引入使用投队。處理器把它想要寫入到主存的值寫到緩存枫疆,然后繼續(xù)去處理其他事情。當所有失效確認(Invalidate Acknowledge)都接收到時敷鸦,數(shù)據(jù)才會最終被提交息楔。
執(zhí)行失效也不是一個簡單的操作寝贡,它需要處理器去處理。另外值依,存儲緩存(Store Buffers)并不是無窮大的圃泡,所以處理器有時需要等待失效確認的返回。這兩個操作都會使得性能大幅降低愿险。為了應付這種情況颇蜡,引入了失效隊列——對于所有的收到的Invalidate請求,Invalidate Acknowlege消息必須立刻發(fā)送辆亏,Invalidate并不真正執(zhí)行风秤,而是被放在一個特殊的隊列中,在方便的時候才會去執(zhí)行扮叨,處理器不會發(fā)送任何消息給所處理的緩存條目缤弦,直到它處理Invalidate。