cpu存取數(shù)據(jù)-【公粽號:堆棧future】
cpu存取數(shù)據(jù)大致可以認(rèn)為是下圖的流程(此處圖比較簡單)
cpu拿到需要的內(nèi)存地址,之后這個(gè)地址會被mmu轉(zhuǎn)換成真正的物理地址产喉,接下來會去查接下來查L1 cache捂掰,L1 cache不命中查L2 cache敢会,L2 cache不命中查L3 cache,L3 cache不能命中查內(nèi)存这嚣。
其實(shí)現(xiàn)在查到內(nèi)存還算完鸥昏,現(xiàn)在有了虛擬內(nèi)存,內(nèi)存其實(shí)也是一層cache疤苹,是磁盤的cache,也就是說查內(nèi)存也有可能不會命中敛腌,因?yàn)閮?nèi)存中的數(shù)據(jù)可能被虛擬內(nèi)存系統(tǒng)放到磁盤中了卧土,如果內(nèi)存也不能命中就要查磁盤。
為什么需要cache
程序局部性原理
如果我們訪問內(nèi)存中的一個(gè)數(shù)據(jù)A像樊,那么很有可能接下來再次訪問到尤莺,同時(shí)還很有可能訪問與數(shù)據(jù)A相鄰的數(shù)據(jù)B,這分別叫做時(shí)間局部性和空間局部性生棍。
cpu cache 有多快
根據(jù)摩爾定律颤霎,CPU 的訪問速度每 18 個(gè)月就會翻 倍,相當(dāng)于每年增? 60% 左右涂滴,內(nèi)存的速度當(dāng)然也會不斷增?友酱,但是增?的速度遠(yuǎn)小于 CPU,平均每年 只增? 7% 左右柔纵。于是缔杉,CPU 與內(nèi)存的訪問性能的差距不斷拉大。
為了彌補(bǔ) CPU 與內(nèi)存兩者之間的性能差異搁料,就在 CPU 內(nèi)部引入了 CPU Cache或详,也稱高速緩存。
CPU Cache 通常分為大小不等的三級緩存郭计,分別是 L1 Cache霸琴、L2 Cache 和 L3 Cache。其中L3是多個(gè)核心共享的昭伸。
程序執(zhí)行時(shí)梧乘,會先將內(nèi)存中的數(shù)據(jù)加載到共享的 L3 Cache 中首装,再加載到每個(gè)核心獨(dú)有的 L2 Cache押袍,最后 進(jìn)入到最快的 L1 Cache罪治,之后才會被 CPU 讀取雹洗。它們之間的層級關(guān)系平项,如下圖
越靠近 CPU 核心的緩存其訪問速度越快
cpu cache 讀取過程
CPU Cache 的數(shù)據(jù)是從內(nèi)存中讀取過來的蚂会,它是以一小塊一小塊讀取數(shù)據(jù)的勾效,而不是按照單個(gè)數(shù)組元素來
讀取數(shù)據(jù)的茸苇,在 CPU Cache 中的各吨,這樣一小塊一小塊的數(shù)據(jù)枝笨,稱為 Cache Line(緩存塊)袁铐。
可以在linux機(jī)器下執(zhí)行一下命令查詢L1cache的大小,單位是字節(jié)
<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`#查看cache line 大小
cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
查看各級緩存大小 inde0-3分別是 L1數(shù)據(jù)緩存 L1指令緩存 L2數(shù)據(jù)緩存 L3數(shù)據(jù)緩存
cat /sys/devices/system/cpu/cpu0/cache/index0/size` </pre>
比如横浑,有一個(gè) int array[100] 的數(shù)組剔桨,當(dāng)載入 array[0] 時(shí),由于這個(gè)數(shù)組元素的大小在內(nèi)存只占 4 字 節(jié)徙融,不足 64 字節(jié)洒缀,CPU 就會順序加載數(shù)組元素到 array[15] ,意味著 array[0]~array[15] 數(shù)組元素都會 被緩存在 CPU Cache 中了欺冀,因此當(dāng)下次訪問這些數(shù)組元素時(shí)树绩,會直接從 CPU Cache 讀取,而不用再從內(nèi) 存中讀取隐轩,大大提高了 CPU 讀取數(shù)據(jù)的性能饺饭。
如何寫出讓cpu跑的更快的代碼
其實(shí),這個(gè)問題定義為如何提高cpu緩存利用率更好
大家可以看下如下代碼哪個(gè)執(zhí)行效率更高
<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`func main() {
n := 100
x := 0
arr := createArray(n)
//var arr [][]int
t := time.Now().UnixNano()
for i := 0; i < n; i++ {
for j := 0; j < n; j++ {
x = arr[i][j]
}
}
t1 := time.Now().UnixNano()
for i := 0; i < n; i++ {
for j := 0; j < n; j++ {
x = arr[j][i]
}
}
fmt.Println(x)
}
//創(chuàng)建二維數(shù)組
func createArray(n int) [][]int {
var arr [][]int
for i := 0; i < n; i++ {
var tmp []int
for j := 0; j < n; j++ {
tmp = append(tmp, i+j)
}
arr = append(arr, tmp)
}
return arr
}
/**
經(jīng)過測試职车,形式一 array[i][j] 執(zhí)行時(shí)間比形式二 array[j][i] 快好幾倍瘫俊。
之所以有這么大的差距,是因?yàn)槎S數(shù)組 array 所占用的內(nèi)存是連續(xù)的悴灵,比如?度 N 的指是 2 的 話扛芽,那么內(nèi)存中的數(shù)組元素的布局順序是這樣的:
array[0][0] array[0][1] array[1][0] array[1][1]
形式一用 array[i][j] 訪問數(shù)組元素的順序,正是和內(nèi)存中數(shù)組元素存放的順序一致积瞒。當(dāng) CPU 訪問 array[0][0] 時(shí)胸哥,由于該數(shù)據(jù)不在 Cache 中,
于是會「順序」把跟隨其后的 3 個(gè)元素從內(nèi)存中加載到 CPU Cache赡鲜,這樣當(dāng) CPU 訪問后面的 3 個(gè)數(shù)組元素時(shí)空厌,就能在 CPU Cache 中成功地找到數(shù)據(jù),
這意味著緩存命中率很高银酬,緩存命中的數(shù)據(jù)不需要訪問內(nèi)存嘲更,這便大大提高了代碼的性能。
而如果用形式二的 array[j][i] 來訪問揩瞪,則訪問的順序就是:
array[0][0] array[1][0] array[0][1] array[1][1]
你可以看到赋朦,訪問的方式跳躍式的,而不是順序的李破,那么如果 N 的數(shù)值很大宠哄,那么操作 array[j][i] 時(shí),是 沒辦法把 array[j+1][i] 也讀入到
CPU Cache 中的嗤攻,既然 array[j+1][i] 沒有讀取到 CPU Cache毛嫉,那么就 需要從內(nèi)存讀取該數(shù)據(jù)元素了。很明顯妇菱,這種不連續(xù)性承粤、跳躍式訪問數(shù)據(jù)元素
的方式暴区,可能不能充分利用 到了 CPU Cache 的特性,從而代碼的性能不高辛臊。那訪問 array[0][0] 元素時(shí)仙粱,CPU 具體會一次從內(nèi)存中加載多少元素到
CPU Cache 呢?這個(gè)問題,在前 面我們也提到過彻舰,這跟 CPU Cache Line 有關(guān)伐割,它表示 CPU Cache 一次性能加載數(shù)據(jù)的大小,可以在 Linux 里通過
coherency_line_size 配置查看 它的大小刃唤,通常是 64 個(gè)字節(jié)隔心。
*/` </pre>
cpu cache的結(jié)構(gòu)
CPU Cache 是由很多個(gè) Cache Line 組成的,CPU Line 是 CPU 從內(nèi)存讀取數(shù)據(jù)的基本單位透揣,而 CPU Line 是由各種標(biāo)志(Tag)+ 數(shù)據(jù)塊(Data Block)組成
cpu cache數(shù)據(jù)的寫入
事實(shí)上济炎,數(shù)據(jù)不止有讀取還有寫入川抡,如果數(shù)據(jù)寫入cache之后辐真,內(nèi)存和cache的數(shù)據(jù)就不同了,我們需要把cache同步到內(nèi)存中崖堤。
問題的關(guān)鍵就在于我們在什么時(shí)機(jī)去把數(shù)據(jù)寫到內(nèi)存侍咱?一般來講有以下兩種策略
寫直達(dá)
保持內(nèi)存與 Cache 一致性最簡單的方式是,把數(shù)據(jù)同時(shí)寫入內(nèi)存和 Cache 中密幔,這種方法稱為寫直達(dá) (Write Through)楔脯。
在這個(gè)方法里,寫入前會先判斷數(shù)據(jù)是否已經(jīng)在 CPU Cache 里面了:
如果數(shù)據(jù)已經(jīng)在 Cache 里面胯甩,先將數(shù)據(jù)更新到 Cache 里面昧廷,再寫入到內(nèi)存里面; 如果數(shù)據(jù)沒有在 Cache 里面,就直接把數(shù)據(jù)更新到內(nèi)存里面偎箫。
寫直達(dá)法很直觀木柬,也很簡單,但是問題明顯淹办,無論數(shù)據(jù)在不在 Cache 里面眉枕,每次寫操作都會寫回到內(nèi)存, 這樣寫操作將會花費(fèi)大量的時(shí)間怜森,無疑性能會受到很大的影響速挑。
寫回
由于寫直達(dá)的機(jī)制會有性能問題,所以產(chǎn)生了寫回(Write Back)的方法
在寫回機(jī)制中副硅,當(dāng)發(fā)生寫操作時(shí)姥宝,新的數(shù)據(jù)僅僅被寫入 Cache Block 里,只有當(dāng)修改過的 Cache Block 「被替換」時(shí)才需要寫到內(nèi)存中恐疲,減少了數(shù)據(jù)寫回內(nèi)存的頻率伶授,這樣便可以提高系統(tǒng)的性能断序。
- 如果當(dāng)發(fā)生寫操作時(shí),數(shù)據(jù)已經(jīng)在 CPU Cache 里的話糜烹,則把數(shù)據(jù)更新到 CPU Cache 里违诗,同時(shí)標(biāo)記 CPU Cache 里的這個(gè) Cache Block 為臟(Dirty)的,這個(gè)臟的標(biāo)記代表這個(gè)時(shí)候疮蹦,我們 CPU Cache 里面的這個(gè) Cache Block 的數(shù)據(jù)和內(nèi)存是不一致的诸迟,這種情況是不用把數(shù)據(jù)寫到內(nèi)存里的;
- 如果當(dāng)發(fā)生寫操作時(shí),數(shù)據(jù)所對應(yīng)的 Cache Block 里存放的是「別的內(nèi)存地址的數(shù)據(jù)」的話愕乎,就要檢 查這個(gè) Cache Block 里的數(shù)據(jù)有沒有被標(biāo)記為臟的阵苇,如果是臟的話,我們就要把這個(gè) Cache Block 里的數(shù)據(jù)寫回到內(nèi)存感论,然后再把當(dāng)前要寫入的數(shù)據(jù)绅项,寫入到這個(gè) Cache Block 里,同時(shí)也把它標(biāo)記為 臟的;如果 Cache Block 里面的數(shù)據(jù)沒有被標(biāo)記為臟比肄,則就直接將數(shù)據(jù)寫入到這個(gè) Cache Block 里快耿,然后再把這個(gè) Cache Block 標(biāo)記為臟的就好了。
可以發(fā)現(xiàn)寫回這個(gè)方法芳绩,在把數(shù)據(jù)寫入到 Cache 的時(shí)候掀亥,只有在緩存不命中,同時(shí)數(shù)據(jù)對應(yīng)的 Cache 中 的 Cache Block 為臟標(biāo)記的情況下妥色,才會將數(shù)據(jù)寫到內(nèi)存中搪花,而在緩存命中的情況下,則在寫入后 Cache 后嘹害,只需把該數(shù)據(jù)對應(yīng)的 Cache Block 標(biāo)記為臟即可撮竿,而不用寫到內(nèi)存里。
這樣的好處是笔呀,如果我們大量的操作都能夠命中緩存幢踏,那么大部分時(shí)間里 CPU 都不需要讀寫內(nèi)存,自然性 能相比寫直達(dá)會高很多凿可。
緩存一致性問題
現(xiàn)在的CPU都是多核的惑折,由于L1/L2cache是各個(gè)核心獨(dú)有的,那么會帶來多核心的緩存一致性問題枯跑,如果不能保證緩存一致性問題就會造成錯(cuò)誤的結(jié)果
那緩存一致性的問題具體是怎么發(fā)生的呢惨驶?我們以一個(gè)含有兩個(gè)核心的 CPU 作為例子看一看。
假設(shè) A 號核心和 B 號核心同時(shí)運(yùn)行兩個(gè)線程敛助,都操作共同的變量 i(初始值為 0 )粗卜。
這時(shí)如果 A 號核心執(zhí)行了 i++
語句的時(shí)候,為了考慮性能纳击,使用了我們前面所說的寫回策略续扔,先把值為 1
的執(zhí)行結(jié)果寫入到 L1/L2 Cache 中攻臀,然后把 L1/L2 Cache 中對應(yīng)的 Block 標(biāo)記為臟的,這個(gè)時(shí)候數(shù)據(jù)其實(shí)沒有被同步到內(nèi)存中的纱昧,因?yàn)閷懟夭呗耘傩ィ挥性?A 號核心中的這個(gè) Cache Block 要被替換的時(shí)候,數(shù)據(jù)才會寫入到內(nèi)存里识脆。
如果這時(shí)旁邊的 B 號核心嘗試從內(nèi)存讀取 i 變量的值设联,則讀到的將會是錯(cuò)誤的值,因?yàn)閯偛?A 號核心更新 i 值還沒寫入到內(nèi)存中灼捂,內(nèi)存中的值還依然是 0离例。這個(gè)就是所謂的緩存一致性問題,A 號核心和 B 號核心的緩存悉稠,在這個(gè)時(shí)候是不一致宫蛆,從而會導(dǎo)致執(zhí)行結(jié)果的錯(cuò)誤。
那么的猛,要解決這一問題耀盗,就需要一種機(jī)制,來同步兩個(gè)不同核心里面的緩存數(shù)據(jù)衰絮。要實(shí)現(xiàn)的這個(gè)機(jī)制的話袍冷,要保證做到下面這 2 點(diǎn):
- 第一點(diǎn)磷醋,某個(gè) CPU 核心里的 Cache 數(shù)據(jù)更新時(shí)猫牡,必須要傳播到其他核心的 Cache,這個(gè)稱為寫傳播(Wreite Propagation)**邓线;
- 第二點(diǎn)淌友,某個(gè) CPU 核心里對數(shù)據(jù)的操作順序,必須在其他核心看起來順序是一樣的骇陈,這個(gè)稱為事務(wù)的串形化(Transaction Serialization)**震庭。
第一點(diǎn)寫傳播很容易就理解,當(dāng)某個(gè)核心在 Cache 更新了數(shù)據(jù)你雌,就需要同步到其他核心的 Cache 里器联。
而對于第二點(diǎn)事務(wù)的串形化,我們舉個(gè)例子來理解它婿崭。
假設(shè)我們有一個(gè)含有 4 個(gè)核心的 CPU拨拓,這 4 個(gè)核心都操作共同的變量 i(初始值為 0 )。A 號核心先把 i 值變?yōu)?100氓栈,而此時(shí)同一時(shí)間渣磷,B 號核心先把 i 值變?yōu)?200,這里兩個(gè)修改授瘦,都會「傳播」到 C 和 D 號核心醋界。
那么問題就來了竟宋,C 號核心先收到了 A 號核心更新數(shù)據(jù)的事件,再收到 B 號核心更新數(shù)據(jù)的事件形纺,因此 C 號核心看到的變量 i 是先變成 100丘侠,后變成 200。
而如果 D 號核心收到的事件是反過來的逐样,則 D 號核心看到的是變量 i 先變成 200婉陷,再變成 100,雖然是做到了寫傳播官研,但是各個(gè) Cache 里面的數(shù)據(jù)還是不一致的秽澳。
所以,我們要保證 C 號核心和 D 號核心都能看到相同順序的數(shù)據(jù)變化戏羽,比如變量 i 都是先變成 100担神,再變成 200,這樣的過程就是事務(wù)的串形化始花。
要實(shí)現(xiàn)事務(wù)串形化妄讯,要做到 2 點(diǎn):
- CPU 核心對于 Cache 中數(shù)據(jù)的操作,需要同步給其他 CPU 核心酷宵;
- 要引入「鎖」的概念亥贸,如果兩個(gè) CPU 核心里有相同數(shù)據(jù)的 Cache,那么對于這個(gè) Cache 數(shù)據(jù)的更新浇垦,只有拿到了「鎖」炕置,才能進(jìn)行對應(yīng)的數(shù)據(jù)更新。
那接下來我們看看男韧,寫傳播和事務(wù)串形化具體是用什么技術(shù)實(shí)現(xiàn)的朴摊。
總線嗅探
寫傳播的原則就是當(dāng)某個(gè) CPU 核心更新了 Cache 中的數(shù)據(jù),要把該事件廣播通知到其他核心此虑。最常?實(shí) 現(xiàn)的方式是總線嗅探(Bus Snooping)甚纲。
我還是以前面的 i 變量例子來說明總線嗅探的工作機(jī)制,當(dāng) A 號 CPU 核心修改了 L1 Cache 中 i 變量的 值朦前,通過總線把這個(gè)事件廣播通知給其他所有的核心介杆,然后每個(gè) CPU 核心都會監(jiān)聽總線上的廣播事件,并 檢查是否有相同的數(shù)據(jù)在自己的 L1 Cache 里面韭寸,如果 B 號 CPU 核心的 L1 Cache 中有該數(shù)據(jù)春哨,那么也需 要把該數(shù)據(jù)更新到自己的 L1 Cache。
可以發(fā)現(xiàn)棒仍,總線嗅探方法很簡單悲靴, CPU 需要每時(shí)每刻監(jiān)聽總線上的一切活動(dòng),但是不管別的核心的 Cache 是否緩存相同的數(shù)據(jù),都需要發(fā)出一個(gè)廣播事件癞尚,這無疑會加重總線的負(fù)載耸三。
另外,總線嗅探只是保證了某個(gè) CPU 核心的 Cache 更新數(shù)據(jù)這個(gè)事件能被其他 CPU 核心知道浇揩,但是并 不能保證事務(wù)串形化仪壮。
于是,有一個(gè)協(xié)議基于總線嗅探機(jī)制實(shí)現(xiàn)了事務(wù)串形化胳徽,也用狀態(tài)機(jī)機(jī)制降低了總線帶寬壓力积锅,這個(gè)協(xié)議 就是 MESI 協(xié)議,這個(gè)協(xié)議就做到了 CPU 緩存一致性养盗。
MESI協(xié)議
MESI 協(xié)議其實(shí)是 4 個(gè)狀態(tài)單詞的開頭字母縮寫缚陷,分別是:
- Modified,已修改
- Exclusive往核,獨(dú)占
- Shared箫爷,共享
- Invalidated,已失效
這四個(gè)狀態(tài)來標(biāo)記 Cache Line 四個(gè)不同的狀態(tài)聂儒。
「已修改」?fàn)顟B(tài)就是我們前面提到的臟標(biāo)記虎锚,代表該 Cache Block 上的數(shù)據(jù)已經(jīng)被更新過,但是還沒有寫到內(nèi)存里衩婚。而「已失效」?fàn)顟B(tài)窜护,表示的是這個(gè) Cache Block 里的數(shù)據(jù)已經(jīng)失效了,不可以讀取該狀態(tài)的數(shù)據(jù)非春。
「獨(dú)占」和「共享」?fàn)顟B(tài)都代表 Cache Block 里的數(shù)據(jù)是干凈的柱徙,也就是說,這個(gè)時(shí)候 Cache Block 里的數(shù)據(jù)和內(nèi)存里面的數(shù)據(jù)是一致性的税娜。
「獨(dú)占」和「共享」的差別在于坐搔,獨(dú)占狀態(tài)的時(shí)候藏研,數(shù)據(jù)只存儲在一個(gè) CPU 核心的 Cache 里敬矩,而其他 CPU 核心的 Cache 沒有該數(shù)據(jù)。這個(gè)時(shí)候蠢挡,如果要向獨(dú)占的 Cache 寫數(shù)據(jù)弧岳,就可以直接自由地寫入,而不需要通知其他 CPU 核心业踏,因?yàn)橹挥心氵@有這個(gè)數(shù)據(jù)禽炬,就不存在緩存一致性的問題了,于是就可以隨便操作該數(shù)據(jù)勤家。
另外腹尖,在「獨(dú)占」?fàn)顟B(tài)下的數(shù)據(jù),如果有其他核心從內(nèi)存讀取了相同的數(shù)據(jù)到各自的 Cache 伐脖,那么這個(gè)時(shí)候热幔,獨(dú)占狀態(tài)下的數(shù)據(jù)就會變成共享狀態(tài)乐设。
那么,「共享」?fàn)顟B(tài)代表著相同的數(shù)據(jù)在多個(gè) CPU 核心的 Cache 里都有绎巨,所以當(dāng)我們要更新 Cache 里面的數(shù)據(jù)的時(shí)候近尚,不能直接修改,而是要先向所有的其他 CPU 核心廣播一個(gè)請求场勤,要求先把其他核心的 Cache 中對應(yīng)的 Cache Line 標(biāo)記為「無效」?fàn)顟B(tài)戈锻,然后再更新當(dāng)前 Cache 里面的數(shù)據(jù)。
舉個(gè)例子
<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`當(dāng) A 號 CPU 核心從內(nèi)存讀取變量 i 的值和媳,數(shù)據(jù)被緩存在 A 號 CPU 核心自己的 Cache 里面格遭,此時(shí)其他 CPU 核心的 Cache 沒有緩存該數(shù)據(jù),于是標(biāo)記 Cache Line 狀態(tài)為「獨(dú)占」留瞳,此時(shí)其 Cache 中的數(shù)據(jù)與內(nèi)存是一致的如庭;
然后 B 號 CPU 核心也從內(nèi)存讀取了變量 i 的值,此時(shí)會發(fā)送消息給其他 CPU 核心撼港,由于 A 號 CPU 核心已經(jīng)緩存了該數(shù)據(jù)坪它,所以會把數(shù)據(jù)返回給 B 號 CPU 核心。在這個(gè)時(shí)候帝牡, A 和 B 核心緩存了相同的數(shù)據(jù)往毡,Cache Line 的狀態(tài)就會變成「共享」,并且其 Cache 中的數(shù)據(jù)與內(nèi)存也是一致的靶溜;
當(dāng) A 號 CPU 核心要修改 Cache 中 i 變量的值开瞭,發(fā)現(xiàn)數(shù)據(jù)對應(yīng)的 Cache Line 的狀態(tài)是共享狀態(tài),則要向所有的其他 CPU 核心廣播一個(gè)請求罩息,要求先把其他核心的 Cache 中對應(yīng)的 Cache Line 標(biāo)記為「無效」?fàn)顟B(tài)嗤详,然后 A 號 CPU 核心才更新 Cache 里面的數(shù)據(jù),同時(shí)標(biāo)記 Cache Line 為「已修改」?fàn)顟B(tài)瓷炮,此時(shí) Cache 中的數(shù)據(jù)就與內(nèi)存不一致了葱色。
如果 A 號 CPU 核心「繼續(xù)」修改 Cache 中 i 變量的值,由于此時(shí)的 Cache Line 是「已修改」?fàn)顟B(tài)娘香,因此不需要給其他 CPU 核心發(fā)送消息苍狰,直接更新數(shù)據(jù)即可。
如果 A 號 CPU 核心的 Cache 里的 i 變量對應(yīng)的 Cache Line 要被「替換」烘绽,發(fā)現(xiàn) Cache Line 狀態(tài)是「已修改」?fàn)顟B(tài)淋昭,就會在替換前先把數(shù)據(jù)同步到內(nèi)存。` </pre>
所以安接,可以發(fā)現(xiàn)當(dāng) Cache Line 狀態(tài)是「已修改」或者「獨(dú)占」?fàn)顟B(tài)時(shí)翔忽,修改更新其數(shù)據(jù)不需要發(fā)送廣播給其他 CPU 核心,這在一定程度上減少了總線帶寬壓力。
事實(shí)上歇式,整個(gè) MESI 的狀態(tài)可以用一個(gè)有限狀態(tài)機(jī)來表示它的狀態(tài)流轉(zhuǎn)矢赁。還有一點(diǎn),對于不同狀態(tài)觸發(fā)的事件操作贬丛,可能是來自本地 CPU 核心發(fā)出的廣播事件撩银,也可以是來自其他 CPU 核心通過總線發(fā)出的廣播事件。下圖即是 MESI 協(xié)議的狀態(tài)圖:
MESI 協(xié)議的四種狀態(tài)之間的流轉(zhuǎn)過程豺憔,我匯總成了下面的表格额获,你可以更詳細(xì)的看到每個(gè)狀態(tài)轉(zhuǎn)換的原因:
mesi可視化
MMU
百度百科:MMU是Memory Management Unit的縮寫,中文名是內(nèi)存管理單元恭应,有時(shí)稱作分頁內(nèi)存管理單元(英語:paged memory management unit抄邀,縮寫為PMMU)。它是一種負(fù)責(zé)處理中央處理器(CPU)的內(nèi)存訪問請求的計(jì)算機(jī)硬件昼榛。
為什么需要mmu境肾?
一
在單片機(jī)時(shí)代,是沒有操作系統(tǒng)的胆屿,每次寫完代碼都需要借助工具把程序燒進(jìn)去奥喻,這樣程序才能跑起來,單片機(jī)的CPU是直接操作內(nèi)存的物理地址
在這種情況下要想在內(nèi)存中同時(shí)運(yùn)行兩個(gè)程序是不可能的非迹。比如第一個(gè)程序在2000這個(gè)寫入一個(gè)新的值环鲤,將會擦掉第二個(gè)程序存放在相同位置的內(nèi)容。
所以操作系統(tǒng)引入虛擬地址憎兽,進(jìn)程都有自己的冷离,互不干涉。
操作系統(tǒng)會提供一種機(jī)制纯命,將不同進(jìn)程的虛擬地址和不同內(nèi)存的物理地址映射起來西剥。 如果程序要訪問虛擬地址的時(shí)候,由操作系統(tǒng)轉(zhuǎn)換成不同的物理地址亿汞,這樣不同的進(jìn)程運(yùn)行的時(shí)候瞭空,寫入 的是不同的物理地址,這樣就不會沖突了留夜。
二
在現(xiàn)在的硬件情況下匙铡,雖然內(nèi)存在一步步的變大,但是對應(yīng)的程序使用的內(nèi)存也在一步步變大碍粥,這個(gè)時(shí)候虛擬內(nèi)存就可以提供遠(yuǎn)遠(yuǎn)超實(shí)際內(nèi)存限制的空間,使計(jì)算機(jī)能夠同時(shí)執(zhí)行更多的程序黑毅。
這個(gè)edge瀏覽器占用的內(nèi)存
這個(gè)是詳情
[圖片上傳中...(image-341cb4-1640921419596-5)]
mmu的好處
- 為編程提供方便統(tǒng)一的內(nèi)存空間抽象嚼摩,在應(yīng)用開發(fā)而言,好似都完全擁有各自獨(dú)立的用戶內(nèi)存空間的訪問權(quán)限,這樣隱藏了底層實(shí)現(xiàn)細(xì)節(jié)枕面,提供了統(tǒng)一可移植用戶抽象愿卒。
- 以最小的開銷換取性能最大化,利用MMU管理內(nèi)存肯定不如直接對內(nèi)存進(jìn)行訪問效率高潮秘,為什么需要用這樣的機(jī)制進(jìn)行內(nèi)存管理琼开,是因?yàn)椴l(fā)進(jìn)程每個(gè)進(jìn)程都擁有完整且相互獨(dú)立的內(nèi)存空間。那么實(shí)際上內(nèi)存是昂貴的枕荞,即使內(nèi)存成本遠(yuǎn)比從前便宜柜候,但是應(yīng)用進(jìn)程對內(nèi)存的尋求仍然無法在實(shí)際硬件中,設(shè)計(jì)足夠大的內(nèi)存實(shí)現(xiàn)直接訪問躏精,即使能滿足渣刷,CPU利用地址總線直接尋址空間也是有限的。
- 其實(shí)更重要的不是擴(kuò)展了內(nèi)存而是給每個(gè)程序提供了一個(gè)連續(xù)的內(nèi)存空間矗烛,降低了我們操作內(nèi)存的復(fù)雜性辅柴。
實(shí)際上虛擬內(nèi)存可以實(shí)現(xiàn)的是 將內(nèi)存看作一個(gè)存儲在硬盤上的虛擬地址空間的高速緩存,并且只在內(nèi)存中緩存活動(dòng)區(qū)域(比如一個(gè)瀏覽器打開需要200mb內(nèi)存 每個(gè)頁面需要200內(nèi)存 瀏覽器即使開十幾個(gè)頁面瞭吃,內(nèi)存占用也只是400mb碌嘀,當(dāng)然這是一個(gè)簡單的例子)
分頁
分?是把整個(gè)虛擬和物理內(nèi)存空間切成一段段固定尺寸的大小。這樣一個(gè)連續(xù)并且尺寸固定的內(nèi)存空間歪架, 我們叫?(Page)筏餐。在 Linux 下,每一?的大小為 4KB 牡拇。
CPU在獲得虛擬地址之后魁瞪,需要通過MMU將虛擬地址翻譯為物理地址。而在翻譯的過程中還需要借助頁表惠呼,所謂頁表就是一個(gè)存放在物理內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)导俘,它記錄了虛擬頁與物理頁的映射關(guān)系。
頁表是一個(gè)元素為頁表?xiàng)l目(Page Table Entry, PTE)的集合剔蹋,每個(gè)虛擬頁在頁表中一個(gè)固定偏移量的位置上都有一個(gè)PTE旅薄。下面是PTE僅含有一個(gè)有效位標(biāo)記的頁表結(jié)構(gòu),該有效位代表這個(gè)虛擬頁是否被緩存在物理內(nèi)存中泣崩。
<figcaption style="margin-top: 5px; text-align: center; color: #888; display: block; font-size: 12px; font-family: PingFangSC-Light;">image-20211228160304209</figcaption>
虛擬頁VP 0
少梁、VP 4
、VP 6
矫付、VP 7
被緩存在物理內(nèi)存中凯沪,虛擬頁VP 2
和VP 5
被分配在頁表中,但并沒有緩存在物理內(nèi)存买优,虛擬頁VP 1
和VP 3
還沒有被分配妨马。
在進(jìn)行動(dòng)態(tài)內(nèi)存分配時(shí)挺举,例如malloc()
函數(shù)或者其他高級語言中的new
關(guān)鍵字,操作系統(tǒng)會在硬盤中創(chuàng)建或申請一段虛擬內(nèi)存空間烘跺,并更新到頁表(分配一個(gè)PTE湘纵,使該P(yáng)TE指向硬盤上這個(gè)新創(chuàng)建的虛擬頁)。
由于CPU每次進(jìn)行地址翻譯的時(shí)候都需要經(jīng)過PTE滤淳,所以如果想控制內(nèi)存系統(tǒng)的訪問梧喷,可以在PTE上添加一些額外的許可位(例如讀寫權(quán)限、內(nèi)核權(quán)限等)脖咐,這樣只要有指令違反了這些許可條件铺敌,CPU就會觸發(fā)一個(gè)一般保護(hù)故障,將控制權(quán)傳遞給內(nèi)核中的異常處理程序文搂。一般這種異常被稱為“段錯(cuò)誤(Segmentation Fault)”适刀。
頁命中
<figcaption style="margin-top: 5px; text-align: center; color: #888; display: block; font-size: 12px; font-family: PingFangSC-Light;">image-20211228160408348</figcaption>
如上圖所示,MMU根據(jù)虛擬地址在頁表中尋址到了PTE 4
煤蹭,該P(yáng)TE的有效位為1笔喉,代表該虛擬頁已經(jīng)被緩存在物理內(nèi)存中了,最終MMU得到了PTE中的物理內(nèi)存地址(指向PP 1
)硝皂。
缺頁
[圖片上傳中...(image-7eaffe-1640921419594-2)]
<figcaption style="margin-top: 5px; text-align: center; color: #888; display: block; font-size: 12px; font-family: PingFangSC-Light;">image-20211228160442595</figcaption>
如上圖所示常挚,MMU根據(jù)虛擬地址在頁表中尋址到了PTE 2
,該P(yáng)TE的有效位為0稽物,代表該虛擬頁并沒有被緩存在物理內(nèi)存中奄毡。虛擬頁沒有被緩存在物理內(nèi)存中(緩存未命中)被稱為缺頁。
當(dāng)CPU遇見缺頁時(shí)會觸發(fā)一個(gè)缺頁異常贝或,缺頁異常將控制權(quán)轉(zhuǎn)向操作系統(tǒng)內(nèi)核吼过,然后調(diào)用內(nèi)核中的缺頁異常處理程序,該程序會選擇一個(gè)犧牲頁咪奖,如果犧牲頁已被修改過盗忱,內(nèi)核會先將它復(fù)制回硬盤(采用寫回機(jī)制而不是直寫也是為了盡量減少對硬盤的訪問次數(shù)),然后再把該虛擬頁覆蓋到犧牲頁的位置羊赵,并且更新PTE趟佃。
當(dāng)缺頁異常處理程序返回時(shí),它會重新啟動(dòng)導(dǎo)致缺頁的指令昧捷,該指令會把導(dǎo)致缺頁的虛擬地址重新發(fā)送給MMU闲昭。由于現(xiàn)在已經(jīng)成功處理了缺頁異常,所以最終結(jié)果是頁命中靡挥,并得到物理地址序矩。
這種在硬盤和內(nèi)存之間傳送頁的行為稱為頁面調(diào)度(paging):頁從硬盤換入內(nèi)存和從內(nèi)存換出到硬盤。當(dāng)缺頁異常發(fā)生時(shí)芹血,才將頁面換入到內(nèi)存的策略稱為按需頁面調(diào)度(demand paging)贮泞,所有現(xiàn)代操作系統(tǒng)基本都使用的是按需頁面調(diào)度的策略楞慈。
虛擬內(nèi)存跟CPU高速緩存(或其他使用緩存的技術(shù))一樣依賴于局部性原則幔烛。雖然處理缺頁消耗的性能很多(畢竟還是要從硬盤中讀瓤胁痢),而且程序在運(yùn)行過程中引用的不同虛擬頁的總數(shù)可能會超出物理內(nèi)存的大小饿悬,但是局部性原則保證了在任意時(shí)刻令蛉,程序?qū)②呄蛴谠谝粋€(gè)較小的活動(dòng)頁面(active page)集合上工作,這個(gè)集合被稱為工作集(working set)狡恬。根據(jù)空間局部性原則(一個(gè)被訪問過的內(nèi)存地址以及其周邊的內(nèi)存地址都會有很大幾率被再次訪問)與時(shí)間局部性原則(一個(gè)被訪問過的內(nèi)存地址在之后會有很大幾率被再次訪問)珠叔,只要將工作集緩存在物理內(nèi)存中,接下來的地址翻譯請求很大幾率都在其中弟劲,從而減少了額外的硬盤流量祷安。
如果一個(gè)程序沒有良好的局部性,將會使工作集的大小不斷膨脹兔乞,直至超過物理內(nèi)存的大小汇鞭,這時(shí)程序會產(chǎn)生一種叫做抖動(dòng)(thrashing)的狀態(tài),頁面會不斷地?fù)Q入換出庸追,如此多次的讀寫硬盤開銷霍骄,性能自然會十分“恐怖”。所以淡溯,想要編寫出性能高效的程序读整,首先要保證程序的時(shí)間局部性與空間局部性。
多級頁表
在 32 位的環(huán)境下咱娶,虛擬地址空間共有 4GB米间,假設(shè)一個(gè)?的大小是 4KB(2^12),那么就需要大約 100 萬 (2^20) 個(gè)?膘侮,每個(gè)「?表項(xiàng)」需要 4 個(gè)字節(jié)大小來存儲屈糊,那么整個(gè) 4GB 空間的映射就需要有 4MB 的內(nèi)存來存儲?表。
這 4MB 大小的?表喻喳,看起來也不是很大另玖。但是要知道每個(gè)進(jìn)程都是有自己的虛擬地址空間的,也就說都有 自己的?表表伦。
那么谦去, 100 個(gè)進(jìn)程的話,就需要 400MB 的內(nèi)存來存儲?表蹦哼,這是非常大的內(nèi)存了鳄哭,更別說 64 位的環(huán) 境了。
要解決上面的問題纲熏,就需要采用一種叫作多級?表(Multi-Level Page Table)的解決方案妆丘。
我們把這個(gè) 100 多萬個(gè)「?表項(xiàng)」的單級?表再分?锄俄,將?表(一級?表)分為 1024 個(gè)?表(二級? 表),每個(gè)表(二級?表)中包含 1024 個(gè)「?表項(xiàng)」勺拣,形成二級分?奶赠。
[圖片上傳中...(image-a1f95b-1640921419594-1)]
<figcaption style="margin-top: 5px; text-align: center; color: #888; display: block; font-size: 12px; font-family: PingFangSC-Light;">image-20211228160859548</figcaption>
雖然分了二級表,映射 4GB 地址空間就需要 4KB(一級?表)+ 4MB(二級?表)的內(nèi)存药有,這樣 占用空間不是更大了嗎?
當(dāng)然如果 4GB 的虛擬地址全部都映射到了物理內(nèi)存上的話毅戈,二級分?占用空間確實(shí)是更大了,但是愤惰,我們 往往不會為一個(gè)進(jìn)程分配那么多內(nèi)存苇经。
如果使用了二級分?,一級?表就可以覆蓋整個(gè) 4GB 虛擬地址空間宦言,但如果某個(gè)一級?表的?表項(xiàng)沒有被 用到扇单,也就不需要?jiǎng)?chuàng)建這個(gè)?表項(xiàng)對應(yīng)的二級?表了,即可以在需要時(shí)才創(chuàng)建二級?表奠旺。做個(gè)簡單的計(jì) 算蜘澜,假設(shè)只有 20% 的一級?表項(xiàng)被用到了,那么?表占用的內(nèi)存空間就只有 4KB(一級?表) + 20% * 4MB(二級?表)= 0.804MB 凉倚,這對比單級?表的 4MB 是巨大的節(jié)約
這個(gè)結(jié)構(gòu)看起來很像是一個(gè)B-Tree
兼都,這種層次結(jié)構(gòu)有效的減緩了內(nèi)存要求:
- 如果一個(gè)一級頁表的一個(gè)PTE是空的,那么相應(yīng)的二級頁表也不會存在稽寒。這代表一種巨大的潛在節(jié)約(對于一個(gè)普通的程序來說扮碧,虛擬地址空間的大部分都會是未分配的)。
- 只有一級頁表才總是需要緩存在內(nèi)存中的杏糙,這樣虛擬內(nèi)存系統(tǒng)就可以在需要時(shí)創(chuàng)建慎王、頁面調(diào)入或調(diào)出二級頁表(只有經(jīng)常使用的二級頁表才會被緩存在內(nèi)存中),這就減少了內(nèi)存的壓力宏侍。
對于 64 位的系統(tǒng)赖淤,兩級分?肯定不夠了,就變成了四級目錄谅河,分別是:
全局?目錄項(xiàng) PGD(Page Global Directory); 上層?目錄項(xiàng) PUD(Page Upper Directory); 中間?目錄項(xiàng) PMD(Page Middle Directory); ?表項(xiàng) PTE(Page Table Entry);
TLB
多級?表雖然解決了空間上的問題咱旱,但是虛擬地址到物理地址的轉(zhuǎn)換就多了幾道轉(zhuǎn)換的工序,這顯然就降 低了這倆地址轉(zhuǎn)換的速度绷耍,也就是帶來了時(shí)間上的開銷吐限。
又是無處不在的局部性原理
我們就可以利用這一原理,把最常訪問的幾個(gè)?表項(xiàng)存儲到訪問速度更快的硬件褂始,于是計(jì)算機(jī)科學(xué)家們诸典, 就在 CPU 芯片中,加入了一個(gè)專?存放程序最常訪問的?表項(xiàng)的 Cache崎苗,這個(gè) Cache 就是 TLB (Translation Lookaside Buffer) 狐粱,通常稱為?表緩存舀寓、轉(zhuǎn)址旁路緩存、快表等肌蜻。
[圖片上傳中...(image-bc339e-1640921419594-0)]
<figcaption style="margin-top: 5px; text-align: center; color: #888; display: block; font-size: 12px; font-family: PingFangSC-Light;">image-20211228161346517</figcaption>
在 CPU 芯片里面互墓,封裝了內(nèi)存管理單元(Memory Management Unit)芯片,它用來完成地址轉(zhuǎn)換和 TLB 的訪問與交互宋欺。
有了 TLB 后轰豆,那么 CPU 在尋址時(shí)胰伍,會先查 TLB齿诞,如果沒找到,才會繼續(xù)查常規(guī)的?表骂租。 TLB 的命中率其實(shí)是很高的,因?yàn)槌绦蜃畛TL問的?就那么幾個(gè)。
公粽號:【堆棧future】
參考
虛擬內(nèi)存的那點(diǎn)事兒 - 掘金 (juejin.cn)
[零拷貝是什么妙蔗? - 云+社區(qū) - 騰訊云 (tencent.com)](https://cloud.tencent.com/developer/article/1921341#:~:text=MMU 的核心思想是利用虛擬地址替代物理地址呆抑,即 CPU 尋址時(shí)使用虛址,由 MMU,負(fù)責(zé)將虛址映射為物理地址互站。 MMU的引入私蕾,解決了對物理內(nèi)存的限制, 對程序來說胡桃,就像自己在使用 4G 內(nèi)存一樣 踩叭。)