Core i7のSSE4.2にも対応、インテル コンパイラーで作るSSE対応プログラム 2ページ

インテル コンパイラーで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)。

表2 使用するSSEバージョンを指定するコンパイルオプション
コンパイルオプション 最適化対象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に最適なコードが選択・実行される。ただし、このオプションを指定することで若干のオーバーヘッドが発生するほか、バイナリサイズが大きくなるので注意が必要である。

表3 複数アーキテクチャで動作するバイナリを出力するコンパイルオプション
コンパイルオプション 対応する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のようなコードである。

リスト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%程度の高速化が実現できている。

表4 テストに利用した環境
要素 スペック
CPU Core i7 920(2.66GHz)
メモリ 3GB
OS Windows Vista Home Premium(32bit版)
開発環境 Visual Studio 2008、インテル コンパイラー 11.1
表5 最大値探索処理にかかった時間
実行時間(秒)
指定無し /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命令を使用しており、アセンブラコードの行数も短くなっているのが確認できる。

リスト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))」を付加すればよい。