(2)Store buffer
我們考慮另外一個場景:在上一節中step e中的操作變成CPU 0對共享變量進行寫的操作。這時候,寫的性能變得非常的差,因為CPU 0必須要等到CPU n上的cacheline 資料傳遞到其cacheline之後,才能進行寫的操作(CPU n上的cacheline 變成invalid狀态,CPU 0則切換成exclusive狀态,為後續的寫動作做準備)。而從一個CPU的cacheline傳遞資料到另外一個CPU的cacheline是非常消耗時間的,而這時候,CPU 0的寫的動作隻是hold住,直到cacheline的資料完成傳遞。而實際上,這樣的等待是沒有意義的,是以,這時候cacheline的資料仍然會被覆寫掉。為了解決這個問題,多核系統中的cache修改如下:
這樣,問題解決了,寫操作不必等到cacheline被加載,而是直接寫到store buffer中然後歡快的去幹其他的活。在CPU n的cacheline把資料傳遞到其cache 0的cacheline之後,硬體将store buffer中的内容寫入cacheline。
雖然性能問題解決了,但是邏輯錯誤也随之引入,我們可以看下面的例子:
我們假設a和b是共享變量,初始值都是0,可以被cpu0和cpu1通路。cpu 0的cache中儲存了b的值(exclusive狀态),沒有a的值,而cpu 1的cache中儲存了a的值,沒有b的值,cpu 0執行的彙編代碼是(用的是ARM彙編,沒有辦法,其他的都不是那麼熟悉):
ldr r2, [pc, #28] -------------------------- 取變量a的位址
ldr r4, [pc, #20] -------------------------- 取變量b的位址
mov r3, #1
str r3, [r2] --------------------------a=1
str r3, [r4] --------------------------b=1
CPU 1執行的代碼是:
ldr r2, [pc, #28] -------------------------- 取變量a的位址
ldr r3, [pc, #20] -------------------------- 取變量b的位址
start: ldr r3, [r3] -------------------------- 取變量b的值
cmp r3, #0 ------------------------ b的值是否等于0?
beq start ------------------------ 等于0的話跳轉到start
ldr r2, [r2] -------------------------- 取變量a的值
當cpu 1執行到--取變量a的值--這條指令的時候,b已經是被cpu0修改為1了,這也就是說a=1這個代碼已經執行了,是以,從彙編代碼的邏輯來看,這時候a值應該是确定的1。然而并非如此,cpu 0和cpu 1執行的指令和動作描述如下:
對于硬體,CPU不清楚具體的代碼邏輯,它不可能直接幫助軟體工程師,隻是提供一些memory barrier的指令,讓軟體工程師告訴CPU他想要的記憶體通路邏輯順序。這時候,cpu 0的代碼修改如下:
確定清空store buffer的memory barrier instruction
這種情況下,cpu 0和cpu 1執行的指令和動作描述如下:
由于增加了memory barrier,保證了a、b這兩個變量的通路順序,進而保證了程式邏輯。
(3)Invalidate Queue
我們先回憶一下為何出現了stroe buffer:為了加快cache miss狀态下寫的性能,硬體提供了store buffer,以便讓CPU先寫入,進而不必等待invalidate ack(這些互動是為了保證各個cpu的cache的一緻性)。然而,store buffer的size比較小,不需要特别多的store指令(假設每次都是cache miss)就可以将store buffer填滿,這時候,沒有空間寫了,是以CPU也隻能是等待invalidate ack了,這個狀态和memory barrier指令的效果是一樣的。
怎麼解決這個問題?CPU設計的硬體工程師對性能的追求是不會停歇的。我們首先看看invalidate ack為何如此之慢呢?這主要是因為cpu在收到invalidate指令後,要對cacheline執行invalidate指令,確定該cacheline的确是invalid狀态後,才會發送ack。如果cache正忙于其他工作,當然不能立刻執行invalidate指令,也就無法會ack。
怎麼破?CPU設計的硬體工程師提供了下面的方法:
Invalidate Queue這個HW block從名字就可以看出來是儲存invalidate請求的隊列。其他CPU發送到本CPU的invalidate指令會儲存于此,這時候,并不需要等到實際對cacheline的invalidate操作完成,CPU就可以回invalidate ack了。
同store buffer一樣,雖然性能問題解決了,但是對memory的通路順序導緻的邏輯錯誤也随之引入,我們可以看下面的例子(和store buffer中的例子類似):
我們假設a和b是共享變量,初始值都是0,可以被cpu0和cpu1通路。cpu 0的cache中儲存了b的值(exclusive狀态),而CPU 1和CPU 0的cache中都儲存了a的值,狀态是shared。cpu 0執行的彙編代碼是:
ldr r2, [pc, #28] -------------------------- 取變量a的位址
可怕的memory misorder問題又來了,都是由于引入了invalidate queue引起,看來我們還需要一個memory barrier的指令,我們将程式修改如下:
ldr r2, [pc, #28] -------------------------- 取變量a的位址
確定清空invalidate queue的memory barrier instruction