基於MMX指令集的程序設計簡介 - 中國WEB開發者網絡 (http://www.webasp.net) -- 技術教程 (http://www.webasp.net/article/) --- 基於MMX指令集的程序設計簡介 (http://www.webasp.net/article/9/8388.htm) |
| -- 作者:未知 -- 發佈日期: 2004-03-23 |
| MMX技術簡介
Intel 公司的MMX™(多媒體增強指令集)技術可以大大提高應用程序對二維三維圖形和圖像的處理能力。Intel MMX技術可用於對大量數據和複雜數組進行的複雜處理,使用MMX技術可處理的數據基本單位可以是字節(byte)、字(word),或者是雙字(double-word)。 Visual Studio .NET 2003提供了對MMX指令集特性的支持,從而可以不必編寫彙編代碼,直接使用C++代碼就可以實現MMX指令的功能。通過參考Intel軟件說明書(Intel Software manuals)[1]以及閱讀MSDN中有關MMX編程技術的主題會使你更好地把握MMX編程的要點。 MMX技術實現了單道指令多道數據流(SIMD,single-instruction, multiple-data)的執行模式。考慮下面一個需要編程完成的任務,在一個字節(BYTE)數組中使其中每一個元素加上一個數,在傳統的程序中,實現這個功能的算法如下: for each b in array //對數組中的每一個元素b b = b + n //加上一個數n 下面看看它的實現細節: for each b in array //對數組中的每一個元素b { 把b加載到寄存器中 把此寄存器中的數加上n 把所得寄存器中的結果放回內存 } 具有MMX指令集支持的處理器有八個64位的寄存器,每一個寄存器可以存放8個字節(byte)、4個字(word)或2個雙字(double-word)。MMX技術同時提供了一個MMX指令集,其中的指令可以可以把一個數值(其類型可以是字節、字或雙字)加載到這些MMX寄存器中,在寄存器中進行算術或邏輯運算,然後把寄存器中的結果放回內存存儲單元。上面的例子採用MMX技術後的算法是這樣的: for each 8 members in array //把數組中的8個字節(其中一個字節為數組中的一個單位)作為一組取出 { 把這8個字節加載到MMX寄存器中 通過一個CPU指令執行週期把這個寄存器中的8個字節都加上n 把寄存器中計算的結果寫回內存 } C++編程人員不必直接使用MMX指令集中的指令訪問這些MMX寄存器。你可以使用64位的數據類型__m64和一系列C++函數來進行相關的算術和邏輯運算。而決定程序使用哪個MMX寄存器以及代碼優化是C++編譯器的任務。 Visual C++ MMXSwarm [4]是MSDN中提供的一個很好的使用MMX技術進行圖像處理的例子,它包含了一些封裝好了的類簡化了使用MMX技術的操作,並向你展示了對各種不同格式圖像進行處理的操作(如單色24位象素RGB、32位象素RGB等)。本文只是對使用Visual C++實現MMX程序設計的簡單介紹。如果你感興趣的話,可以參看MSDN上MMXSwarm的例子。 MMX程序設計詳細介紹 包含的頭文件 所有的MMX指令集函數在emmintrin.h文件中定義: #include <emmintrin.h> 因為程序中用到的MMX處理器指令是由編譯器決定,所以它並沒有相關的.lib庫文件。 __m64 數據類型 這種類型的變量可用作MMX指令的操作數,它不能被直接訪問。_m64類型的變量被自動分配為8個字節的字長。 CPU對MMX指令集的支持 如果你的CPU能夠具有了MMX指令集,你就可以使用Visual Studio .NET 2003提供的對MMX指令集支持的C++函數庫了,你可以查看MSDN中的一個Visual C++ CPUID[3]的例子,它可以幫你檢測你的CPU是否支持SSE、MMX指令集或其它的CPU功能。 飽和算法(Saturation Arithmetic)和封裝模式(Wraparound Mode) MMX技術支持一種叫做saturating arithmetic(飽和算法)的計算模式。在飽和模式下,當計算結果發生溢出(上溢或下溢)時,CPU會自動去掉溢出的部分,使計算結果取該數據類型表示數值的上限值(如果上溢)或下限值(如果下溢)。飽和模式的計算用於對圖像的處理。 下面的例子能夠讓你理解飽和模式和封裝模式的區別。如果一個字節(BYTE)類型變量的值為255,然後將其值加一。在封裝模式下,相加結果為0(去掉進位);在飽和模式下,結果為255。飽和模式用類似的方法來處理下溢出,比如對於一個字節數據類型的數在飽和模式下,1減2的結果為0(而不是-1)。每一個MMX算術指令都有這兩種模式:飽和模式和封裝模式。本文所要討論的項目只使用飽和模式下的MMX指令。 編程實例 以下講解了MMX技術在Visual Studio .NET 2003下的應用實例,你可以在http://www.codeproject.com/cpp/mmxintro/MMX_src.zip下載示例程序壓縮包。該壓縮包中含有兩個項目,這兩個項目是基於微軟基本類庫(MFC)建立的Visual C++.NET項目,你也可以按照下面的講解建立這兩個項目。 MMX8 演示項目 MMX8是一個單文檔界面(SDI)的應用程序,用來對每象素8位的單色位圖進行簡單處理。源圖像和處理後的圖像會在窗體中顯示出來。新建的ATL(活動模版庫)類 Cimage用來從資源中提取圖像並在窗體中顯示出來。程序要對圖像進行兩種處理操作:圖像顏色反相和改變圖像的亮度。每一種處理操作可以用下面幾種方法之中其中的一種來實現: 純C++代碼; 使用C++的MMX功能函數的代碼; 使用MMX彙編指令的代碼。 對圖像進行處理計算的時間會顯示在狀態欄中。 用純C++實現的圖像顏色反相函數: void CImg8Operations::InvertImageCPlusPlus( BYTE* pSource, BYTE* pDest, int nNumberOfPixels) { for ( int i = 0; i < nNumberOfPixels; i++ ) { *pDest++ = 255 - *pSource++; } } 為了查詢使用C++ MMX指令函數的方法,需要參考Intel軟件說明書(Intel Software manuals)中有關MMX彙編指令的說明,首先我是在第一卷的第八章找到了MMX相關指令的大體介紹,然後在第二卷找到了有關這些MMX指令的詳細說明,這些說明有一部分涉及了與其特性相關的C++函數。然後我通過這些MMX指令對應的C++函數查找了MSDN中與其相關的說明。在MMX8示例程序中用到的MMX指令和相關的C++函數見下表: 實現的功能 對應的MMX彙編指令 Visual C++.NET中的MMX函數 清除MMX寄存器中的內容,即初始化(以避免和浮點數操作發生衝突)。 emms _mm_empty 將兩個64位數中對應的(8個)無符號(8位)字節同時進行減法操作。 psubusb _mm_subs_pu8 將兩個64位數中對應的(8個)無符號(8位)字節同時進行加法操作。 paddusb _mm_adds_pu8 用Visual C++.NET的MMX指令函數實現圖像顏色反相的函數: void CImg8Operations::InvertImageC_MMX( BYTE* pSource, BYTE* pDest, int nNumberOfPixels) { __int64 i = 0; i = ~i; // 0xffffffffffffffff // 每次循環處理8個像素 int nLoop = nNumberOfPixels/8; __m64* pIn = (__m64*) pSource; // 輸入的字節數組指針 __m64* pOut = (__m64*) pDest; // 輸出的字節數組指針 __m64 tmp; // 臨時工作變量 _mm_empty(); // 執行MMX指令:emms,初始化MMX寄存器 __m64 n1 = Get_m64(i); for ( int i = 0; i < nLoop; i++ ) { tmp = _mm_subs_pu8 (n1 , *pIn); // 飽和模式下的無符號減法 //對每一個字節執行操作:tmp = n1 - *pIn *pOut = tmp; pIn++; // 取下面的8個像素點 pOut++; } _mm_empty(); // 執行MMX指令:emms,清除MMX寄存器中的內容 } __m64 CImg8Operations::Get_m64(__int64 n) { union __m64__m64 { __m64 m; __int64 i; } mi; mi.i = n; return mi.m; } 雖然這個函數在非常短的時間就執行完成了,但我記錄了這3種方法需要的時間,以下是在我的計算機上運行的結果: 純C++代碼 43毫秒 使用C++的MMX指令函數的代碼 26毫秒 使用MMX彙編指令的代碼 26毫秒 上面的圖像處理時間必須在程序Release優化編譯後執行時才能體現出很好的效果。 而改變圖像的亮度我採用了最簡單的方法:對圖像中的每一個象素的顏色值進行加減運算。相對前面的處理函數而言,這樣的轉換函數有些複雜,因為我們需要把處理過程分成兩種情況,一種是增加象素顏色值,另一種是減少象素顏色值。 用純C++函數實現的改變圖像亮度的函數: 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指令函數實現的改變圖像亮度函數: 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; } // 在一次循環中處理8個像素 int nNumberOfLoops = nNumberOfPixels / 8; __m64* pIn = (__m64*) pSource; // 輸入的字節數組 __m64* pOut = (__m64*) pDest; // 輸出的字節數組 __m64 tmp; // 臨時工作變量 _mm_empty(); // 執行MMX指令:emms __m64 nChange64 = Get_m64(c); if ( nChange > 0 ) { for ( i = 0; i < nNumberOfLoops; i++ ) { tmp = _mm_adds_pu8(*pIn, nChange64); // 飽和模式下的無符號加法 // 對每一個字節執行操作:tmp = *pIn + nChange64 *pOut = tmp; pIn++; // 取下面8個像素 pOut++; } } else { for ( i = 0; i < nNumberOfLoops; i++ ) { tmp = _mm_subs_pu8(*pIn, nChange64); // 飽和模式下的無符號減法 // 對每一個字節執行操作:tmp = *pIn - nChange64 *pOut = tmp; pIn++; //取下面8個像素 pOut++; } } _mm_empty(); // 執行MMX指令:emms } 注意參數nChange的符號每次調用函數時在循環體外只檢查一次,而不是放在循環體內,那樣會被檢查成千上萬次。下面是在我的計算機上處理圖像花費的時間: 純C++代碼 49毫秒 使用C++的MMX指令函數的代碼 26毫秒 使用MMX彙編指令的代碼 26毫秒 MMX32 演示項目 MMX32項目可對32位象素的RGB圖像進行處理。進行的圖像處理工作是圖像顏色反相操作和更改圖像顏色的平衡度(將象素點的每一種顏色乘以一定的值)操作。 MMX的乘法實現起來比加減法複雜得多,因為乘法運算通常得出的結果的位數不再是以前位數的大小。比如,如果乘法的操作數有一個字節(8位的BYTE)大小,那麼結果會達到一個字(16位的WORD)大小。這需要額外的轉換,並且使用MMX彙編指令和C++代碼進行圖像轉換花費時間的差別不是很大(時間差為5-10%)。 用Visual C++.NET的MMX指令函數實現的更改圖像顏色平衡度的函數: 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); // 設置相乘係數 __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); // 臨時工作臨時變量初始化 _mm_empty(); // 清空MMX寄存器。 __m64 nCoeff = Get_m64(c); DWORD* pIn = (DWORD*) pSource; // 輸入雙字數組 DWORD* pOut = (DWORD*) pDest; // 輸出雙字數組 for ( int i = 0; i < nNumberOfPixels; i++ ) { tmp = _m_from_int(*pIn); // tmp = *pIn (在tmp的低32位寫入數據) tmp = _mm_unpacklo_pi8(tmp, nNull ); //將tmp中低位的4個字節轉化為字 //字的高位用nNull中對應位上的位值填充。 tmp = _mm_mullo_pi16 (tmp , nCoeff); //將tmp中的每一個字相乘,將相乘結果的高位送到nCoeff,在tmp中只保留每個結果的低位。 tmp = _mm_srli_pi16 (tmp , 8); // 將tmp中的每一個字右移8位,相當於除以256 tmp = _mm_packs_pu16 (tmp, nNull); // 使用飽和模式將tmp中的結果做如下處理: //將tmp中的4個字轉化為4個字節,並將這4個字節寫到tmp中的低32位中 // 同時,將nNull中的4個字轉化為4個字節,並將這4個字節寫到tmp的高32位中。 *pOut = _m_to_int(tmp); // *pOut = tmp (將tmp低32位的數據放入pOut數組中) pIn++; pOut++; } _mm_empty(); } 你可以參看示例項目的源代碼瞭解有關此項目的更多的細節。 SSE2 技術 SSE2技術包含有一個類似MMX中對整數操作的指令集,同時也包含128位的SSE寄存器組。比如,用SSE2技術實現更改圖像顏色平衡度能夠比用純C++代碼實現此功能在效率上有很大提升。SSE2同時是SSE技術的擴展,比如它不僅可以單精度浮點數數組,而且能夠處理雙精度浮點數數據類型的數組。用C++實現的MMXSwarm 示例項目不僅使用了MMX指令函數,而且使用了SSE2指令對整型數操作的函數。 參考文檔: [1] Intel軟件說明書(Intel Software manuals):http://developer.intel.com/design/archives/processors/mmx/index.htm 。 [2] MSDN中有關MMX技術的主題:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclang/html/vcrefsupportformmxtechnology.asp。 [3] Microsoft Visual C++ CPUID項目示例:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vcsample/html/vcsamcpuiddeterminecpucapabilities.asp。 [4] Microsoft Visual C++ MMXSwarm項目示例: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vcsample/html/vcsamMMXSwarmSampleDemonstratesCImageVisualCsMMXSupport.asp。 |
| webasp.net |