false-sharing原理淺析和測試


緒論

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的長度,即可實現隔離,雖然這不可避免的會浪費一些內存。

  1. 對於共享數組而言,增大數組元素的間隔使得由不同線程存取的數組元素位於不同的cache line上,使一個核上的Cache line修改不會影響其它核;或者在每個線程中創建全局數組的本地拷貝,然后執行結束后再寫回全局數組,此方法比較粗暴不優雅。
  2. 對於共享結構體而言,使每個結構體成員變量按照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倍。

 

注:本文整理自多篇文章,參考文章列表后續補充。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM