幾年前我寫了一篇“優化分支代碼——避免跳轉指令堵塞流水線”(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