天天看點

指令重排和優化屏障

1. 優化帶來的煩惱

用過GCC編譯的同學應該知道GCC有O0、O1、O2、O3等優化選項,啟用這些選項往往可以提高程式的運作效率,但它并不是萬無一失的,尤其是在多線程場景下。而這些優化背後的技術正是指令重排。因為編譯器或處理器也很難确定代碼邏輯的原本意圖。

鎖能夠保持原子性,但是經過編譯器優化之後的代碼,并不是絕對時序正确的,況且處理器還有可能進一步優化。這裡面最經典的一個例子就是單例模式,Double-Checked Locking is Fixed In C++11 。

2. 核心提供的解決方案

核心提供以下方法,阻止編譯器和處理器進行指令重排

  • mb() rmb() wmb() 會将硬體記憶體屏障插入到代碼中。rmb用于讀通路記憶體屏障,wmb用于寫通路屏障,mb兼具二者。讀屏障插入到代碼中之後,保證屏障之前的讀操作代碼結束之後,屏障之後的讀操作代碼才讀。
  • barrier 插入優化屏障。屏障之前所有有效的記憶體位址,在屏障之後都将失效。也就是屏障之後,不能再讀寫了。
  • smb_mb() smb_rmb() smb_wmb() 在SMP系統産生硬體記憶體屏障,如果用在單處理器系統上産生的将是軟體屏障。
  • read_barrier_depends 會考慮讀操作之間的依賴性,設定讀通路屏障。

屏障肯定是會影響性能的,但總不能為了優化性能而讓程式出現錯誤。在任何程式面前,正确性永遠是第一位的。

說明兩個概念

記憶體屏障:rmb(), wmb(), mb(),可以防止硬體上的指令重排,針對的是處理器CPU。

優化屏障:barrier(),避免編譯器對記憶體通路的優化,針對的是編譯器。

它們在很多地方稱為記憶體栅欄,但英文的話都是memory barrier。

3. 典型的應用

3.1 安全的單例模式

Double-Checked Locking 咋一眼看山去沒什麼問題,但是考慮下指令重排,就會發現問題。如果傳回指針在new之前,那是我們不願看到的,而這裡使用屏障可以解決這個問題。

Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance;
    ...// insert memory barrier
    if (tmp == NULL) {
        Lock lock;
        tmp = m_instance;
        if (tmp == NULL) {
            tmp = new Singleton;
            ...// insert memory barrier
            m_instance = tmp;
        }
    }
    return tmp;
}
           

3.2 核心搶占

preempt_disable();
do_something();
preempt_enable();
           

在核心搶占中,preempt_disable對計數器加1,也就是告訴其他線程,不要來搶這個是我的, preempt_enable反之。

#define preempt_disable() \
do { \
    inc_preempt_count(); \
    barrier();  \
} while(0)
           
#define preempt_enable() \
do { \
     barrier();  \
    preempt_check_resched(); \
} while(0)
           

參考:

[0] 深入Linux核心架構

[1] https://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/

繼續閱讀