緒論
SMP(對稱多處理)架構簡單的說就是多個CPU核,共享同一個內存和總線。L1 cache也叫芯片緩存,一般是CPU Core私有的,即每個CPU核一個,L2 cache可能是私有的也可能是部分共享的,L3 cache則多數是共享的。false-sharing是在SMP的架構下常見的問題。
false-sharing產生背景及原因
CPU利用cache和內存之間交換數據的最小粒度不是字節,而是稱為cache line的一塊固定大小的區域,緩存行是內存交換的實際單位。緩存行是2的整數冪個連續字節,一般為32-256個字節,最常見的緩存行大小是64個字節。
在寫多線程代碼時,為了避免使用鎖,通常會采用這樣的數據結構:根據線程的數目,安排一個數組, 每個線程一個項,互相不沖突。從邏輯上看這樣的設計無懈可擊,但是實踐的過程可能會發現有些場景下非但沒提高執行速度,反而會性能很差,而且年輕司機通常很難定位問題所在。
問題在於cpu的cache line,當多線程修改互相獨立的變量時,如果這些變量共享同一個緩存行,就會無意中影響彼此的性能,這就是偽共享,即false-sharing。
實際案例
在多處理器,多線程情況下,如果兩個線程分別運行在不同的CPU上,而其中某個線程修改了cache line中的元素,由於cache一致性的原因,另一個線程的cache line被宣告無效,在下一次訪問時會出現一次cache line miss,大量的cache line miss會導致性能的顯著下降。究其原因,cache line miss是由於兩個線程的Cache line有重合(非共享的變量實際上卻共享的使用了同一個cacheline,導致競爭)引起的。
如在Intel Core 2 Duo處理器平台上,L2 cache是由兩個core共享的,而L1 data cache是分開的,由兩個core分別存取。cache line的大小是64 Bytes。假設有個全局共享結構體變量f由2個線程A和B共享讀寫,該結構體一共8個字節同時位於同一條cache line上。
struct foo { int x; int y; };
若此時兩個線程一個讀取f.x另一個讀取f.y,即便兩個線程的執行是在獨立的cpu core上的,實際上結構體對象f被分別讀入到兩個CPUs的cache line中且該cache line 處於shared狀態。此時在核心1上運行的線程A想更新變量X,同時核心2上的線程B想要更新變量Y。
如果核心1上線程A優先獲得了所有權,線程A修改f.x會使該CPU core 1 上的這條cache line將變為modified狀態,另一個CPU core 2上對應的cache line將變成invalid狀態;此時若線程B馬上讀取f.y,為了確保cache一致性,B所在CPU核上的相應cache line的數據必須被更新;當核心2上線程B優先獲得了所有權然后執行更新操作,核心1就要使自己對應的緩存行失效。這會來來回回的經過L3緩存,大大影響了性能。如果互相競爭的核心位於不同的插槽,就要額外橫跨插槽連接,若讀寫的次數頻繁,將增大cache miss的次數,嚴重影響系統性能。
雖然在memory的角度這兩種的訪問時隔離的,但是由於錯誤的緊湊地放在了一起,是的兩個變量處於同一個緩存行中。每個線程都要去競爭緩存行的所有權來更新變量。可見,false sharing會導致多核處理器上對於緩存行cache line的寫競爭,造成嚴重的系統性能下降,有人將偽共享描述成無聲的性能殺手,因為從代碼中很難看清楚是否會出現偽共享。
false-sharing避免方法
把每個項湊齊cache line的長度,即可實現隔離,雖然這不可避免的會浪費一些內存。
- 對於共享數組而言,增大數組元素的間隔使得由不同線程存取的數組元素位於不同的cache line上,使一個核上的Cache line修改不會影響其它核;或者在每個線程中創建全局數組的本地拷貝,然后執行結束后再寫回全局數組,此方法比較粗暴不優雅。
- 對於共享結構體而言,使每個結構體成員變量按照Cache Line大小(一般64B)對齊。可能需要使用#pragma宏。
注意事項
單線程或單核多線程都不存在這個問題,因為只有一個CPU核也即只有一個L1 Cache,不存在緩存一致性的問題。
示例程序
注意程序中的LEVEL1_DCACHE_LINESIZE宏來自g++編譯命令傳入的,使用Shell命令getconf LEVEL1_DCACHE_LINESIZE能獲取cpu cache line的大小。(有關getconf命令的使用可以自行google)
#include <stdio.h> #include <sys/time.h> #include <time.h> #include <pthread.h> #define PACK __attribute__ ((packed)) typedef int cache_line_int __attribute__((aligned(LEVEL1_DCACHE_LINESIZE))); #ifdef FS struct data { cache_line_int a; cache_line_int b; }; #endif #ifdef NONFS struct data { int a; int b; }; #endif #define MAX_NUM 500000000 void* thread_func_1(void* param) { timeval start, end; gettimeofday(&start, NULL); data* d = (data*)param; for (int i=0; i<MAX_NUM; ++i) { ++d->a; } gettimeofday(&end, NULL); printf("thread 1, time=%d\n", (int)(end.tv_sec-start.tv_sec)*1000000+(int)(end.tv_usec-start.tv_usec)); return NULL; } void* thread_func_2(void* param) { timeval start, end; gettimeofday(&start, NULL); data* d = (data*)param; for (int i=0; i<MAX_NUM; ++i) { ++d->b; } gettimeofday(&end, NULL); printf("thread 2, time=%d\n", (int)(end.tv_sec-start.tv_sec)*1000000+(int)(end.tv_usec-start.tv_usec)); return NULL; } int main() { data d = {a:0, b:0}; printf("sizeof(data) : %d\n", sizeof(data)); pthread_t t1, t2; pthread_create(&t1, NULL, thread_func_1, &d); pthread_create(&t2, NULL, thread_func_2, &d); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("end, a=%d,b=%d\n", d.a, d.b); return 0; }
編譯、運行可以看到結果對比:
/*編譯指令*/ g++ -o 1 1.cpp -g -Wall -lpthread -DLEVEL1_DCACHE_LINESIZE=`getconf LEVEL1_DCACHE_LINESIZE` -DFS g++ -o 1 1.cpp -g -Wall -lpthread -DLEVEL1_DCACHE_LINESIZE=`getconf LEVEL1_DCACHE_LINESIZE` -DNONFS /*輸出結果:*/ thread 1, time=1607430 thread 2, time=1629508
我的騰訊雲主機只有一個CPU核,所以運行的結果並沒有差異,但是在多核CPU上執行大約相差2~3倍。
注:本文整理自多篇文章,參考文章列表后續補充。