大規模資料處理漫談【4】(流水線)

來源:互聯網
上載者:User

    我們會看到這樣的原始碼

    bool dosomething(int& count,int& sum)
   {
        if (likely(count<sum)) {
                if (unlikely(count<ZERO))

                 {
                             print_error(LESSTHANZERO);

                             return false;

                  }
                count++;
        }

        return true;
   }
   這個likely和unlikely是什麼呢?我們稱之為分支預測提示,便於指令預取。

   在 Linux 核心中最常用的最佳化技術之一是 __builtin_expect。在開發人員使用有條件代碼時,常常知道,最可能執行哪個分支,而哪個分支很少執行。如果編譯器知道這種預測資訊,就可以圍繞最可能執行的分支產生最優的代碼。

   如下所示,__builtin_expect 的使用方法基於兩個宏 likely 和 unlikely(見 ./linux/include/linux/compiler.h)。

#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

 

 

   或者還會看到這樣的原始碼

 

   for (size_t i=0; i<cnt; i+=8)
  {
       buffer[i] = value;
       buffer[i+1] = value;
       ......
       buffer[i+7] = value;
  }

      為什麼要進行迴圈展開呢?

      要理解這兩塊代碼,就必須瞭解CPU指令流水線,下面將展開討論,最後回顧這兩個例子,給出編碼的一些指導思想。

 

      減少CPU指令集中每條指令所需的時間就能最大程度發揮CPU的效用,雖然有些是軟體工程師無法控制的,但理解這些特性,能夠是程式更加面向這種硬體的設計,從而獲得最佳化的回報。

      精簡指令電腦(RISC)處理器的設計目標就是平均每個刻度執行一條指令,雖然RISC是精簡的但執行一條指令還需要多個步驟(需要多個刻度),怎麼可能做到每周期執行一條指令呢?

      答案是並行。

      我們考察這樣一個簡單的指令

     mov([ebx],eax)需要經過的步驟

    (1)從記憶體中擷取指令的作業碼(即這個mov指令)

    (2)更新EIP寄存器,將其值改為緊隨作業碼之後的位元組的地址(例如指令流中mov的下一個指令是jnz,則EIP的值指向jnz這個指令的地址。

    (3)對作業碼進行解碼,得到指定的指令(mov必須翻譯成機器可以執行的指令)

    (4)從原寄存器中取值(從ebx寄存器中取值)

    (5)將值儲存到目標寄存器中(寫入eax寄存器中)

 

      當然這裡由雩都是寄存器間的操作,因此步驟比較簡單,更複雜的,如果運算元來自於記憶體,EIP寄存器還需要進一些變化,作業碼+運算元+作業碼,也就是說EIP需要知道運算元的長度,才能知道下一個作業碼的位置。

      這不打算展開討論,我們來看一個基本的流水線實現。

      假定一個6級流水,定義為

     

     取作業碼  解碼作業碼(並預取運算元) 計算有效地址  擷取地址值   計算 存入結果

       

     我們來看這樣一個刻度和指令執行的過程,假定都可以並存執行(後面我們會討論流水線停滯)

 

                  T1    T2   T3    T4    T5    T6     T7    T8    T9    T10   T11  T12

指令1,7   取碼  解碼  取址  取值   計算 存值  取碼  解碼  取址  取值  計算 存值

指令2                取碼  解碼  取址   取值  計算 存值

指令3                        取碼  解碼   取址  取值  計算 存值

指令4                                取碼   解碼  取址  取值  計算  存值

指令5                                         取碼  解碼  取址  取值  計算 存值

指令6                                                 取碼  解碼  取址  取值  計算  存值

 

      理想的情況下,我們看到,在T1到T6這6個時間段中,流水線被打滿,前6個指令依次裝入流水線。從生產結果的情況看,從T6開始流水線做完了指令1,T7做完了指令2,......T11做完了指令6,T12時刻完成了指令7,產生了輪迴。這樣就呈現出(從頭T6開始)每周期執行一條指令的態勢,這一切都是並髮指令帶來的結果。

   

     但我們不難理解在執行某個指令時,必須要能夠正確地猜測出下一個指令的位置,才有可能進行正確的預取,一次錯誤的猜測會導致整個流水線毀掉,重新初始化。因此我們看到了此前的第一個例子中,編碼過程中告訴編譯器那一個指令更有可能是下一條指令,而在最大程度上避免了猶豫猜測錯下一個指令而導致的問題;還有我們需要避免跳轉,凡是出現跳轉指令都會讓編譯器去猜測下一個地址,總會猜錯,所以流水線友好的代碼是要求儘可能地避免跳轉,迴圈展開就是這樣的一個例子。

 

      另外就是要注意流水線的階段數並不是我這裡舉的簡單的6段,不同硬體劃分不同,流水越深預取失敗的代價就越大,流水越深可以提高主頻。

 

      綜上,在編碼過程中,一方面,可以通過特別最佳化協助編譯器猜測下一條指令的位置;另一方面,可以通過在演算法上選擇跳轉少的演算法來獲得流水線友好的演算法,比如倒排表壓縮PforDelta演算法,幾乎無跳轉,還可以通過迴圈展開顯示地減少跳轉。

 

     當然這裡所提到的都是理想的情況下,但事實上流水線是會停滯的,包括(1)匯流排爭用(2)資料相關(3)猜測錯下一條指令,其中第3個已經討論過,下一次會討論(1)和(2)這兩種情況,以及亂序執行方面的一些想法。

 

 

    

 

  

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.