一、MMX技术简介
一、MMX技術(shù)簡介
Intel 公司的MMX(多媒體增強(qiáng)指令集)技術(shù)可以大大提高應(yīng)用程序?qū)ΧS三維圖形和圖象的處理能力。Intel MMX技術(shù)可用于對(duì)大量數(shù)據(jù)和復(fù)雜數(shù)組進(jìn)行的復(fù)雜處理,使用MMX技術(shù)可處理的數(shù)據(jù)基本單位可以是字節(jié)(byte)、字(word),或者是雙字(double-word)。
Visual Studio .NET 2003提供了對(duì)MMX指令集特性的支持,從而可以不必編寫匯編代碼,直接使用C++代碼就可以實(shí)現(xiàn)MMX指令的功能。通過參考Intel軟件說明書(Intel Software manuals)[1]以及閱讀MSDN中有關(guān)MMX編程技術(shù)的主題會(huì)使你更好地把握MMX編程的要點(diǎn)。
MMX技術(shù)實(shí)現(xiàn)了單道指令多道數(shù)據(jù)流(SIMD,single-instruction, multiple-data)的執(zhí)行模式。考慮下面一個(gè)需要編程完成的任務(wù),在一個(gè)字節(jié)(BYTE)數(shù)組中使其中每一個(gè)元素加上一個(gè)數(shù),在傳統(tǒng)的程序中,實(shí)現(xiàn)這個(gè)功能的算法如下:
for each b in array // 對(duì)數(shù)組中的每一個(gè)元素b{
??b = b + n; // 加上一個(gè)數(shù)n
}
下面看看它的實(shí)現(xiàn)細(xì)節(jié):
for each b in array // 對(duì)數(shù)組中的每一個(gè)元素b{
??把b加載到寄存器中
??把此寄存器中的數(shù)加上n
??把所得寄存器中的結(jié)果放回內(nèi)存
}
具有MMX指令集支持的處理器有八個(gè)64位的寄存器,每一個(gè)寄存器可以存放8個(gè)字節(jié)(byte)、4個(gè)字(word)或2個(gè)雙字(double-word)。MMX技術(shù)同時(shí)提供了一個(gè)MMX指令集,其中的指令可以可以把一個(gè)數(shù)值(其類型可以是字節(jié)、字或雙字)加載到這些MMX寄存器中,在寄存器中進(jìn)行算術(shù)或邏輯運(yùn)算,然后把寄存器中的結(jié)果放回內(nèi)存存儲(chǔ)單元。上面的例子采用MMX技術(shù)后的算法是這樣的:
for each 8 members in array //把數(shù)組中的8個(gè)字節(jié)(其中一個(gè)字節(jié)為數(shù)組中的一個(gè)單位)作為一組取出{
??把這8個(gè)字節(jié)加載到MMX寄存器中
??通過一個(gè)CPU指令執(zhí)行周期把這個(gè)寄存器中的8個(gè)字節(jié)都加上n
??把寄存器中計(jì)算的結(jié)果寫回內(nèi)存
}
C++編程人員不必直接使用MMX指令集中的指令訪問這些MMX寄存器。你可以使用64位的數(shù)據(jù)類型__m64和一系列C++函數(shù)來進(jìn)行相關(guān)的算術(shù)和邏輯運(yùn)算。而決定程序使用哪個(gè)MMX寄存器以及代碼優(yōu)化是C++編譯器的任務(wù)。
Visual C++ MMXSwarm [4]是MSDN中提供的一個(gè)很好的使用MMX技術(shù)進(jìn)行圖象處理的例子,它包含了一些封裝好了的類簡化了使用MMX技術(shù)的操作,并向你展示了對(duì)各種不同格式圖象進(jìn)行處理的操作(如單色24位象素RGB、32位象素RGB等)。本文只是對(duì)使用Visual C++實(shí)現(xiàn)MMX程序設(shè)計(jì)的簡單介紹。如果你感興趣的話,可以參看MSDN上MMXSwarm的例子。
二、MMX程序設(shè)計(jì)詳細(xì)介紹
1.包含的頭文件
所有的MMX指令集函數(shù)在emmintrin.h文件中定義:
#include因?yàn)槌绦蛑杏玫降腗MX處理器指令是由編譯器決定,所以它并沒有相關(guān)的.lib庫文件。
2.__m64 數(shù)據(jù)類型
這種類型的變量可用作MMX指令的操作數(shù),它不能被直接訪問。_m64類型的變量被自動(dòng)分配為8個(gè)字節(jié)的字長。
3.CPU對(duì)MMX指令集的支持
如果你的CPU能夠具有了MMX指令集,你就可以使用Visual Studio .NET 2003提供的對(duì)MMX指令集支持的C++函數(shù)庫了,你可以查看MSDN中的一個(gè)Visual C++ CPUID[3]的例子,它可以幫你檢測(cè)你的CPU是否支持SSE、MMX指令集或其它的CPU功能。
4.飽和算法(Saturation Arithmetic)和封裝模式(Wraparound Mode)
MMX技術(shù)支持一種叫做saturating arithmetic(飽和算法)的計(jì)算模式。在飽和模式下,當(dāng)計(jì)算結(jié)果發(fā)生溢出(上溢或下溢)時(shí),CPU會(huì)自動(dòng)去掉溢出的部分,使計(jì)算結(jié)果取該數(shù)據(jù)類型表示數(shù)值的上限值(如果上溢)或下限值(如果下溢)。飽和模式的計(jì)算用于對(duì)圖象的處理。
下面的例子能夠讓你理解飽和模式和封裝模式的區(qū)別。如果一個(gè)字節(jié)(BYTE)類型變量的值為255,然后將其值加一。在封裝模式下,相加結(jié)果為0(去掉進(jìn)位);在飽和模式下,結(jié)果為255。飽和模式用類似的方法來處理下溢出,比如對(duì)于一個(gè)字節(jié)數(shù)據(jù)類型的數(shù)在飽和模式下,1減2的結(jié)果為0(而不是-1)。每一個(gè)MMX算術(shù)指令都有這兩種模式:飽和模式和封裝模式。本文所要討論的項(xiàng)目只使用飽和模式下的MMX指令。
三、MMX8 演示項(xiàng)目
MMX8是一個(gè)單文檔界面(SDI)的應(yīng)用程序,用來對(duì)每象素8位的單色位圖進(jìn)行簡單處理。源圖象和處理后的圖象會(huì)在窗體中顯示出來。新建的ATL(活動(dòng)模版庫)類 Cimage用來從資源中提取圖象并在窗體中顯示出來。程序要對(duì)圖象進(jìn)行兩種處理操作:圖象顏色反相和改變圖象的亮度。每一種處理操作可以用下面幾種方法之中其中的一種來實(shí)現(xiàn):
- 純C++代碼; 
 
- 使用C++的MMX功能函數(shù)的代碼; 
 
- 使用MMX匯編指令的代碼。
對(duì)圖象進(jìn)行處理計(jì)算的時(shí)間會(huì)顯示在狀態(tài)欄中。
用純C++實(shí)現(xiàn)的圖象顏色反相函數(shù):
void CImg8Operations::InvertImageCPlusPlus(BYTE* pSource, BYTE* pDest, int nNumberOfPixels){
??for ( int i = 0; i < nNumberOfPixels; i++ )
??{
????*pDest++ = 255 - *pSource++;
?? }
}
為了查詢使用C++ MMX指令函數(shù)的方法,需要參考Intel軟件說明書(Intel Software manuals)中有關(guān)MMX匯編指令的說明,首先我是在第一卷的第八章找到了MMX相關(guān)指令的大體介紹,然后在第二卷找到了有關(guān)這些MMX指令的詳細(xì)說明,這些說明有一部分涉及了與其特性相關(guān)的C++函數(shù)。然后我通過這些MMX指令對(duì)應(yīng)的C++函數(shù)查找了MSDN中與其相關(guān)的說明。在MMX8示例程序中用到的MMX指令和相關(guān)的C++函數(shù)見下表:
| 實(shí)現(xiàn)的功能 | 對(duì)應(yīng)的MMX匯編指令 | Visual C++.NET中的MMX函數(shù) | 
| 清除MMX寄存器中的內(nèi)容,即初始化(以避免和浮點(diǎn)數(shù)操作發(fā)生沖突) | emms | _mm_empty | 
| 將兩個(gè)64位數(shù)中對(duì)應(yīng)的(8個(gè))無符號(hào)(8位)字節(jié)同時(shí)進(jìn)行減法操作 | psubusb | _mm_subs_pu8 | 
| 將兩個(gè)64位數(shù)中對(duì)應(yīng)的(8個(gè))無符號(hào)(8位)字節(jié)同時(shí)進(jìn)行加法操作 | paddusb | _mm_adds_pu8 | 
用Visual C++.NET的MMX指令函數(shù)實(shí)現(xiàn)圖象顏色反相的函數(shù):
void CImg8Operations::InvertImageC_MMX(BYTE* pSource, BYTE* pDest, int nNumberOfPixels){
??__int64 i = 0;
??i = ~i; // 0xffffffffffffffff
??int nLoop = nNumberOfPixels/8;??// 每次循環(huán)處理8個(gè)象素
??__m64* pIn = (__m64*) pSource;??// 輸入的字節(jié)數(shù)組指針
??__m64* pOut = (__m64*) pDest;??// 輸出的字節(jié)數(shù)組指針
??__m64 tmp;????// 臨時(shí)工作變量
??_mm_empty();??// 執(zhí)行MMX指令:emms,初始化MMX寄存器
??__m64 n1 = Get_m64(i);
??for ( int i = 0; i < nLoop; i++ )
??{
????tmp = _mm_subs_pu8 (n1 , *pIn); // 飽和模式下的無符號(hào)減法,對(duì)每一個(gè)字節(jié)執(zhí)行操作:tmp = n1 - *pIn
????*pOut = tmp;
????pIn++; // 取下面的8個(gè)象素點(diǎn)
????pOut++;
?? }
??_mm_empty(); // 執(zhí)行MMX指令:emms,清除MMX寄存器中的內(nèi)容
}
__m64 CImg8Operations::Get_m64(__int64 n)
{
??union __m64__m64
??{
????__m64 m;
????__int64 i;
?? } mi;
??mi.i = n;
??return mi.m;
}
雖然這個(gè)函數(shù)在非常短的時(shí)間就執(zhí)行完成了,但我記錄了這3種方法需要的時(shí)間,以下是在我的計(jì)算機(jī)上運(yùn)行的結(jié)果:
- 純C++代碼 43毫秒 
 
- 使用C++的MMX指令函數(shù)的代碼 26毫秒 
 
- 使用MMX匯編指令的代碼 26毫秒
上面的圖象處理時(shí)間必須在程序Release優(yōu)化編譯后執(zhí)行時(shí)才能體現(xiàn)出很好的效果。
而改變圖象的亮度我采用了最簡單的方法:對(duì)圖象中的每一個(gè)象素的顏色值進(jìn)行加減運(yùn)算。相對(duì)前面的處理函數(shù)而言,這樣的轉(zhuǎn)換函數(shù)有些復(fù)雜,因?yàn)槲覀冃枰烟幚磉^程分成兩種情況,一種是增加象素顏色值,另一種是減少象素顏色值。
用純C++函數(shù)實(shí)現(xiàn)的改變圖象亮度的函數(shù):
void CImg8Operations::ChangeBrightnessCPlusPlus(BYTE* pSource, BYTE* pDest, int nNumberOfPixels, int nChange){
??if ( nChange > 255 )
????nChange = 255;
??else if ( nChange < -255 )
????nChange = -255;
??BYTE b = (BYTE) abs(nChange);
??int i, n;
??if ( nChange > 0 ) //增加象素顏色值
??{
????for ( i = 0; i < nNumberOfPixels; i++ )
????{
??????n = (int)(*pSource++ + b);
??????if ( n > 255 )
????????n = 255;
??????*pDest++ = (BYTE) n;
???? }
?? }
??else //減少象素顏色值
??{
????for ( i = 0; i < nNumberOfPixels; i++ )
????{
??????n = (int)(*pSource++ - b);
??????if ( n < 0 )
????????n = 0;
??????*pDest++ = (BYTE) n;
???? }
?? }
}
用Visual C++.NET的MMX指令函數(shù)實(shí)現(xiàn)的改變圖象亮度函數(shù):
void CImg8Operations::ChangeBrightnessC_MMX(BYTE* pSource, BYTE* pDest, int nNumberOfPixels, int nChange){
??if ( nChange > 255 )
????nChange = 255;
??else if ( nChange < -255 )
????nChange = -255;
??BYTE b = (BYTE) abs(nChange);
??__int64 c = b;
??for ( int i = 1; i <= 7; i++ )
??{
????c = c << 8;
????c |= b;
?? }
??int nNumberOfLoops = nNumberOfPixels / 8; // 在一次循環(huán)中處理8個(gè)象素
??__m64* pIn = (__m64*) pSource;??// 輸入的字節(jié)數(shù)組
??__m64* pOut = (__m64*) pDest;??// 輸出的字節(jié)數(shù)組
??__m64 tmp;????// 臨時(shí)工作變量
??_mm_empty();??// 執(zhí)行MMX指令:emms
??__m64 nChange64 = Get_m64(c);
??if ( nChange > 0 )
??{
????for ( i = 0; i < nNumberOfLoops; i++ )
????{
??????tmp = _mm_adds_pu8(*pIn, nChange64); // 飽和模式下的無符號(hào)加法,對(duì)每一個(gè)字節(jié)執(zhí)行操作:tmp = *pIn + nChange64
??????*pOut = tmp;
??????pIn++; // 取下面8個(gè)象素
??????pOut++;
???? }
?? }
??else
??{
????for ( i = 0; i < nNumberOfLoops; i++ )
????{
??????tmp = _mm_subs_pu8(*pIn, nChange64); // 飽和模式下的無符號(hào)減法,對(duì)每一個(gè)字節(jié)執(zhí)行操作:tmp = *pIn - nChange64
??????*pOut = tmp;
??????pIn++; // 取下面8個(gè)象素
??????pOut++;
???? }
?? }
??_mm_empty(); // 執(zhí)行MMX指令:emms
}
注意參數(shù)nChange的符號(hào)每次調(diào)用函數(shù)時(shí)在循環(huán)體外只檢查一次,而不是放在循環(huán)體內(nèi),那樣會(huì)被檢查成千上萬次。下面是在我的計(jì)算機(jī)上處理圖象花費(fèi)的時(shí)間:
- 純C++代碼 49毫秒 
 
- 使用C++的MMX指令函數(shù)的代碼 26毫秒 
 
- 使用MMX匯編指令的代碼 26毫秒
四、MMX32 演示項(xiàng)目
MMX32項(xiàng)目可對(duì)32位象素的RGB圖象進(jìn)行處理。進(jìn)行的圖象處理工作是圖象顏色反相操作和更改圖象顏色的平衡度(將象素點(diǎn)的每一種顏色乘以一定的值)操作。
MMX的乘法實(shí)現(xiàn)起來比加減法復(fù)雜得多,因?yàn)槌朔ㄟ\(yùn)算通常得出的結(jié)果的位數(shù)不再是以前位數(shù)的大小。比如,如果乘法的操作數(shù)有一個(gè)字節(jié)(8位的BYTE)大小,那么結(jié)果會(huì)達(dá)到一個(gè)字(16位的WORD)大小。這需要額外的轉(zhuǎn)換,并且使用MMX匯編指令和C++代碼進(jìn)行圖象轉(zhuǎn)換花費(fèi)時(shí)間的差別不是很大(時(shí)間差為5-10%)。
用Visual C++.NET的MMX指令函數(shù)實(shí)現(xiàn)的更改圖象顏色平衡度的函數(shù):
void CImg32Operations::ColorsC_MMX(BYTE* pSource, BYTE* pDest, int nNumberOfPixels, float fRedCoefficient, float fGreenCoefficient, float fBlueCoefficient){
??int nRed = (int)(fRedCoefficient * 256.0f);
??int nGreen = (int)(fGreenCoefficient * 256.0f);
??int nBlue = (int)(fBlueCoefficient * 256.0f);
??// 設(shè)置相乘系數(shù)
??__int64 c = 0;
??c = nRed;
??c = c << 16;
??c |= nGreen;
??c = c << 16;
??c |= nBlue;
??__m64 nNull = _m_from_int(0);??// null
??__m64 tmp = _m_from_int(0);????// 臨時(shí)工作臨時(shí)變量初始化
??_mm_empty(); // 清空MMX寄存器。
??__m64 nCoeff = Get_m64(c);
??DWORD* pIn = (DWORD*) pSource;??// 輸入雙字?jǐn)?shù)組
??DWORD* pOut = (DWORD*) pDest;??// 輸出雙字?jǐn)?shù)組
??for ( int i = 0; i < nNumberOfPixels; i++ )
??{
????tmp = _m_from_int(*pIn); // tmp = *pIn (在tmp的低32位寫入數(shù)據(jù))
????tmp = _mm_unpacklo_pi8(tmp, nNull ); //將tmp中低位的4個(gè)字節(jié)轉(zhuǎn)化為字,字的高位用nNull中對(duì)應(yīng)位上的位值填充。
????tmp = _mm_mullo_pi16 (tmp , nCoeff); //將tmp中的每一個(gè)字相乘,將相乘結(jié)果的高位送到nCoeff,在tmp中只保留每個(gè)結(jié)果的低位。
????tmp = _mm_srli_pi16 (tmp , 8); // 將tmp中的每一個(gè)字右移8位,相當(dāng)于除以256
????tmp = _mm_packs_pu16 (tmp, nNull); // 使用飽和模式將tmp中的結(jié)果做如下處理:將tmp中的4個(gè)字轉(zhuǎn)化為4個(gè)字節(jié),并將這4個(gè)字節(jié)寫到tmp中的低32位中。同時(shí),將nNull中的4個(gè)字轉(zhuǎn)化為4個(gè)字節(jié),并將這4個(gè)字節(jié)寫到tmp的高32位中。
????*pOut = _m_to_int(tmp); // *pOut = tmp (將tmp低32位的數(shù)據(jù)放入pOut數(shù)組中)
????pIn++;
????pOut++;
?? }
??_mm_empty();
}
你可以參看示例項(xiàng)目的源代碼了解有關(guān)此項(xiàng)目的更多的細(xì)節(jié)。
五、SSE2 技術(shù)
SSE2技術(shù)包含有一個(gè)類似MMX中對(duì)整數(shù)操作的指令集,同時(shí)也包含128位的SSE寄存器組。比如,用SSE2技術(shù)實(shí)現(xiàn)更改圖象顏色平衡度能夠比用純C++代碼實(shí)現(xiàn)此功能在效率上有很大提升。SSE2同時(shí)是SSE技術(shù)的擴(kuò)展,比如它不僅可以單精度浮點(diǎn)數(shù)數(shù)組,而且能夠處理雙精度浮點(diǎn)數(shù)數(shù)據(jù)類型的數(shù)組。用C++實(shí)現(xiàn)的MMXSwarm 示例項(xiàng)目不僅使用了MMX指令函數(shù),而且使用了SSE2指令對(duì)整型數(shù)操作的函數(shù)。
六、參考文檔
原文作者:Alex Farber
原文出處:http://www.codeproject.com/cpp/mmxintro.asp
編程實(shí)例:http://www.codeproject.com/cpp/mmxintro/MMX_src.zip
譯文出處:http://blog.csdn.net/hifrog/archive/2004/02/01/21644.aspx
格式整理:http://yonsm.reg365.com/index.php?job=art&articleid=a_20041008_204042
總結(jié)
 
                            
                        