深入探討用位掩碼代替分支(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;
}


 

 

  測試結果——

全部通過!

發佈了68 篇原創文章 · 獲贊 43 · 訪問量 33萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章