深入探討用位掩碼代替分支(1):利用帶符號移位生成掩碼


  幾年前我寫了一篇“優化分支代碼——避免跳轉指令堵塞流水線”(http://blog.csdn.net/zyl910/article/details/1330614)。因當時是整理筆記,有些粗略。這幾年又有了新的心得,故決定深入探討,順便回答網友評論。

  housisong(http://blog.csdn.net/housisong)提到了用利用帶符號移位生成掩碼——
(假設n是32bit有符號數): (n>>31) 當n>=0的時候結果為0x00000000,當n<0時得到0xFFFFFFFF掩碼,然后利用該掩碼來合並分支。

  這是一個很好的思路,避免了狀態寄存器訪問。
  但該方案也有局限性——
1.某些編程語言(如VB6)沒有帶符號移位運算符。
2.僅能判斷與0比較。但在很多時候,我們需要得到特定整數比較后的掩碼。

  沒有帶符號移位運算符,問題不大。現在主流編程語言大多支持帶符號移位。比如微軟打造了VB.Net,支持帶符號移位。
  計算特定整數比較后的掩碼,最簡單的就是前文所用的方法——根據“C語言比較運算的的結果是0和1”生成掩碼。但現在已經知道該方法會訪問狀態寄存器,影響效率。有沒有不依賴狀態寄存器的辦法呢?

  其中有一個思路就是利用減法,將“與特定整數的比較”轉換為“與0的比較”。但這樣做有時會產生溢出,導致運算結果不正確 或拋出異常(當檢查整數溢出異常時)。如果再做溢出處理的話,增加了復雜度、影響效率。
  我曾因這個難題困擾很久。后來突然想到,在某些時候,其實並不需要處理溢出問題。

  因圖像處理中最常用的帶符號類型是16位整數,所以我在這里采用16位帶符號整數(signed short),在計算掩碼時應右移15位。


一、計算掩碼

1.1 與0比較

  我們先熱身一下,回顧一下與0比較時的掩碼算法——
MASK = n>>15 // “<0”時全1,“>=0”時全0

  加一個取反運算符后,可以得到這樣的掩碼——
MASK = ~(n>>15) // “>=0”時全1,“<0”時全0

  如先對n求負,可以得到這樣的掩碼——
MASK = (-n)>>15 // “>0”時全1,“<=0”時全0
注意:會產生溢出。這是因為整數一般是采用補數表示法的,對於16位帶符號數來說,值域為 [-32768, 32767],無法表示正數的32768。若忽略整數溢出異常,對-32768求負結果是-32768,進行帶符號移位后會變為全1,與“>0”的設想不符。

  再加一個取反運算符后,可以得到這樣的掩碼——
MASK = ~((-n)>>15) // “<=0”時全1,“>0”時全0
注意:當n為-32768時會產生溢出。


1.2 與X的比較

  上面的式子雖然是與0比較,但因式中沒有寫0,理解起來有點吃力。於是現在將0補上——
MASK = (n-0)>>15 // “<0”時全1,“>=0”時全0
MASK = ~((n-0)>>15) // “>=0”時全1,“<0”時全0
MASK = (0-n)>>15 // “>0”時全1,“<=0”時全0
MASK = ~((0-n)>>15) // “<=0”時全1,“>0”時全0

  觀察上式,我們可以將0替換為任意整數X——
MASK = (n-X)>>15 // “<X”時全1,“>=X”時全0
MASK = ~((n-X)>>15) // “>=X”時全1,“<X”時全0
MASK = (X-n)>>15 // “>X”時全1,“<=X”時全0
MASK = ~((X-n)>>15) // “<=X”時全1,“>X”時全0

  因為現在是任意整數X,在進行減法運算時有可能產生溢出。


二、飽和處理

  在編寫圖像處理程序時,經常出現RGB值超過[0, 255]范圍的情況。這時,得做飽和處理,將越界數值飽和到邊界,即這樣的代碼:
if (r <   0) r =   0;
if (r > 255) r = 255;

  現在我們將利用位運算,來優化這樣的代碼。

2.1 “<0”時的處理

  我們可以利用與運算將數值修正為0,應選用【“>=0”時全1,“<0”時全0】的掩碼。語句為——
MASK = ~(n>>15) // “>=0”時全1,“<0”時全0
m = n & MASK

  將其整理為一條表達式——
m = n & ~(n>>15)

  因為該式沒有用到減法,所以當n為任意值時都不會溢出。

2.2 “>255”時的處理

  我們可以利用或運算,將超過范圍的數值修正為全1。再將其與0xFF進行與運算,將超過范圍的數值修正為255,這是根據255(0xFF)正好是低8位。

  怎么判斷超過范圍呢?有三種策略——
A.“>255”。標准方式。
B.“>=256”。因整數的連續性。
C.“>=255”。因255進行飽和處理后,結果仍是255。

  因現在為了避免狀態寄存器,不能利用比較語句,只能利用前面的位掩碼算法。列出三種策略是為了找到效率最高的方案。
  回顧一下“1.2 與X的比較”,找出判斷“>X”、“>=X”的式子——
MASK = ~((n-X)>>15) // “>=X”時全1,“<X”時全0
MASK = (X-n)>>15 // “>X”時全1,“<=X”時全0

  進過對比后發現,判斷“>X”的運算量最少,所以我們應選擇策略A。語句為——
MASK = (255-n)>>15
m = (n | MASK) & 0xFF

  將其整理為一條表達式——
m = (n | ((255-n)>>15)) & 0xFF

  注意該式僅在n大於等於0時有效。

2.3 飽和處理

  現在開始考慮實際的飽和處理,即將“<0”的修正為0,又將“>255”的修正為255。

  先整理一下上面的成果——
m = n & ~(n>>15) // “<0”時的處理。n為任意值時都有效。
m = (n | ((255-n)>>15)) & 0xFF // “>255”時的處理。僅在n大於等於0時有效。

  因“>255”處理在n小於0時無效,而“<0”處理在任何時候有效。所以我們可以先進行>255”處理,再進行“<0”處理,以屏蔽中間的錯誤值。語句為——
m = ( (n | ((255-n)>>15)) & ~(n>>15) )  & 0xFF

  分析——
當n<0時:雖然“(n | ((255-n)>>15))”的值無效,但因“~(n>>15)”的值為0,進行“& ~(n>>15)”運算后,結果為0。
當n>=0且n<=255時:“((255-n)>>15)”的值為0,“| ((255-n)>>15)”會保留原值。“~(n>>15)”的值為全1,“& ~(n>>15)”也會保留原值。
當n>255時:“((255-n)>>15)”的值為全1,“~(n>>15)”的值也為全1,最后遇到“& 0xFF”,結果為255。

  由於我們一般是將結果保存到一個BYTE型變量中,進行一次強制類型轉換就行了,不需要“& 0xFF”——
m = (BYTE)( (n | ((255-n)>>15)) & ~(n>>15) )


三、實際運用

  在實際運用,上述代碼比較長不易維護。可以將其封裝為宏,並順便推廣一下——
#define LIMITSW_FAST(n, bits) ( ( (n) | ((signed short)((1<<(bits)) - 1 - (n)) >> 15) ) & ~((signed short)(n) >> 15) )
#define LIMITSW_SAFE(n, bits) ( (LIMITSW_FAST(n, bits)) & ((1<<(bits)) - 1) )

  bits代表限制多少位,如BYTE就是8——
#define LIMITSW_BYTE(n) ((BYTE)(LIMITSW_FAST(n, 8)))


四、測試代碼

  測試代碼如下——

// 用位掩碼做飽和處理.用求負生成掩碼
#define LIMITSU_FAST(n, bits) ( (n) & -((n) >= 0) | -((n) >= (1<<(bits))) )
#define LIMITSU_SAFE(n, bits) ( (LIMITSU_FAST(n, bits)) & ((1<<(bits)) - 1) )
#define LIMITSU_BYTE(n) ((BYTE)(LIMITSU_FAST(n, 8)))

// 用位掩碼做飽和處理.用帶符號右移生成掩碼
//#define LIMITSW_FAST(n, bits) ( (n) & ~((signed short)(n) >> 15) | ((signed short)((1<<(bits)) - 1 - (n)) >> 15) )
#define LIMITSW_FAST(n, bits) ( ( (n) | ((signed short)((1<<(bits)) - 1 - (n)) >> 15) ) & ~((signed short)(n) >> 15) )
#define LIMITSW_SAFE(n, bits) ( (LIMITSW_FAST(n, bits)) & ((1<<(bits)) - 1) )
#define LIMITSW_BYTE(n) ((BYTE)(LIMITSW_FAST(n, 8)))

signed short buf[0x10000]; // 將數值放在數組中,避免編譯器過度優化

int main(int argc, char* argv[])
{
int i; // 循環變量(32位)
signed short n; // 當前數值
signed short m; // 臨時變量
BYTE by0; // 用if分支做飽和處理
BYTE by1; // 用位掩碼做飽和處理.用求負生成掩碼
BYTE by2; // 用位掩碼做飽和處理.用帶符號右移生成掩碼

//printf("Hello World!\n");
printf("== noifCheck ==\n");

// 初始化buf
for(i=0; i<0x10000; ++i)
{
buf[i] = (signed short)(i - 0x8000);
//printf("%d\n", buf[i]);
}

// 檢查 “<0”處理
printf("[Test: less0]\n");
for(i=0; i<0x8100; ++i) // [-32768, 255]
//for(i=0x7FFE; i<=0x8002; ++i) // [-2, 2]
{
// 加載數值
n = buf[i];

// 用if分支做飽和處理
m = n;
if (m < 0) m = 0;
by0 = (BYTE)m;

// 用位掩碼做飽和處理.用求負生成掩碼
by1 = (BYTE)(n & -(n >= 0));
if (by1 != by0) printf("[Error] 1.1 neg: [%d] %d!=%d\n", n, by0, by1); // 驗證

// 用位掩碼做飽和處理.用帶符號右移生成掩碼
by2 = (BYTE)(n & ~((signed short)n >> 15));
if (by2 != by0) printf("[Error] 1.2 sar: [%d] %d!=%d\n", n, by0, by2); // 驗證
}

// 檢查 “>255”處理
printf("[Test: great255]\n");
for(i=0x8000; i<0x10000; ++i) // [0, 32767]
//for(i=0x80FE; i<=0x8102; ++i) // [254, 258]
{
// 加載數值
n = buf[i];

// 用if分支做飽和處理
m = n;
if (m > 255) m = 255;
by0 = (BYTE)m;

// 用位掩碼做飽和處理.用求負生成掩碼
by1 = (BYTE)(n | -(n >= 256) );
if (by1 != by0) printf("[Error] 2.1 neg: [%d] %d!=%d\n", n, by0, by1); // 驗證

// 用位掩碼做飽和處理.用帶符號右移生成掩碼
by2 = (BYTE)(n | ((signed short)(255-n) >> 15));
if (by2 != by0) printf("[Error] 2.2 sar: [%d] %d!=%d\n", n, by0, by2); // 驗證
}

// 檢查 飽和處理
printf("[Test: saturation]\n");
for(i=0; i<0x10000; ++i) // [-32768, 32767]
//for(i=0x7FFE; i<=0x8102; ++i) // [-2, 258]
{
// 加載數值
n = buf[i];

// 用if分支做飽和處理
m = n;
if (m < 0) m = 0;
else if (m > 255) m = 255;
by0 = (BYTE)m;

// 用位掩碼做飽和處理.用求負生成掩碼
by1 = LIMITSU_BYTE(n);
if (by1 != by0) printf("[Error] 3.1 neg: [%d] %d!=%d\n", n, by0, by1); // 驗證

// 用位掩碼做飽和處理.用求負生成掩碼
by2 = LIMITSW_BYTE(n);
if (by2 != by0) printf("[Error] 3.2 sar: [%d] %d!=%d\n", n, by0, by2); // 驗證
}

return 0;
}

 

 

  測試結果——

全部通過!

 

源碼下載——
http://files.cnblogs.com/zyl910/noifCheck.rar

 


免責聲明!

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



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