閱讀430 返回首頁    go 阿裏雲 go 技術社區[雲棲]


由一道淘寶麵試題到False sharing問題


今天在看淘寶之前的一道麵試題目,內容是

在高性能服務器的代碼中經常會看到類似這樣的代碼:

typedef union
{
  erts_smp_rwmtx_t rwmtx;
  byte cache_line_align_[ERTS_ALC_CACHE_LINE_ALIGN_SIZE(sizeof(erts_smp_rwmtx_t))];
}erts_meta_main_tab_lock_t;
erts_meta_main_tab_lock_t main_tab_lock[16];

請問其中用來填充的cache_line_align的作用是?

之前有學習到c語言中宏align是內存補齊的作用,那這個不就是cache line補齊?但是啥是cache line??為啥有這麼一步?

1.首先,什麼是cache line?

CPU處理指令時,由於“Locality of Reference”原因,需要決定哪些數據需要加載到CPU的緩存中,以及如何預加載。因為不同的處理器有不同的規範,導致這部分工作具有不確定性。在加載的過程中,涉及到一個非常關鍵的術語:cache line。

cache line是能被cache處理的內存chunks,chunk的大小即為cache line size,典型的大小為32,64及128 bytes. cache能處理的內存大小除以cache line size即為cache line。

了解了cache line,然後再熟悉一下cpu上cache的一些策略

2.cpu上cache的策略

cache entry (cache條目)
包含如下部分
1) cache line : 從主存一次copy的數據大小)
2) tag : 標記cache line對應的主存的地址
3) falg : 標記當前cache line是否invalid, 如果是數據cache, 還有是否dirty

cpu訪問主存的規律
1) cpu從來都不直接訪問主存, 都是通過cache間接訪問主存
2) 每次需要訪問主存時, 遍曆一遍全部cache line, 查找主存的地址是否在某個cache line中.
3) 如果cache中沒有找到, 則分配一個新的cache entry, 把主存的內存copy到cache line中, 再從cache line中讀取.

cache中包含的cache entry條目有限, 所以, 必須有合適的cache淘汰策略
一般使用的是LRU策略.
將一些主存區域標記為non-cacheble, 可以提高cache命中率, 降低沒用的cache

回寫策略
cache中的數據更新後,需要回寫到主存, 回寫的時機有多種
1) 每次更新都回寫. write-through cache
2) 更新後不回寫,標記為dirty, 僅當cache entry被evict時才回寫
3) 更新後, 把cache entry送如回寫隊列, 待隊列收集到多個entry時批量回寫.

cache一致性問題
有兩種情況可能導致cache中的數據過期
1) DMA, 有其他設備直接更新主存的數據
2) SMP, 同一個cache line存在多個CPU各自的cache中. 其中一個CPU對其進行了更新.

3.為啥需要cache line 補齊呢?

讓我們先看一個例子,

舉例:


// 如下代碼在SMP環境下存在cache頻繁刷新問題
double sum=0.0, sum_local[NUM_THREADS];
#pragma omp parallel num_threads(NUM_THREADS)
{
 int me = omp_get_thread_num();
 sum_local[me] = 0.0;

 #pragma omp for
 for (i = 0; i < N; i++)
 sum_local[me] += x[i] * y[i];

 #pragma omp atomic
 sum += sum_local[me];
}
    因為sum_local數組是個全局變量, 多個線程都會訪問, 並且, 各個線程訪問的地方很接近, 會導致一個線程更新, 其他CPU的cache line失效.
    所以在盡量不要讓更新頻率非常高(例如,計數器)和經常訪問的變量分布在同一個cache line中,以避免“cache ping-pong”,亦“false sharing”現象。
      OK,為啥需要補齊呢,上麵的例子裏麵多個線程的訪問會出現false sharing現象,如果服務器采用這樣的,則服務器性能會嚴重影響,為了解決這個問題,最簡單的辦法是采用cache line 補齊的方法。

ps:在查找這個麵試題的時候,有意思的是我在淘寶核心係統團隊博客上發現了對這個題目的解答,我覺得簡答的不是很認真,他們是參考一篇外文文獻《Avoiding and Identifying False Sharing Among Threads》,這篇文章主要解決在SMP環境下cache line被頻繁刷新的的問題。所以隻是簡單的將大意翻譯過來。
將複製過來:

在做多線程程序的時候,為了避免使用鎖,我們通常會采用這樣的數據結構:根據線程的數目,安排一個數組, 每個線程一個項,互相不衝突. 從邏輯上看這樣的設計無懈可擊,但是實踐的過程我們會發現這樣並沒有提高速度. 問題在於cpu的cache line. 我們在讀主存的時候,數據同時被讀到L1,L2中去,而且在L1中是以cache line(通常64)字節為單位的. 每個Core都有自己的L1,L2,所以每個線程在讀取自己的項的時候, 也把別人的項讀進去, 所以在更新的時候,為了保持數據的一致性, core之間cache要進行同步, 這個會導致嚴重的性能問題. 這就是所謂的False sharing問題, 有興趣的同學可以wiki下.


解決方法很簡單:
把每個項湊齊cache line的長度,實現隔離.

1
2
3
4
5
6
7
8
typedef union {
    erts_smp_rwmtx_t rwmtx;
    byte cache_line_align__[ERTS_ALC_CACHE_LINE_ALIGN_SIZE(
                sizeof(erts_smp_rwmtx_t))];
} erts_meta_main_tab_lock_t;
或者
_declspec (align(64)) int thread1_global_variable;
__declspec (align(64)) int thread2_global_variable;

這就是為什麼在高性能服務器中到處看到cache_line_align, 號稱是避免cache的trash.

類似valgrind和intel vtune的工具可以做這個層次的性能微調.

(淘寶核心係統博客:https://rdc.taobao.com/blog/cs/?p=523
(英文文獻資料網址:https://software.intel.com/en-us/articles/avoiding-and-identifying-false-sharing-among-threads/ 
或者https://software.intel.com/sites/default/files/m/d/4/1/d/8/3-4-MemMgt_-_Avoiding_and_Identifying_False_Sharing_Among_Threads.pdf


   

最後更新:2017-04-03 16:49:04

  上一篇:go 企業級API設計
  下一篇:go ajax不跳轉頁麵提交表單