深入探討用位掩碼代替分支(3):VC6速度測試

  wuhanbingwhdx提到了數據相關也會影響流水線(http://blog.csdn.net/zyl910/article/details/1330614)。
  他的說法是有一定道理的。但是,在很多時候我們並不僅僅處理一個數值。比如將循環展開,在內循環處理2個或更多個的數值。而現代編譯器面對循環展開時,在編譯優化操作中會調整指令順序,錯開有相關性指令。因現代處理器支持超標量,這樣的指令順序調整能獲得較好的指令級並行度,從而優化了性能。
  其次,就算編譯器對循環展開優化的不夠徹底,沒將相關性指令錯開。但因現代處理器支持亂序執行,當遇到相關性指令需要等待時,處理器會處理後面未相關的指令,從而保持處理器滿載儘量減輕相關性等待造成的性能損失。
  第三,現代處理器還支持寄存器重命名技術——當兩處代碼用到同名的寄存器時,譯碼器會做寄存器重命名處理給它們分配不同的寄存器,使數據不會干擾,從而獲得更高的指令級並行度。

  上面說了很多理論知識,實際性能到底怎麼樣呢?還是寫代碼測一測吧。


一、目標——將64位像素轉爲32位像素

  將64位像素轉爲32位像素,是飽和處理的最典型應用。
  64位像素有4個通道,每個通道16位(帶符號16位整數)。每像素8個字節。
  32位像素有4個通道,每個通道8位(無符號8位整數)。每像素4個字節。

  轉換方法爲——將每個像素的4個通道由16位(帶符號16位整數)轉爲8位(無符號8位整數)。因爲每次都是對4個通道進行處理,所以能獲得較高的指令級並行度。

  具體的存儲格式爲——

注:
1.內存地址由低到高(從下到上),垂直方向的每一格是一個字節。+0代表數據基址,+1代表數據基址+1,以此類推。
2.左爲“64位像素”數組,右爲“32位像素”數組。
3.圖中使用了用雙邊線來分隔像素。因“64位像素”是8字節,而“32位像素”是4字節。所以對於16個字節的空間,左側能存放2個像素、右側能存放4個像素。
4.圖中使用了用實線來分隔通道。“64位像素”的通道是16位,佔用2個字節。“32位像素”的通道是8位,只佔用1個字節。
5.圖中使用了用虛線來分隔字節。主要用於“64位像素”。
6.這裏採用了Windows位圖通道規則,即通道順序爲B、G、R、A(從低到高)。例如:“B0”代表像素0的B(藍色)通道、“A0”代表像素0的A(不透明度)通道、“A1”代表像素1的A通道……以此類推。
7.這裏採用了小端(Little Endian)方式的字節序(Endianness),即最低地址存放的最低字節。採用小寫的“h”、“l”來表示“64位像素”通道的高、低字節。例如:“B0l”代表像素0的B通道的低字節、“A0h”代表像素0的A通道的高字節……以此類推。

  上面貌似挺複雜,又是圖表又是大段文字的。其實,代碼寫起來很簡單的,一般情況下不需要理會通道順序與字節序問題——

// 用if分支做飽和處理
void f0_if(BYTE* pbufD, const signed short* pbufS, int cnt)
{
	const signed short* pS = pbufS;
	BYTE* pD = pbufD;
	int i;
	for(i=0; i<cnt; ++i)
	{
		// 分別對4個通道做飽和處理
		pD[0] = (pS[0]<0) ? 0 : ( (pS[0]>255) ? 255 : (BYTE)pS[0] );
		pD[1] = (pS[1]<0) ? 0 : ( (pS[1]>255) ? 255 : (BYTE)pS[1] );
		pD[2] = (pS[2]<0) ? 0 : ( (pS[2]>255) ? 255 : (BYTE)pS[2] );
		pD[3] = (pS[3]<0) ? 0 : ( (pS[3]>255) ? 255 : (BYTE)pS[3] );
		// next
		pS += 4;
		pD += 4;
	}
}


 

  參數說明——
pbufD:目標緩衝區的地址。如“64位像素”數組的首地址。
pbufS:源緩衝區的地址。如“32位像素”數組的首地址。
cnt:像素個數。

  使用方法——

signed short	bufS[DATASIZE*4];	// 源緩衝區。64位的顏色(4通道,每通道16位)
BYTE	bufD[DATASIZE*4];	// 目標緩衝區。32位的顏色(4通道,每通道8位)

f0_if(bufD, bufS, DATASIZE);

 

  對於數據處理來說,用指針比用數組寫起來更簡潔,而且執行速度更快。
  而且C語言中的指針支持下標運算符,能夠用下標訪問後面的元素(“pD[1]”相當於“*(pD + 1)”),簡化了不少代碼。(指針下標可參考 http://www.lupaworld.com/home-space-uid-77885-do-blog-id-28843.html
  例如“pD[1] = (pS[1]<0) ? 0 : ( (pS[1]>255) ? 255 : (BYTE)pS[1] );”這行代碼的數組寫法爲——

pbufD[i*4+1] = (pbufS[i*4+1]<0) ? 0 : ( (pbufS[i*4+1]>255) ? 255 : (BYTE)pbufS[i*4+1] );



  因爲條件語句“if”的代碼寫起來比較繁瑣,所以這裏用到了條件運算符“?:”來簡化代碼。例如“pD[1] = (pS[1]<0) ? 0 : ( (pS[1]>255) ? 255 : (BYTE)pS[1] );”這行代碼的“if”寫法爲——

if (pS[1]<0)
	pD[1]=0
else
	if (pS[1]>255)
		pD[1]=255
	else
		pD[1]=(BYTE)pS[1];




二、測試方法——測試程序的框架

  前面已經編寫了一個函數f0_if,隨後我們會編寫多個函數,分別測試性能。具體怎麼測試呢?難道是爲每一個函數都寫一套測試代碼……不,那樣的話太糟糕了。
  我們可以利用函數指針進行統一的測試。函數指針定義如下,與f0_if的參數列相同——

// 測試時的函數類型
typedef void (*TESTPROC)(BYTE* pbufD, const signed short* pbufS, int cnt);



  有了函數指針後,進行測試就很簡單了,只需要將要測試的函數傳遞過去就行了。例如這樣測試f0_if——

runTest("f0_if", f0_if);

 

  runTest代碼如下——

// 進行測試
void runTest(char* szname, TESTPROC proc)
{
	int i,j;
	DWORD	tm0, tm1;	// 存儲時間
	for(i=1; i<=3; ++i)	// 多次測試
	{
		//tm0 = GetTickCount();
		tm0 = timeGetTime();
		// main
		for(j=1; j<=4000; ++j)	// 重複運算幾次延長時間,避免計時精度問題
		{
			proc(bufD, bufS, DATASIZE);
		}
		// show
		//tm1 = GetTickCount() - tm0;
		tm1 = timeGetTime() - tm0;
		printf("%s[%d]:\t%u\n", szname, i, tm1);

	}
}



  printf輸出的是測試時間,單位毫秒。值越小,表示所花時間越少、運行速度越快、性能越高。
  這裏用到了timeGetTime來計算時間,要注意加上winmm.lib庫——

  對於bufD、bufS、DATASIZE,我是這樣定義的——

// 數據規模
#define DATASIZE	16384	// 128KB / (sizeof(signed short) * 4)

// 緩衝區
signed short	bufS[DATASIZE*4];	// 源緩衝區。64位的顏色(4通道,每通道16位)
BYTE	bufD[DATASIZE*4];	// 目標緩衝區。32位的顏色(4通道,每通道8位)


  緩衝區的尺寸是特意規定的。對於現在主流CPU來說,Intel處理器的二級緩存一般是每核心256KB,而AMD處理器的二級緩存一般是每核心512KB。所以數據最好不要超過256KB,這樣就能在二級緩存上完成處理,避免了內存訪問延時造成的干擾。
  於是我給bufS分配了128KB,給bufD分配了64KB。


三、更多的測試

  用min、max做飽和處理——

// 用min、max飽和處理
void f1_min(BYTE* pbufD, const signed short* pbufS, int cnt)
{
	const signed short* pS = pbufS;
	BYTE* pD = pbufD;
	int i;
	for(i=0; i<cnt; ++i)
	{
		// 分別對4個通道做飽和處理
		pD[0] = min(max(0, pS[0]), 255);
		pD[1] = min(max(0, pS[1]), 255);
		pD[2] = min(max(0, pS[2]), 255);
		pD[3] = min(max(0, pS[3]), 255);
		// next
		pS += 4;
		pD += 4;
	}
}



  用位掩碼做飽和處理,用求負生成掩碼——

// 用位掩碼做飽和處理.用求負生成掩碼
void f2_neg(BYTE* pbufD, const signed short* pbufS, int cnt)
{
	const signed short* pS = pbufS;
	BYTE* pD = pbufD;
	int i;
	for(i=0; i<cnt; ++i)
	{
		// 分別對4個通道做飽和處理
		pD[0] = LIMITSU_BYTE(pS[0]);
		pD[1] = LIMITSU_BYTE(pS[1]);
		pD[2] = LIMITSU_BYTE(pS[2]);
		pD[3] = LIMITSU_BYTE(pS[3]);
		// next
		pS += 4;
		pD += 4;
	}
}


  用位掩碼做飽和處理,用帶符號右移生成掩碼——

// 用位掩碼做飽和處理.用帶符號右移生成掩碼
void f3_sar(BYTE* pbufD, const signed short* pbufS, int cnt)
{
	const signed short* pS = pbufS;
	BYTE* pD = pbufD;
	int i;
	for(i=0; i<cnt; ++i)
	{
		// 分別對4個通道做飽和處理
		pD[0] = LIMITSW_BYTE(pS[0]);
		pD[1] = LIMITSW_BYTE(pS[1]);
		pD[2] = LIMITSW_BYTE(pS[2]);
		pD[3] = LIMITSW_BYTE(pS[3]);
		// next
		pS += 4;
		pD += 4;
	}
}



四、全部代碼

  全部代碼爲——

// 用位掩碼做飽和處理.用求負生成掩碼
#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)((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)))


// 數據規模
#define DATASIZE	16384	// 128KB / (sizeof(signed short) * 4)

// 緩衝區
signed short	bufS[DATASIZE*4];	// 源緩衝區。64位的顏色(4通道,每通道16位)
BYTE	bufD[DATASIZE*4];	// 目標緩衝區。32位的顏色(4通道,每通道8位)

// 測試時的函數類型
typedef void (*TESTPROC)(BYTE* pbufD, const signed short* pbufS, int cnt);

// 用if分支做飽和處理
void f0_if(BYTE* pbufD, const signed short* pbufS, int cnt)
{
	const signed short* pS = pbufS;
	BYTE* pD = pbufD;
	int i;
	for(i=0; i<cnt; ++i)
	{
		// 分別對4個通道做飽和處理
		pD[0] = (pS[0]<0) ? 0 : ( (pS[0]>255) ? 255 : (BYTE)pS[0] );
		pD[1] = (pS[1]<0) ? 0 : ( (pS[1]>255) ? 255 : (BYTE)pS[1] );
		pD[2] = (pS[2]<0) ? 0 : ( (pS[2]>255) ? 255 : (BYTE)pS[2] );
		pD[3] = (pS[3]<0) ? 0 : ( (pS[3]>255) ? 255 : (BYTE)pS[3] );
		// next
		pS += 4;
		pD += 4;
	}
}

// 用min、max飽和處理
void f1_min(BYTE* pbufD, const signed short* pbufS, int cnt)
{
	const signed short* pS = pbufS;
	BYTE* pD = pbufD;
	int i;
	for(i=0; i<cnt; ++i)
	{
		// 分別對4個通道做飽和處理
		pD[0] = min(max(0, pS[0]), 255);
		pD[1] = min(max(0, pS[1]), 255);
		pD[2] = min(max(0, pS[2]), 255);
		pD[3] = min(max(0, pS[3]), 255);
		// next
		pS += 4;
		pD += 4;
	}
}

// 用位掩碼做飽和處理.用求負生成掩碼
void f2_neg(BYTE* pbufD, const signed short* pbufS, int cnt)
{
	const signed short* pS = pbufS;
	BYTE* pD = pbufD;
	int i;
	for(i=0; i<cnt; ++i)
	{
		// 分別對4個通道做飽和處理
		pD[0] = LIMITSU_BYTE(pS[0]);
		pD[1] = LIMITSU_BYTE(pS[1]);
		pD[2] = LIMITSU_BYTE(pS[2]);
		pD[3] = LIMITSU_BYTE(pS[3]);
		// next
		pS += 4;
		pD += 4;
	}
}

// 用位掩碼做飽和處理.用帶符號右移生成掩碼
void f3_sar(BYTE* pbufD, const signed short* pbufS, int cnt)
{
	const signed short* pS = pbufS;
	BYTE* pD = pbufD;
	int i;
	for(i=0; i<cnt; ++i)
	{
		// 分別對4個通道做飽和處理
		pD[0] = LIMITSW_BYTE(pS[0]);
		pD[1] = LIMITSW_BYTE(pS[1]);
		pD[2] = LIMITSW_BYTE(pS[2]);
		pD[3] = LIMITSW_BYTE(pS[3]);
		// next
		pS += 4;
		pD += 4;
	}
}

// 進行測試
void runTest(char* szname, TESTPROC proc)
{
	int i,j;
	DWORD	tm0, tm1;	// 存儲時間
	for(i=1; i<=3; ++i)	// 多次測試
	{
		//tm0 = GetTickCount();
		tm0 = timeGetTime();
		// main
		for(j=1; j<=4000; ++j)	// 重複運算幾次延長時間,避免計時精度問題
		{
			proc(bufD, bufS, DATASIZE);
		}
		// show
		//tm1 = GetTickCount() - tm0;
		tm1 = timeGetTime() - tm0;
		printf("%s[%d]:\t%u\n", szname, i, tm1);

	}
}

int main(int argc, char* argv[])
{
	int i;	// 循環變量

	//printf("Hello World!\n");
	printf("== noif:VC6 ==");

	// 初始化
	srand( (unsigned)time( NULL ) );
	for(i=0; i<DATASIZE*4; ++i)
	{
		bufS[i] = (signed short)((rand()&0x1FF) - 128);	// 使數值在 [-128, 383] 區間
	}

	// 準備開始。可以將將進程優先級設爲實時
	if (argc<=1)
	{
		printf("<Press any key to continue>");
		getch();
		printf("\n");
	}

	// 進行測試
	runTest("f0_if", f0_if);
	runTest("f1_min", f1_min);
	runTest("f2_neg", f2_neg);
	runTest("f3_sar", f3_sar);

	// 結束前提示
	if (argc<=1)
	{
		printf("<Press any key to exit>");
		getch();
		printf("\n");
	}

	return 0;
}



五、測試結果

  將程序編譯爲“Release”版,然後分別在不同的系統環境中進行測試。

  在32位winXP上的測試結果——

== noif:VC6 ==<Press any key to continue>
f0_if[1]:       2016
f0_if[2]:       2016
f0_if[3]:       2015
f1_min[1]:      2063
f1_min[2]:      2062
f1_min[3]:      2063
f2_neg[1]:      718
f2_neg[2]:      719
f2_neg[3]:      719
f3_sar[1]:      672
f3_sar[2]:      687
f3_sar[3]:      672


  在64位win7上的測試結果——

== noif:VC6 ==<Press any key to continue>
f0_if[1]:       2075
f0_if[2]:       2012
f0_if[3]:       2028
f1_min[1]:      2059
f1_min[2]:      2075
f1_min[3]:      2075
f2_neg[1]:      717
f2_neg[2]:      718
f2_neg[3]:      718
f3_sar[1]:      670
f3_sar[2]:      687
f3_sar[3]:      686


  硬件環境——
CPU:Intel Core i3-2310M, 2100 MHz
內存:DDR3-1066


源碼下載——
http://files.cnblogs.com/zyl910/noifVC6.rar
(建議閱讀編譯器生成的彙編代碼,位於Release\noifVC6.asm )

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