Tuesday, November 29, 2005

對Intel (R) Pentium 4處理器,開始使用SSE/SSE2


當軟體的演算法最佳化已經達到了一定的程度,要再向上攀升,已難如登天,我們開始考慮藉重硬體來幫忙提昇效能。

對Intel (R) Pentium 4處理器,開始使用SSE/SSE2(Getting Started with SSE/SSE2 for Intel (R) Pentium 4 Processor)



實行概要


本 文教導程式員如何開始使用Streaming SIMD Extensions(SSE)和Streaming SIMD Extensions 2(SSE2)指令集,它們對Intel (R) 4的處理器有效用。在本文中,我們藉由提出環境需求及使用這種編程技巧的例子,來介紹這些技術。

簡介


Intel (R) Pentium 4處理器根據Intel (R) NetBurst TM 微架構(microarchitecture)。此架構從它的前一代(predecessors)提供新的強化,包括對SIMD(Single Instruction Multiple Data)執行技術的改善。SIMD在具MMX技術的Pentium處理器首次被介紹,在Pentium III處理器家族擴充成包含更多在Streaming SIMD Extensions (SSE)的資料集,而今天Pentium 4處理器則以Streaming SIMD Extensions (SSE) 包含了更多(的資料集)。

為了Interl NetBurst微架構發展的主要特性是從Pentium III處理器的P6微架構找到的指令集的擴充,以引入在雙精度浮點數資料元素上的運算。此架構以它平行處理更多運算的能力,支援更多的資料,且更有效率。

本文件的目標在於提供給不識此道者一堂SSE和SSE2的「速成課(crash course)」,讓你能快速地到達能開始在你的應用程式中實作SSE程式碼的水準。

SIMD


Single Instruction Multiple Data 或SIMD 執行是以單一指令平行地執行多重運算的概念。這項技術,被MMX技術介紹,允許SIMD運算使用64 bit的MMX暫存器來執行。

SSE2 F1
(圖1)

SSE


Streaming SIMD Extensions在給Pentium III處理器的P6微架構中被介紹,擴展了MMX技術,且允許同時對四個packed的單精度浮點數的資料元素執行SIMD計算。這些計算是一次同時執行128 bits。

SSE2


Streaming SIMD Extensions 2在Intel NetBurst 架構中被介紹。SSE2允許以平行來執行更多的運算,而延伸這些於MMX及SSE中的指令。注意!SSE2介紹SIMD的計算在二個雙精度浮點數的資料元素。

開始囉


硬體/軟體需求


選項一(建議需求):這是為了在現在及未來執得到行SSE或SSE2最佳運算效能所建議的硬體和軟體組態設定。當使用SSE及SSE2時,推薦Intel (R) C++編譯器。

Intel (R) Pentium 4 Processor
Microsoft* Windows*2000
Microsoft Visual Studio* C++ 6.0
Intel (R) C++ Compiler 5.0.1 (或更新版本)

選項二(最小需求):此硬體和軟體組態設定也是可行的,且使用有Microsoft Processor Pack的Microsoft編譯器來編譯SSE和SSE.此發展環境包含下列的元件:

Intel (R) Pentium 4 Processor
Microsoft* Windows*2000
Microsoft Visual Studio* C++ 6.0
Microsoft Processor Pack for Visual Studio C++ 6.0

jiing的則是:
Intel (R) Pentium 4 Processor
Microsoft* Windows* XP
Microsoft Visual Studio .NET 2003 C++ 6.0
Intel (R) C++ Compiler 8.1.025 (感謝Jedi

環境設定


為了設定你目前的發展環境以便能夠撰寫並編譯SSE程式碼,你會需要使用下列的步驟來設定環境:(照我的做法寫)
1. 安裝Microsoft Visual Studio .NET 2003
2. 執行Microsoft Visual Studio .NET 2003中的Visual C++ .NET
3. 關掉Visual Studio .NET
4. 安裝Intel (R) C++ Compiler 8.1.025
5. 執行Microsoft Visual Studio .NET 2003中的Visual C++ .NET

根據下列步驟設定建置環境(工具/選項/專案/Visual C++)

SSE2 F2
(圖2)為了Intel (R) 編譯器,在你的「可執行檔」目錄中設定“C:\Program Files\Intel\CPP\Compiler80\Ia32\Bin"目錄,如圖所示。

Include檔案:

SSE2 F3
(圖3)為了Intel (R)編譯器設定設定Include目錄,在你的“C:\Program Files\Intel\CPP\Compiler80\Ia32\Include"目錄,如圖所示。

SSE2 F4
(圖4)為了Intel (R)編譯器設定程式庫檔目錄,在你的“C:\Program Files\Intel\CPP\Compiler80\Ia32\Lib"目錄,如圖所示。

從選單中選擇,工具/選項中可以看到Intel (R) C++,點進去可看到下圖:

SSE2 F5
(圖5)

現在你已經將發展平台設定好了。下一節談論準備你的程式碼以備SSE及SSE2.

辨識是否支援SSE/SSE2


原來文件提的方法在我的電腦上不適用,你可在微軟的MSDN中找到一個幫你測試系統CPUID是否支援MMX/SSE/SSE2的程式。
連結在:http://download.microsoft.com/download/VisualStudioNET/Sample/7.0/NT5XP/EN-US/debugging_crt_debug_cpuid.exe

http://msdn.microsoft.com/library/en-us/vcsample/html/vcsamCPUIDDetermineCPUCapabilities.asp

準備好你的資料!


為 了善用SSE和SSE2的優點,你必須對你的原始碼做一些準備。最重要的事是資料被排列(align)成以16-byte為一個邊界 (boundary)。資料用16-byte排列以避免例外發生是重要的。為了產生輸出,一些指令需要排列好的資料。這些指令,如果資料沒被在使用它們前 排列好,程式會在過早中止而沒有任何的輸出。有可能在未排列好的資料上執行一些SSE和SSE2指令,不過要注意到排列成同樣大小的(aligned equivalents)會比較快。有數個方法可用來確保在靜態配置期間資料的排列。

用Intel編譯器在編譯好的檔案中排列資料最簡單 的方法是使用 __declspec(a1lign(16)) 限定子(specifier)。如果你使用這個方法,你必須重新排列資料結構以確保Intel編譯器能編譯所有Streaming SIMD Extension參考到的資料。如果資料定義是在標頭檔裡,你可能必須使用一個建構函數(construct),例如下面的那一個。對於非結構的 (non-struct),union或class資料型別,你可以在標頭檔裡宣告資料,而不用__declspec(a1lign(16)),然後以 __declspec(a1lign(16))在使用資料的檔案中定義它們,依次地由Intel編譯器編譯。

class cdata{
union{
float xmm_data[400];
#ifdef __ICL
__m128 m128_data[100];
#endif
} ;
……
}

另一個方式是藉由增加一個指標(pointer)到class並且在class建構子(constructor)中強制排列強迫資料被排列為16 bytes。然後class 資料使用pointer來存取,就如同它是以16 bytes排列的,以下列的程式碼舉例說明。

class cdata{
float *m_xmm
float xmm_data[400];
public cdata( ){
m_xmm = (float*) (((unsigned) xmm_data+15)& ~0xf);
}
//譯註:上面那行是在找附近為16倍數的位址(將後面四個bit設成0,就是確保一定是16的倍數)
}

第三個方法是在程式碼中或是相對應的intrinsics函式的未排列版本中使用movups (Move Unaligned Packed Single)。要注意,根據你的應用,這可能會減低你執行期間的效能。

動態地配置的記憶體應該也要被排列。Intel編譯器和伴隨著process pack的Micosoft編譯器,提供了_mm_malloc,可使排列到特定的byte邊界變得可行。它的語法如下:
void * _mm_malloc(int size, int alignment)

也有對應到釋放(free)記憶體的_mm_free(char *p)。新的運算子是被最佳多載化了(overload)的,如:

void * operator new(size_t s) {return _mm_malloc(s,16);}

注意到你也將必須增加新運算子的除錯版本:

void * operator new(size_t s, char *file, int line)
{return _mm_malloc(s,16);}

也 要注意 #pragma pack(num) 或 –Zp[num]編譯器選項的使用,其中num不是16能中斷(disrupt)的16-byte排列。因此,避免在包含Streaming SIMD Extensions的程式碼中,使用邊界排列(boundary alignment)定義的這二個型式。

部署選項


現在你的資料已經準備好給SSE和SSE,有數個方法來在你的程式中實作平行計算。

對C/C ++程式員而言,實作SSE和/或SSE2最簡單的方法會是使用Intel (R) C/C++ Class Libraries。這些函式庫讓程式員使用C++ classes來實作SIMD指令。這些classes被包含在檔案fvec.h, ivec.h和dvec.h(各別是浮點數,整數和雙精度浮點數(double))。Intel (R) C/C++ 類別函式庫參考了SSE和SSE2 intrinsics。在某些情況下,這個替代方案與其它的實作比較起來會花費稍微長一點的執行時間。
SSE 和SSE2 intrinsics對C++ classes而言是一個替代方案。它們使你能直接地存取對應的指令而不需要程式員手動管理暫存器,且使得編譯器能最佳化指令排程 (instruction scheduling)。SSE intrinsic的定義被包含在xmmintrin.h中,裡頭描述了算數、邏輯、比較、轉換、記憶體和對型別__m128做SIMD浮點數運算的初始 化。SSE2 intrinsics定義被包含在emmintrin.h檔裡。它描述了算數、邏輯、比較、轉換、記憶體和各別對型別__m128d和__m128i做 SIMD雙精度浮點數和整數運算的初始化。

可能可以獲得最多執行效能的方法是在組合語言的層次上使用SSE和SSE2指令。SSE和 SSE2組合語言指令對64-bit資料處理使用八個64-bit MMX暫存器(即,MM0-MM7),而對128-bit資料使用八個128-bit XMM暫存器(即,XMM0-XMM7)。使用這個方法,為了執行,程式員必須管理暫存器及指令執行的次序。

在下一節中,這些編程方法中的每一個都有例子。

使用SSE/SSE2的程式碼片段


下面是使用各種SSE和SSE2編程技巧的例子。每個例子開始時以等價的C++寫作,接著以等價的SSE/SSE2。第一個例子秀出使用SSE2指令集的各種方法。第二個例子告訴你如何使用SSE指令集。

範例1:簡單的乘法例子,它一次執行多個整數乘法


譯註:這份原始文件的程式有很多錯的地方,大都已更正如下,下面是在Visual Studio .NET中的程式碼,對了,記得要在專案上按右鍵,把這個專案轉換為Intel (R) C++ Project System。

SSE2 F6
(圖6)
範例1:簡單的乘法例子,它一次執行多個整數乘法
實例1-1:
秀一個簡單的應用程式,它取二個32-bit integer陣列,然後將它們相乘並傳回一個long資料型別的結果。
#include “stdafx.h"
#include
#include
int _tmain(int argc, _TCHAR* argv[])
{
long mul;
int t1[100000], t2[100000];

for (int j = 0; j <100000; j++){
t1[j]= rand();;
t2[j]= rand();;
}

for(int i=0 ; i< 100000; i++){
mul = t1[i]*t2[i];
}
getchar();
return 0;
}

實例1-2:
和上面同樣的計算也是取二個32-bit陣列,然後相乘這二個陣列並傳回一個32-bit的結果,不過使用C++ F32vec4 class來寫,為了重載(overload)運算子(operator),此class參考了SSE2 指令集。
#include “stdafx.h"
#include
#include
#include

int _tmain(int argc, _TCHAR* argv[]){
__declspec(align(16)) int t1[100000];
__declspec(align(16)) int t2[100000];
F32vec4 mul;
F32vec4 temp1, temp2;

for (int j = 0; j <100000; j++){
t1[j]= rand();
t2[j]= rand();
}

// 真正的做運算
for (int i = 0; i < 100000; i+=4)
{
temp1 = _mm_cvtepi32_ps( _mm_load_si128( (__m128i*) ( (char*) &t1[i]) ) );
temp2 = _mm_cvtepi32_ps( _mm_load_si128( (__m128i*) ( (char*) &t2[i]) ) );
mul = temp1*temp2;
}
getchar();
return 0;
}
實例1-3:
顯示與上面相同的計算,取二個32-bit的陣列並將這二個陣列相乘後傳回一個64-bit的結果(存在一個128-bit暫存器中),這次用SSE2 intrinsics寫。
#include “stdafx.h"
#include
#include
#include

int _tmain(int argc, _TCHAR* argv[])
{
// 排列資料
__declspec(align(16)) int t1[100000];
__declspec(align(16)) int t2[100000];

// SSE2 整數變數,temp變數是放置4個32-bit integer到一個128-bit XMM 暫存器中
__m128i temp1, temp2;
__m128i mul1, mul2;
__m128i num1, num2;

for (int j = 0; j <100000; j++){
t1[j]= j;
t2[j]= j+1;
} // 設定暫存變數

// 真正的做運算
for (int i = 0; i < 100000; i+=4)
{
// 載入前四個32-bit integer到 128 bit XMM 暫存器
temp1 = _mm_load_si128( (__m128i*) ( (int*) &t1[i]) );
temp2 = _mm_load_si128( (__m128i*) ( (int*) &t2[i]) );
// 將前二個32-bit integer相乘並儲存此64-bit的結果到一個128-bit的暫存器之中
mul1 = _mm_mul_epu32(temp1,temp2) ;
//搞亂(shuffle)temp1的內容,放到其它二個32 bit integer的的位置以備做相乘的動作,並存到一個128-bit的暫存器之中
num1 = _mm_shuffle_epi32(temp1, _MM_SHUFFLE(1,1,3,3));
num2 = _mm_shuffle_epi32(temp2, _MM_SHUFFLE(1,1,3,3));
//將二個32-bit integer相乘並儲存64-bit的結果到128-bit的暫存器之中
mul2 = _mm_mul_epu32(num1, num2);
}
return 0;
}
實例1-4:
顯示與上面相同的計算,取二個32-bit的陣列並將這二個陣列相乘後傳回一個64-bit的結果(存在一個128-bit暫存器中),不過是用SSE2 組合語言(assembly)寫。
譯註:這個實例一定要轉換成Intel (R) C++ Project 才行。

int _tmain(int argc, _TCHAR* argv[]){
__declspec(align(16)) long mul;
__declspec(align(16)) int t1[100000];
__declspec(align(16)) int t2[100000];

__m128i mul1, mul2;

for (int j = 0; j <100000; j++){
t1[j]= j;
t2[j]= j+1;
}
// 真正的做運算
_asm
{
mov eax, 0
label: movdqa xmm0, xmmword ptr[t1+eax]
movdqa xmm1, xmmword ptr[t2+eax]
pmuludq xmm0, xmm1
movdqa mul1, xmm0
movdqa xmm0, xmmword ptr[t1+eax]
pshufd xmm0, xmm0, 05fh
pshufd xmm1, xmm1, 05fh
pmuludq xmm0, xmm1
movdqa mul2, xmm0
add eax, 16
cmp eax, 100000
jnge label
}
return 0;
}
範例2:
實例2-1:
顯示一個簡單的函式,使用代表一個包含了紅、綠、藍三個部份的像素(pixel)的32-bit integer陣列,並執行一個混合(blend)運算。
void blend(long r1[], long g1[], long b1[], long r2[], long g2[], long b2[])
{
// 用混合因子來將顏色值混在一起
for (int i=0; i < 10000, i++)
{
r1[i] = r1[i]*B + r1[i]*(1-B);
g1[i] = g1[i]*B + g1[i]*(1-B);
b1[i] = b1[i]*B + b1[i]*(1-B);
}
}
實例2-2:
顯示一個簡單的函式,它取了一個包含了紅、綠、藍三個部份的像素(pixel)的32-bit integer陣列,並執行一個混合(blend)運算,不過用SSE intrinsics來一次對三個做混合運算,而不是像前例一樣一次一個顏色。

void blend(long r1[], long g1[], long b1[], long r2[], long g2[], long b2[])
{
//宣告SSE變數
__m128 pixel1, pixel2;
//將混合因子值設到一個128-bit變數裡的32-bit區段
__m128 blend_mu1 = _mm_set_ps1(B);
__m128 blend_m1 = _mm_set_ps1(1.0-B);
// 將pixel1設成包含四個浮點變數。首先載入整數值,然後轉成單精度浮點數值。Pixel2也這麼做
for (int i = 0; i <100000; i++){
pixel1 = _mm_cvtepi32_ps( _mm_set_epi32(0.0, r1[i], g1[i], b1[i] ) );
pixel2 = _mm_cvtepi32_ps( _mm_set_epi32(0.0, r2[i], g2[i], b2[i] ) );
//使用intrinsics來執行乘法和加法,如同在實例2-1中看到的
pixel1 = _mm_add_ps(_mm_mul_ps(pixel1, blend_mu1), _mm_mul_ps(pixel2, blend_m1));
}
}


結論


這 只是介紹如何開始去使用SSE和SSE編程的技術。善用SSE和SSE2指令的優點可以在Pentium 4處理器上改進你的應用程式執行效能。在此展示的例子只驗證了少數可用的指令。若想要學習更多關於SSE/SSE2和其它最佳化的技術,請參考以下的 “References"小節。

參考資料


Intel (R) C++ Compiler User Guide and Reference
Intel (R) Pentium 4 Processor Optimization, Reference Manual.
The Macroarchitecture of the Pentium 4 Processor
Developing Applications Using Both the Intel (R) C/C++ Compiler and the Microsoft Visual C++* Compiler
Intel (R) C++ Compiler for Windows*
IA-32 Intel (R) Architecture Software Developer’s Manual, Volume 1: Basic Architecture
IA-32 Intel (R) Architecture Software Developer’s Manual, Volume 2: Instruction Set Reference
IA-32 Intel (R) Architecture Software Developer’s Manual, Volume 3: System Programming Guide
(幾乎在Intel (R)網站上都找得到)

No comments: