HPC/並列プログラミングポータルでは、HPC(High Performance Computing)プログラミングや並列プログラミングに関する情報を集積・発信しています。 |
[記事一覧を見る]
SSEは「マルチメディア処理向け命令」をうたっていたMMXの後継という側面もあるものの、その用途はマルチメディア処理だけにとどまらず、すべての数値演算処理や文字列処理などにも及ぶ。そのため、多くのアプリケーションでSSEによるパフォーマンスの向上が期待できる。
SSEを利用するには、インラインアセンブラを利用してSSE命令をソースコード中に直接記述するという方法がある。しかし、インラインアセンブラの利用にハードルの高さを感じる人も多いだろう。そこで以下では、アセンブラを利用せずに、C/C++中でSSEによるプログラムの高速化を行う方法について述べていこう。
まず、もっとも手軽なのがコンパイラの最適化機能を利用する方法だ。インテル コンパイラーでは、特にソースコードを変更することなしにSSEを利用するコードを自動的に出力できる。たとえばインテル コンパイラー 11.1ではデフォルトでSSE2を使用したコードを生成するようになっている。また、「/Qx<使用するSSEバージョン>」(Windows版)もしくは「-x<使用するSSEバージョン>」(LinuxおよびMac OS X版)コンパイルオプションの設定によってCPUを指定し、SSE3以降の命令を利用することも可能だ(表2)。
コンパイルオプション | 最適化対象CPU | |
---|---|---|
Windows版 | Linux | |
/QxHost | -xHost | コンパイルを実行したPCのCPU |
/QxAVX | -xAVX | Intel Advanced Vector Extentions(AVX)をサポートするCPU |
/QxSSE4.1 | -xSSE4.1 | SSE 4.1をサポートするCPU |
/QxSSE4.2 | -xSSE4.2 | SSE 4.2をサポートするCPU |
/QxSSSE3 | -xSSSE3 | SSSE3をサポートするCPU |
/QxSSE3_ATOM | -xSSE3_ATOM | Atomシリーズ |
/QxSSE3 | -xSSE3 | SSE3をサポートするCPU |
/QxSSE2 | -xSSE2 | SSE2をサポートするCPU |
なお、AtomにはSSE3に加えてバイトオーダー変換付きのロード/ストアを高速に行う「MOVBE」命令が追加されており、「/QxSSE3_ATOM」や「-xSSE3_ATOM」とともに「/Qinstruction:movbe」(Windows版)もしくは「-minstruction=movbe」(Linux版)というコンパイルオプションを指定することで、この命令を利用するコードを生成できる。
ちなみに、この「/Qx」もしくは「-x」オプション付きでコンパイルしたプログラムは、最適化対象として指定したCPU以外では実行できない。たとえば「/QxSSSE3」オプション付きでコンパイルしたプログラムをSSE3をサポートしないPentium 4やPentium Mを搭載したPC上で実行しようとすると、ランタイムエラーが発生する。もし特定のCPU以外でも動作するプログラムを作成したい場合は、「/Qax<使用するSSEバージョン>」(Windows版)もしくは「-ax<使用するSSEバージョン>」(LinuxおよびMac OS X版)というオプションを利用する(表3)。これらのオプションを利用すると、使用するSSEバージョンに応じた複数のコードが生成され、実行時にランタイムライブラリによって、実行するCPUに最適なコードが選択・実行される。ただし、このオプションを指定することで若干のオーバーヘッドが発生するほか、バイナリサイズが大きくなるので注意が必要である。
コンパイルオプション | 対応するSSEバージョン | |
---|---|---|
Windows版 | Linux | |
/QaxSSE4.2 | -axSSE4.2 | SSE4.2、SSE4.1、SSSE3、SSE2、SSE2、SSE |
/QaxSSE4.1 | -axSSE4.1 | SSE4.1、SSSE3、SSE3、SSE2、SSE |
/QaxSSSE3 | -axSSSE3 | SSSE3、SSE3、SSE2、SSE |
/QaxSSE3_ATOM | -axSSE3_ATOM | Atom |
/QaxSSE3 | -axSSE3 | SSE3、SSE2、SSE |
/QaxSSE2 | -axSSE2 | SSE2、SSE |
さて、それでは簡単なサンプルコードで、インテル コンパイラーによる自動ベクトル化の効果を確認してみよう。サンプルに使用したのは、次のリスト1のようなコードである。
void VectorizationTest() { int size = 100*1024*1024; int* i; float* f; double* d; int max_i; float max_f; double max_d; LARGE_INTEGER freq, begin, end; int n; i = (int*)_aligned_malloc( sizeof(int) * size, 16 ); f = (float*)_aligned_malloc( sizeof(float) * size, 16 ); d = (double*)_aligned_malloc( sizeof(double) * size, 16 ); srand(111); for( n = 0; n size; n++ ) { i[n] = rand(); f[n] = (float)rand() / (float)RAND_MAX; d[n] = (double)rand() / (double)RAND_MAX; } QueryPerformanceFrequency( freq ); /* int */ QueryPerformanceCounter( begin ); max_i = i[0]; for( n = 0; n size; n++ ) { if( max_i i[n] ) { max_i = i[n]; } } QueryPerformanceCounter( end ); printf( "int: %f sec.\n", ( (double)(end.QuadPart - begin.QuadPart) / (double)freq.QuadPart )); /* double */ QueryPerformanceCounter( begin ); max_d = d[0]; for( n = 0; n size; n++ ) { if( max_d d[n] ) { max_d = d[n]; } } QueryPerformanceCounter( end ); printf( "double: %f sec.\n", ( (double)(end.QuadPart - begin.QuadPart) / (double)freq.QuadPart )); /* float */ QueryPerformanceCounter( begin ); max_f = f[0]; for( n = 0; n size; n++ ) { if( max_f f[n] ) { max_f = f[n]; } } QueryPerformanceCounter( end ); printf( "float: %f sec.\n", ( (double)(end.QuadPart - begin.QuadPart) / (double)freq.QuadPart )); printf( "max: %d.\n", max_i ); printf( "max: %lf.\n", max_d ); printf( "max: %lf.\n", max_f ); }
このコードは、100×1024×1024個の要素を持つint、float、double型配列に格納されている数値の中で最大となるものを探索し、それぞれの場合で探索にかかった時間を計測するものだ。
このコードを「/QxSSSE3」(SSSE3対応CPU向け)、「/QxSSE4.2」(SSE4.2対応CPU向け)、指定無しという3種類のコンパイルオプションでコンパイルし、実行した結果が次の表5である。なお、テストに利用したのは表4のような環境である。float型およびdouble型の場合はどの場合もほとんど処理時間に変化は無かったが、int型の場合は/QxSSE4.2オプション付きでコンパイルすることで、5%程度の高速化が実現できている。
要素 | スペック |
---|---|
CPU | Core i7 920(2.66GHz) |
メモリ | 3GB |
OS | Windows Vista Home Premium(32bit版) |
開発環境 | Visual Studio 2008、インテル コンパイラー 11.1 |
型 | 実行時間(秒) | ||
---|---|---|---|
指定無し | /QxSSSE3 | /QxSSE4.2 | |
int | 0.0530 | 0.0533 | 0.0503 |
float | 0.0546 | 0.539 | 0.0536 |
double | 0.107 | 0.107 | 0.106 |
この処理時間の違いであるが、SSE4で新たに追加された、整数型の最大値を探索する「PMAXSD」という命令がその要因である。これは複数のint型変数の最大値を求める命令で、/QxSSE4.2オプション付きでコンパイルしたコードではこの命令が使用され処理の高速化が図られている。/QxSSSE3オプション付きでコンパイルした実行ファイルと、/QxSSE4.2オプション付きでコンパイルした実行ファイルについて、int型配列の探索を行っている部分のアセンブラコードを抜き出したものが次のリスト2およびリスト3だ。/QxSSSE3オプション付きの場合は「PCMPGTD」という、MMXに含まれる命令を使用して比較を行っているのに対し、/QxSSE4.2オプション付きの場合はPMAXSD命令を使用しており、アセンブラコードの行数も短くなっているのが確認できる。
004010DB: 8B 94 24 94 00 00 mov edx,dword ptr [esp+94h] 00 004010E2: 8B 3A mov edi,dword ptr [edx] 004010E4: 83 E2 0F and edx,0Fh 004010E7: 74 35 je 0040111E 004010E9: F6 C2 03 test dl,3 004010EC: 0F 85 C5 03 00 00 jne 004014B7 004010F2: 89 B4 24 90 00 00 mov dword ptr [esp+90h],esi 00 004010F9: 8B B4 24 94 00 00 mov esi,dword ptr [esp+94h] 00 00401100: F7 DA neg edx 00401102: 83 C2 10 add edx,10h 00401105: C1 EA 02 shr edx,2 00401108: 33 C0 xor eax,eax 0040110A: 8B 0C 86 mov ecx,dword ptr [esi+eax*4] 0040110D: 3B CF cmp ecx,edi 0040110F: 0F 4D F9 cmovge edi,ecx 00401112: 40 inc eax 00401113: 3B C2 cmp eax,edx 00401115: 72 F3 jb 0040110A 00401117: 8B B4 24 90 00 00 mov esi,dword ptr [esp+90h] 00 0040111E: 8B 8C 24 94 00 00 mov ecx,dword ptr [esp+94h] 00 00401125: 8B C2 mov eax,edx 00401127: F7 D8 neg eax 00401129: 83 E0 03 and eax,3 0040112C: F7 D8 neg eax 0040112E: 66 0F 6E C7 movd xmm0,edi 00401132: 66 0F 70 C0 00 pshufd xmm0,xmm0,0 00401137: 05 00 00 40 06 add eax,6400000h 0040113C: 66 0F 6F 0C 91 movdqa xmm1,xmmword ptr [ecx+edx*4] 00401141: 66 0F 6F D1 movdqa xmm2,xmm1 00401145: 66 0F EF C8 pxor xmm1,xmm0 00401149: 83 C2 04 add edx,4 0040114C: 66 0F 66 D0 pcmpgtd xmm2,xmm0 00401150: 66 0F DB D1 pand xmm2,xmm1 00401154: 66 0F EF C2 pxor xmm0,xmm2 00401158: 3B D0 cmp edx,eax 0040115A: 72 E0 jb 0040113C 0040115C: 66 0F 6F C8 movdqa xmm1,xmm0 00401160: 66 0F 6F D0 movdqa xmm2,xmm0 00401164: 66 0F 73 D9 08 psrldq xmm1,8 00401169: 66 0F EF C1 pxor xmm0,xmm1 0040116D: 66 0F 66 D1 pcmpgtd xmm2,xmm1 00401171: 66 0F DB D0 pand xmm2,xmm0 00401175: 66 0F EF D1 pxor xmm2,xmm1 00401179: 66 0F 6F C2 movdqa xmm0,xmm2 0040117D: 66 0F 6F DA movdqa xmm3,xmm2 00401181: 66 0F 73 D8 04 psrldq xmm0,4 00401186: 66 0F EF D0 pxor xmm2,xmm0 0040118A: 66 0F 66 D8 pcmpgtd xmm3,xmm0 0040118E: 66 0F DB DA pand xmm3,xmm2 00401192: 66 0F EF D8 pxor xmm3,xmm0 00401196: 66 0F 7E DF movd edi,xmm3 0040119A: 3D 00 00 40 06 cmp eax,6400000h 0040119F: 73 17 jae 004011B8 004011A1: 8B 8C 24 94 00 00 mov ecx,dword ptr [esp+94h] 00 004011A8: 8B 14 81 mov edx,dword ptr [ecx+eax*4] 004011AB: 3B D7 cmp edx,edi 004011AD: 0F 4D FA cmovge edi,edx 004011B0: 40 inc eax 004011B1: 3D 00 00 40 06 cmp eax,6400000h 004011B6: 72 F0 jb 004011A8
004010DB: 8B 37 mov esi,dword ptr [edi] 004010DD: 8B C7 mov eax,edi 004010DF: 83 E0 0F and eax,0Fh 004010E2: 74 1F je 00401103 004010E4: A8 03 test al,3 004010E6: 0F 85 7E 03 00 00 jne 0040146A 004010EC: F7 D8 neg eax 004010EE: 83 C0 10 add eax,10h 004010F1: C1 E8 02 shr eax,2 004010F4: 33 D2 xor edx,edx 004010F6: 8B 0C 97 mov ecx,dword ptr [edi+edx*4] 004010F9: 3B CE cmp ecx,esi 004010FB: 0F 4D F1 cmovge esi,ecx 004010FE: 42 inc edx 004010FF: 3B D0 cmp edx,eax 00401101: 72 F3 jb 004010F6 00401103: 8B D0 mov edx,eax 00401105: F7 DA neg edx 00401107: 83 E2 03 and edx,3 0040110A: 66 0F 6E C6 movd xmm0,esi 0040110E: 66 0F 70 C0 00 pshufd xmm0,xmm0,0 00401113: F7 DA neg edx 00401115: 81 C2 00 00 40 06 add edx,6400000h 0040111B: 66 0F 6F 0C 87 movdqa xmm1,xmmword ptr [edi+eax*4] 00401120: 66 0F 6F D0 movdqa xmm2,xmm0 00401124: 66 0F 6F C1 movdqa xmm0,xmm1 00401128: 66 0F 38 3D C2 pmaxsd xmm0,xmm2 0040112D: 83 C0 04 add eax,4 00401130: 3B C2 cmp eax,edx 00401132: 72 E7 jb 0040111B 00401134: 66 0F 70 C8 0E pshufd xmm1,xmm0,0Eh 00401139: 66 0F 38 3D C1 pmaxsd xmm0,xmm1 0040113E: 66 0F 70 D0 39 pshufd xmm2,xmm0,39h 00401143: 66 0F 38 3D C2 pmaxsd xmm0,xmm2 00401148: 66 0F 7E C6 movd esi,xmm0 0040114C: 81 FA 00 00 40 06 cmp edx,6400000h 00401152: 73 11 jae 00401165 00401154: 8B 04 97 mov eax,dword ptr [edi+edx*4] 00401157: 3B C6 cmp eax,esi 00401159: 0F 4D F0 cmovge esi,eax 0040115C: 42 inc edx 0040115D: 81 FA 00 00 40 06 cmp edx,6400000h 00401163: 72 EF jb 00401154
インテル コンパイラーでSSEを利用するもう1つの手段として、組み込み関数(Intrinsics)と呼ばれている関数群を利用する方法がある。インテル コンパイラーのドキュメントでは、「組み込み関数はアセンブラで記述された関数であり、C++の関数内で呼び出せるほか、(C/C++の)変数を適切にアセンブラ命令に渡すことができる」とされている。
この説明では若干分かりにくいが、要は組み込み関数はCPU命令をC/C++の関数として呼ぶためのラッパー関数である。CPU命令をC/C++で利用する方法としては他にインラインアセンブラがあるが、組み込み関数はCの関数呼び出しと同様の形式でコードを記述できるため、メンテナンス性が高いのが特徴だ。組み込み関数はコンパイル時にインライン関数として展開されるため、呼び出しのオーバーヘッドも少ない。
インテル コンパイラーにはMMXおよびSSE2~SSE4.2、Intel AVXに含まれる各命令に対応した組み込み関数が用意されており、それぞれに対応したヘッダーファイルをincludeすることで利用可能になる。たとえばSSE2に含まれる加算命令「PADDD」は、組み込み関数では次のように定義されている。
__m128i _mm_add_epi32(__m128i a, __m128i b)
詳細についてはインテル コンパイラーのドキュメントなどを参照してほしいが、これは4個の32ビット整数同士を加算するものだ。使用例は次のようになる。
_declspec(align(16)) int a[4]; _declspec(align(16)) int b[4]; _declspec(align(16)) int result[4]; /* ここでa、bに値を代入 */ a[0] = ... : : /* 加算の実行 */ *((__m128i*)result) = _mm_add_epi32( *((__m128i*)a), *((__m128i*)b) );
なお、MMX/SSE命令では処理対象となるメモリが16バイト境界に合わせて確保されていないと一般保護例外が発生する場合がある。メモリを16バイト境界に合わせて確保するには、変数を宣言する個所に「_declspec(align(16))」を付加すればよい。
[PageInfo]
LastUpdate: 2009-11-19 15:18:29, ModifiedBy: hiromichi-m
[Permissions]
view:all, edit:login users, delete/config:members