1. 程式人生 > >程式碼優化-之-優化條件分支

程式碼優化-之-優化條件分支

 程式碼優化-之-優化條件分支
                   
[email protected]   2007.10.05

tag:程式碼優化,條件分支,飽和,MMX,CMOV,掩碼

摘要: 條件分支是程式設計中經常使用的基本操作,然而在某些時候它確可能帶來嚴重的效能問題.
當前的CPU都能對條件分支做預測(動用了龐大的電晶體資源),如果分支預測正確,那麼條件
指令一般只需要花費一個CPU週期,而如果預測錯誤,那麼將可能花費幾十個CPU週期!
  本文將討論條件分支的一些有效優化方法.

正文:
  文章為收集加經驗編輯而成的文章,對優化條件分支做了較全面的闡述. 
文章假定的CPU為x86,示例程式碼為C/C++.

A.什麼是分支?
  分支是程式語言中的常見結構;分支可以分為條件分支和非條件分支;
  條件分支舉例:
    條件判斷: if (a>255) a=255; else if (a<0) a=0;
    迴圈: for (i=0;i<1000;++i) { ...; }
          while(!bOk) { bOk=...;  } 
          ...
    對應彙編指令的jnz,jg等等
  非條件分支舉例:
    函式呼叫(call),函式返回(return/ret),軟體中斷(int 3),直接跳轉(jmp),...
  
B.CPU分支預測錯誤的懲罰由來
  為了加快CPU的處理頻率,現代CPU都設計了多級流水線,有的甚至有20級以上;
當CPU遇到跳轉指令的時候,會做一個預測,把預測的分支程式碼載入流水線,當
發現預測錯誤的時候,需要清空流水線,重新載入正確的分支到流水線;那麼預
測錯誤的代價週期數至少應該和流水線長度相當;然而考慮到各級的快取失效、指
令解碼等等,實際損失的週期數有可能是流水線長度的幾倍!
   對於非條件分支,一般來說CPU都能得到相當高的預測準確率;我們主要來討論
一下條件分支的預測;
 (有人可能會說,當CPU遇到條件分支時不做預測不就沒有預測錯誤的懲罰了嗎? 
這種流水線空著的懲罰實質和每次都預測錯誤然後清空流水線的代價相當,退一步
說就算每次隨機選擇一個分支來執行也有50%的收益)

C:需要優化的條件分支
  當前的CPU對各種簡單的條件分支模式都能做出很的預測,比如奇偶模式:
  for (int i=0;i<1000;++i)
  {
     if (a%2==0) do0();
     else do1();
  }
  而對於隨機的分支模式,再好的預測器也不可能做出好的預測;
  我們要優化條件分支,這些分支程式碼應該滿足:該分支處於時間熱點上,並且
分支預測錯誤率較高;這樣我們才能得到優化的收益;
   (intel的VTune工具可以取樣分支預測錯誤率)

D.把條件分支移動到熱點外
  比如前面的那個奇偶迴圈模式,假設CPU不能正確預測,那麼可以嘗試改寫為兩個
for迴圈,一個處理偶數,一個處理奇數;
  一些影象處理演算法裡(比如模板運算/卷積運算/形態學運算等),經常需要判斷邊
界畫素點,進行特殊處理;可以考略的優化方案是把邊界區域和內部區域分開處理;
或者條件允許的話,可以擴大原影象的邊界,形成"哨兵"資料,這樣訪問畫素的時候
就不用考慮越界的問題了;

E.合併多個條件來減少條件分支
  比如: if ( (a0==0) && (a1==0) && (a2==0) ) ...
    編譯器將生成3個條件跳轉指令,而且使分支可預測性大大降低;
  可以改寫為: if ( (a0|a1|a2)==0 ) ...
    從而同時改進程式碼和分支預測率;

  比如:if ( (b0>=64) || (b1>=64)) ...  //b0,b1>=0
  改寫為: if ( (b0|b1)>=64 ) ...
  (請嘗試證明其等價性)


F.將出現機率高的分支優先處理,從而提高預測準確率

G.優化第一次執行的條件分支
  當CPU第一次執行到一個條件分支的時候,預設的預測分支規則是不跳轉的那
個分支(也就是緊接著條件跳轉指令之後的那些指令); 


  (下面的內容主要討論完全替換掉分支的一些方法; 移除分支意味著程式碼的性
能可以不受輸入資料的影響,並可能能更好的使用SIMD類指令)

H.使用條件狀態值生成掩碼來移除條件分支
  比如: if (color<0) color=0;
  改寫為: color &=-(color>=0);//求負是為了生成掩碼,也可以減1來生成掩碼

  這裡的思路是利用比較來產生0或1值,然後利用生成的值參與運算從而移除了分支;
  
  比如: if (color>255) color=255;
  改寫為: color = (color | -(color>255) ) & 0xFF;

  比如: if (a>=b) return a; else return b;
  改寫為: return  a + ( (b-a) & -(b>a) );

  (警告:這裡利用了C/C++中比較的結果是0或1,在其他語言或編譯器中可能定義不同)

I.使用帶符號的移位生成掩碼來移除條件分支
  (建議使用該方案替代上面的條件狀態值方案)

  比如: if (color<0) color=0; //color為long型別
  改寫為: color &=~(color>>31);  //帶符號移位從而生成需要的掩碼

  比如: if (color>255) color=255;
  改寫為: color = (color | ((255-color)>>31) ) & 0xFF;

  比如: if (a>=b) return a; else return b;
  改寫為: return  a + ( (b-a) & -(b>a) );

  移除分支的一個更通用的思路: 針對不同類的資料生成不同的掩碼資料,然後
讓原資料和掩碼參與運算得到想要的結果,從而移除分支;
  
J: 查表法移除分支
  比如: if (color<0) color=0; 
        else if (color>255) color=255; //假設color屬於[-256..512] 
  改寫為: color=ColorTable[color];
      其中ColorTable的建立:  
          _ColorTable[512+256+1];  ColorTable=&_ColorTable[256];
      for (i=-256;i<=512;++i)
      {
          if (i<0) ColorTable[i]=0;
          else if (i>255) ColorTable[i]=255;
          else ColorTable[i]=i;
      }
 
   比如: if (score>=90)  //score屬於[0..100] 
            return 'A';
         else if (score>=75) 
            return 'B';
         else if (score>=60)
            return 'C';
         else
            return 'D'; 
   改寫為: return scTable[score];
      其中scTable應該預先存的值就不用再寫了吧:)

K:使用CMOV條件傳送指令來移除條件分支
  (為了避免分支預測錯誤造成的效能損失,現代的CPU一般都提供了很多能夠避免
分支的指令,比如條件傳送/掩碼生成/最值等指令,請查閱指令說明和支援的CPU)

  CMOV條件傳送指令是很多條具體的指令,它們根據條件暫存器的值來決定是否賦值.
  比如: if (x<0) x=-x;
  用CMOV改寫為(彙編):
       mov   edx, eax   //假設x的值在eax暫存器,該指令使edx=eax
       neg   eax        //eax=-eax   //該指令的結果將設定條件暫存器的狀態
       cmovs eax,edx    //如果狀態為負,將edx的值傳遞給eax
       
  CMOV指令列表:
  CMOVA/CMOVNBE CMOVAE/CMOVNB/CMOVNC  CMOVB/CMOVC/CMOVNAE
  CMOVBE/CMOVNA CMOVE/CMOVZ CMOVG/CMOVNLE CMOVGE/CMOVNL
  CMOVL/CMOVNGE CMOVLE/CMOVNG CMOVNE/CMOVNZ CMOVNO
  CMOVNP/CMOVPO CMOVNS CMOVO CMOVP/CMOVPE CMOVS

  x87浮點CMOV指令列表:
  FCMOVB FCMOVBE FCMOVE FCMOVNB FCMOVNBE FCMOVNE
  FCMOVNU FCMOVU

L:使用MMX/SSE2中的飽和指令
  對於顏色的飽和處理,比如:
      if (color<0) color=0; 
        else if (color>255) color=255; 
  x86CPU從奔騰MMX開始,提供了MMX指令集(後來的SSE2也有類似指令);
增加了對飽和處理的指令支援,在影象處理和聲音處理中得到了廣泛應用;

(我的blog的很多文章有使用MMX/SSE指令的例子)
(MMX/SSE之類的SIMD指令還能夠同時並行執行多路資料,從而加快執行速度)

M:使用CMP掩碼生成指令來移除條件分支
  比如: 
    //r = (x < y) ? a : b
    // In: MM0 = a,  MM1 = b, MM2 = x, MM3 = y
    // Out: MM0 = r
    pcmpgtd mm3, mm2 //比較y>x,生成掩碼0xFFFFFFFF 或者 0
    pand mm0, mm3    //a 或者 0
    pandn mm3, mm1   //0 或者 b
    por mm0, mm3

  CMP指令包括: 
  CMPPS,CMPSS,CMPPD,CMPS,CMPSB,CMPSW,CMPSD
  PCMPEQB, PCMPEQD, PCMPEQW, PCMPGTB, PCMPGTD, PCMPGTW
  CMPXCHG,CMPXCHG8B 等

N:使用MIN/MAX指令來移除條件分支
  MAXPS,MAXPD,MAXSS,MAXSD,MINPS,MINPD,MINSS,MINSD
  PMAXSW, PMAXUB, PMINSW, PMINUB等