多核開發中常見的一個問題是false sharing(失效共用),這個問題讓我們用一個全新的角度來看待多核程式的編寫,這個角度就是硬體的角度。
Intel Core 2 Duo處理器平台上, L2 cache是由兩個core共用的,而L1 data cache是分開的,由兩個core分別存取。cache line的大小是64 Bytes。當不同的線程同時讀寫不同的,看起來更不相關的2個變數時,由於這2個變數實際儲存在同一條cache line上,從而會暗地裡造成cache line的存取違規而導致潛在的效能損失。例如這段代碼:
unsigned char VectorA[10];
unsigned char VectorB[10];
UINT MyThreadProcA( LPVOID pParam )
{
unsigned long long myCounter = 100000000;
while(--myCounter)
{
for (int i=0; i<10; ++i)
{
++VectorA[i];
}
}
return 0; // thread completed successfully
}
UINT MyThreadProcB( LPVOID pParam )
{
unsigned long long myCounter = 100000000;
while(--myCounter)
{
for (int i=0; i<10; ++i)
{
++VectorB[i];
}
}
return 0; // thread completed successfully
}
儘管MyThreadProc[A/B] 是兩個不同的線程,訪問的也是兩個不同的變數,但是,false sharing卻真真實實的發生了。當MyThreadProcA更新VectorA[i]的時候,對應的Core A上的cache line同時被更新,變為modified狀態,而這個cache line中又儲存了VectorB[i]的一份copy,因此,另一個Core B中的cacheline就會變為失效狀態(invalid),CPU會不得不通過cache protocol(cache的同步協議)去通知Core B上的cache line同時更新VectorB的資料,這樣,儘管MyThreadProcA沒有修改VectorB,卻會導致MyThreadProcB線程訪問VectorB時cache miss增加!而我們知道,cache的訪問速度是普通記憶體的10倍,cache miss增大自然會造成明顯的效能下降!
在Core2平台上,可以使用EXT_SNOOP.ALL_AGENTS.HITM 事件來評測false sharing的影響,它監測的是匯流排(記憶體匯流排)傳輸的情況,如果HITM事件發生,則表明匯流排上響應端的cache正處於修改狀態,這恰恰反映了false sharing問題的根源。
vtune的手冊對於EXT_SNOOP.ALL_AGENTS.HITM 的解釋的:
This event counts the snoop responses to bus transactions. Responses can be counted separately by type and by bus agent. With the 'THIS_AGENT' mask the event counts snoop responses from this processor to bus transactions sent by this processor. With the 'ALL_AGENTS' mask the event counts all snoop responses seen on the bus.
先看看看看上面這段代碼的測量結果吧!
採用sampling測量,EXT_SNOOP.ALL_AGENTS.HITM 發生次數1175次,CPU_CLK 為6373,INST_RETIRED為3796
false sharing的解決也很簡單,只要把共用的資料放到不同的cache line中即可,例如,將代碼改為:
unsigned char VectorA[100];
unsigned char VectorB[100];
這樣,使用的仍然是VectorA[0~9]和VectorB[0~9],VectorA[10~99]則充當了一個pad預留位置,把同一條cache line(64bytes)佔滿。
解決false sharing問題後的測量資料為:
EXT_SNOOP.ALL_AGENTS.HITM 顯著降到179次,CPU_CLK 降為1847,由於指令個數沒有太大變化,INST_RETIRED為3370。通過程式中內嵌計時函數的方法也能得到接近的結果。
總結,解決false sharing問題的方法:
1. 增大數組元素的間隔使得由不同線程存取的元素位於不同的cache line上
2. 在每個線程中建立全域數組各個元素的本地拷貝,然後結束後再寫回全域數組
false sharing是多核程式開發的常見問題,需要引起程式員們的重視。