[[PageNavi(NavigationList)]]
=== インテル コンパイラーでSSEを利用する ===
SSEは「マルチメディア処理向け命令」をうたっていたMMXの後継という側面もあるものの、その用途はマルチメディア処理だけにとどまらず、すべての数値演算処理や文字列処理などにも及ぶ。そのため、多くのアプリケーションでSSEによるパフォーマンスの向上が期待できる。
SSEを利用するには、インラインアセンブラを利用してSSE命令をソースコード中に直接記述するという方法がある。しかし、インラインアセンブラの利用にハードルの高さを感じる人も多いだろう。そこで以下では、アセンブラを利用せずに、C/C++中でSSEによるプログラムの高速化を行う方法について述べていこう。
まず、もっとも手軽なのがコンパイラの最適化機能を利用する方法だ。インテル コンパイラーでは、特にソースコードを変更することなしにSSEを利用するコードを自動的に出力できる。たとえばインテル コンパイラー 11.1ではデフォルトでSSE2を使用したコードを生成するようになっている。また、「/Qx<使用するSSEバージョン>」(Windows版)もしくは「-x<使用するSSEバージョン>」(LinuxおよびMac OS X版)コンパイルオプションの設定によってCPUを指定し、SSE3以降の命令を利用することも可能だ。('''表2''')。
{{{ html
<h6>表2 使用するSSEバージョンを指定するコンパイルオプション</h6>
<table class="wikitable">
<tr><th colspan="2">コンパイルオプション</th><th rowspan="2">最適化対象CPU</th></tr>
<tr><td>Windows版</td><td>Linux</td></tr>
<tr><td>/QxHost</td><td>-xHost</td><td>コンパイルを実行したPCのCPU</td></tr>
<tr><td>/QxAVX</td><td>-xAVX</td><td>Intel Advanced Vector Extentions(AVX)をサポートするCPU</td></tr>
<tr><td>/QxSSE4.1</td><td>-xSSE4.1</td><td>SSE 4.1をサポートするCPU</td></tr>
<tr><td>/QxSSE4.2</td><td>-xSSE4.2</td><td>SSE 4.2をサポートするCPU</td></tr>
<tr><td>/QxSSSE3</td><td>-xSSSE3</td><td>SSSE3をサポートするCPU</td></tr>
<tr><td>/QxSSE3_ATOM</td><td>-xSSE3_ATOM</td><td>Atomシリーズ</td></tr>
<tr><td>/QxSSE3</td><td>-xSSE3</td><td>SSE3をサポートするCPU</td></tr>
<tr><td>/QxSSE2</td><td>-xSSE2</td><td>SSE2をサポートするCPU</td></tr>
</table>
}}}
なお、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に最適なコードが選択・実行される。ただし、このオプションを指定することで若干のオーバーヘッドが発生するほか、バイナリサイズが大きくなるので注意が必要である。
{{{ html
<h6>表3 複数アーキテクチャで動作するバイナリを出力するコンパイルオプション</h6>
<table class="wikitable">
<tr><th colspan="2">コンパイルオプション</th><th rowspan="2">対応するSSEバージョン</th></tr>
<tr><td>Windows版</td><td>Linux</td></tr>
<tr><td>/QaxSSE4.2</td><td>-axSSE4.2</td><td>SSE4.2、SSE4.1、SSSE3、SSE2、SSE2、SSE</td></tr>
<tr><td>/QaxSSE4.1</td><td>-axSSE4.1</td><td>SSE4.1、SSSE3、SSE3、SSE2、SSE</td></tr>
<tr><td>/QaxSSSE3</td><td>-axSSSE3</td><td>SSSE3、SSE3、SSE2、SSE</td></tr>
<tr><td>/QaxSSE3_ATOM</td><td>-axSSE3_ATOM</td><td>Atom</td></tr>
<tr><td>/QaxSSE3</td><td>-axSSE3</td><td>SSE3、SSE2、SSE</td></tr>
<tr><td>/QaxSSE2</td><td>-axSSE2</td><td>SSE2、SSE</td></tr>
</table>
}}}
==== インテル コンパイラーによる自動ベクトル化の例 ====
さて、それでは簡単なサンプルコードで、インテル コンパイラーによる自動ベクトル化の効果を確認してみよう。サンプルに使用したのは、次の'''リスト1'''のようなコードである。
====== リスト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%程度の高速化が実現できている。
{{{ html
<h6>表4 テストに利用した環境</h6>
<table class="wikitable">
<tr><th>要素</th><th>スペック</th></tr>
<tr><td>CPU</td><td>Core i7 920(2.66GHz)</td></tr>
<tr><td>メモリ</td><td>3GB</td></tr>
<tr><td>OS</td><td>Windows Vista Home Premium(32bit版)</td></tr>
<tr><td>開発環境</td><td>Visual Studio 2008、インテル コンパイラー 11.1</td></tr>
</table>
}}}
{{{ html
<h6>表5 最大値探索処理にかかった時間</h6>
<table class="wikitable">
<tr><th rowspan="2">型</th><th colspan="3">実行時間(秒)</th></tr>
<tr><td>指定無し</td><td>/QxSSSE3</td><td>/QxSSE4.2</td></tr>
<tr><td>int</td><td>0.0530</td><td>0.0533</td><td>0.0503</td></tr>
<tr><td>float</td><td>0.0546</td><td>0.539</td><td>0.0536</td></tr>
<tr><td>double</td><td>0.107</td><td>0.107</td><td>0.106</td></tr>
</table>
}}}
この処理時間の違いであるが、SSE4で新たに追加された、整数型の最大値を探索する「PMAXSD」という命令がその要因である。これは複数のint型変数の最大値を求める命令で、/QxSSE4.2オプション付きでコンパイルしたコードではこの命令が使用され処理の高速化が図られている。/QxSSSE3オプション付きでコンパイルした実行ファイルと、/QxSSE4.2オプション付きでコンパイルした実行ファイルについて、int型配列の探索を行っている部分のアセンブラコードを抜き出したものが次の'''リスト2'''および'''リスト3'''だ。/QxSSSE3オプション付きの場合は「PCMPGTD」という、MMXに含まれる命令を使用して比較を行っているのに対し、/QxSSE4.2オプション付きの場合はPMAXSD命令を使用しており、アセンブラコードの行数も短くなっているのが確認できる。
====== リスト2 「/QxSSSE3」オプション付きでコンパイルした場合のコード ======
{{{
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
}}}
====== リスト3 「/QxSSE4.2」オプション付きでコンパイルした場合のコード ======
{{{
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を利用する組み込み関数(Intrinsics)を使う ====
インテル コンパイラーで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))」を付加すればよい。
[[PageNavi(NavigationList)]]