HPC/並列プログラミングポータルでは、HPC(High Performance Computing)プログラミングや並列プログラミングに関する情報を集積・発信しています。

新着トピックス

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

374dabfcc13d28994c5fd879106d2ce3.jpg  インテル コンパイラーの特徴の1つに「自動ベクトル化」がある。これはSSE(Streaming SIMD Extensions)と呼ばれるCPUの機能を利用することで数値演算の高速化を図るものだ。インテル コンパイラーは最新のCore i7といったCPUに搭載されているSSE4.2に対応しており、現行のほとんどのインテルCPUにおいて高速化が期待できる。本記事では、インテル コンパイラーでのSSEの使用と、その効果について解説する。

 インテル コンパイラーには生成するプログラムの処理速度を向上させるためのさまざまな機能が備えられているが、その1つに「自動ベクトル化(Auto vectorization)」というものがある。これは、forループなど同一の演算を繰り返すような処理を、インテル製のCPUが持つ「SSE(Streaming SIMD Extensions)」という命令群を利用して複数のデータを一括処理することで高速化を図る機能だ。

 SSEはPentium III以降のインテル製CPUが備えている機能で、専用128ビットレジスタを使用し、複数の演算処理を一括して実行する、というものだ。この専用レジスタは32ビット環境では8本、64ビット環境では16本が利用できる。SSEを利用することで、たとえば32ビット環境では32ビットのデータを4つ、16ビットのデータなら8つを一括して処理できるようになり、処理時間の高速化が期待できる(図1)。

 SSEのリリース当初は70個の命令が含まれていたが、続いて2000年には新たに114個の命令を追加したSSE2が登場、以後もSSE3、SSSE3(Supplemental Streaming SIMD Extensions 3)、SSE4.1、SSE4.2と改良版がリリースされている。各CPUが対応SSEバージョンをまとめたものが表1だ。

表1 各CPUのSSE対応状況
SSE命令バージョンCPU
SSE4.2Core i7、Xeon 55XXシリーズ
SSE4.1Xeon 74XX、54XX、52XX、33XX、31XXシリーズ、Core 2 Extreme 9XXXシリーズ、Core 2 Quad 9XXXシリーズ、Core 2 Duo 8XXXシリーズおよびE7200
SSSE3(Supplemental Streaming SIMD Extensions 3)Xeon 73XX、72XX、53XX、51XX、32XX、30XXシリーズ、Core 2 Extreme 7XXX、6XXXシリーズ、Core 2 Quad 6XXXシリーズ、Core 2 Duo 7XXX(E7200を除く)、6XXX、5XXX、4XXXシリーズ、Core 2 Solo 2XXXシリーズ、Pentium dual-core E2XXX、T23XXシリーズ、Atomシリーズ
SSE3Xeon 70XX、71XX、50XXシリーズ、ULV/LV版Xeon1.66、2.0、2.16、Xeon 2.8、Core Duo、Core Solo、Pentium dual-core T21XX、T20XXシリーズ、Pentium Extreme Edition、Pentium D
SSE2上記以外のPentium 4、Xeon

 SSEには上位互換性があり、たとえばSSE4.2に対応したCPUであれば、SSSE3やSSE3、SSE2、SSEに含まれる命令すべてを利用できる。それぞれに含まれる命令の詳細についてはインテルのWebページ等を参照してほしいが、基本的な整数および単精度/倍精度浮動小数点の計算についてはSSE2でほぼカバーされており、SSE3およびSSSE3、SSE4.1、SSE4.2ではメモリからのロードや単純でない演算処理などを実行するための命令が追加されている。

 なお、インテルはSSEの後継として「Intel AVX(Advanced Vector Extensions)」という技術の開発を行っている。AVXは256ビットレジスタを利用し、これによってより多数の演算を同時実行できるようになるとのことだ。AVXを搭載したCPUは2010年以降にリリースされる見込みで、まだ市場には出回っていないものの、インテル コンパイラー 11.1ではいち早くAVXのサポートが行われている。

インテル コンパイラーで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-xAVXIntel Advanced Vector Extentions(AVX)をサポートするCPU
/QxSSE4.1-xSSE4.1SSE 4.1をサポートするCPU
/QxSSE4.2-xSSE4.2SSE 4.2をサポートするCPU
/QxSSSE3-xSSSE3SSSE3をサポートするCPU
/QxSSE3_ATOM-xSSE3_ATOMAtomシリーズ
/QxSSE3-xSSE3SSE3をサポートするCPU
/QxSSE2-xSSE2SSE2をサポートする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.2SSE4.2、SSE4.1、SSSE3、SSE2、SSE2、SSE
/QaxSSE4.1-axSSE4.1SSE4.1、SSSE3、SSE3、SSE2、SSE
/QaxSSSE3-axSSSE3SSSE3、SSE3、SSE2、SSE
/QaxSSE3_ATOM-axSSE3_ATOMAtom
/QaxSSE3-axSSE3SSE3、SSE2、SSE
/QaxSSE2-axSSE2SSE2、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 テストに利用した環境
要素スペック
CPUCore i7 920(2.66GHz)
メモリ3GB
OSWindows Vista Home Premium(32bit版)
開発環境Visual Studio 2008、インテル コンパイラー 11.1
表5 最大値探索処理にかかった時間
実行時間(秒)
指定無し/QxSSSE3/QxSSE4.2
int0.05300.05330.0503
float0.05460.5390.0536
double0.1070.1070.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))」を付加すればよい。

組み込み関数の使用例

 さて、SSE命令では128ビットのレジスタを使用するため、同時に処理できるのは最大128ビットであり、たとえば8ビットサイズの変数であれば最大で16個までである。それ以上の変数に対して処理を行う場合は、次のリスト4のようにループを使ってSSE命令を複数回実行すればよい。

 リスト4は1048576(1024×1024)個の要素を持つint型配列同士の加算を行うコードで、forループを使って実装した場合と、組み込み関数である_mm_add_epi32関数を使って実装した場合とでそれぞれの処理時間を計測するものだ。

リスト4 SSEを使って5個以上の変数の加算処理を行う例
#include "emmintrin.h"
  :
  :
    LARGE_INTEGER freq, begin, end;
    int size = 1024*1024;
    int* a;
    int* b;
    int* result;
    int n;

    a = (int*)_aligned_malloc( sizeof(int) * size, 16 );
    b = (int*)_aligned_malloc( sizeof(int) * size, 16 );
    result = (int*)_aligned_malloc( sizeof(int) * size, 16 );

    srand(11111);
    for( n = 0; n  size; n++ ) {
        a[n] = rand();
        b[n] = rand();
    }

    /* forループを使って実行 */
    QueryPerformanceCounter( begin );
    for( n = 0 ; n  size; n++ ) {
        result[n] = a[n] + b[n];
    }
    QueryPerformanceCounter( end );
    printf( "for-loop version: %f sec.\n",  ( (double)(end.QuadPart - begin.QuadPart) / (double)freq.QuadPart ));

    /* SSEを使って実行 */
    QueryPerformanceCounter( begin );
    for( n = 0; n  size - 3 ; n+= 4 ) {
        *((__m128i*)(result+n)) = _mm_add_epi32( *((__m128i*)(a+n)), *((__m128i*)(b+n)) );
    }
    for( ; n  size; n++ ) {
        result[n] = a[n] + b[n];
    }
    QueryPerformanceCounter( end );
    printf( "Intrinsic version: %f sec.\n",  ( (double)(end.QuadPart - begin.QuadPart) / (double)freq.QuadPart ));

    _aligned_free(a);
    _aligned_free(b);
    _aligned_free(result);

 なお、16バイト境界に合わせて動的にメモリを確保するには、「_aligned_malloc」関数を用いる。_aligned_mallocは第二引数としてアライメント境界を取る点が異なるだけで、mallocとほぼ同じように利用できる。

    a = (int*)_aligned_malloc( sizeof(int) * size, 16 );

 気になる処理時間の違いであるが、先ほどと同様(表4)の環境でテストを行ったところ、次の表6のような結果となった。この例ではSSEを利用することで、処理時間を約70%程度に短縮できていることが分かる。

表6 整数の繰り返し加算処理にかかった時間(要素数:1048576)
実装時間(ミリ秒)
SSE(_mm_add_epi32)3.29
forループ4.67

SSE対応のC標準関数を使う

 インテル コンパイラーではabsやsin、cos、tan、logといった各種数値演算関数や、strcpy、strlen、memcpy、memsetといった文字列/メモリアクセス関数といった、C標準ライブラリ関数の一部についても組み込み関数として用意されている。これらの組み込み関数については、「/Oi」(Windows版)もしくは「-fbuiltin」(Linux、Mac OS X版)オプション付きでコンパイルを行うことで利用できる。

 たとえばC標準ライブラリ関数には切り上げ/切り捨て処理を行う関数としてceilおよびfloorという関数が用意されているのだが、インテル コンパイラーで組み込み関数を有効にし、かつSSE4向けの最適化を行うように設定すると、これらの関数がSSE4で追加された浮動小数点の丸め演算命令(ROUND*命令)を利用するものに置き換わり、高速に実行できるようになる。

 次のリスト5のようなコードをコンパイルオプションを変えてコンパイルし、その実行速度の違いを検証したところ、表7のような結果となった(テスト環境は先ほどと同様、表4のとおり)。組み込み関数を使用してSSE4.1および4.2向けの最適化を行った場合、floorおよびceilの実行時間がそれぞれ半分程度にまで短縮されていることが分かる。

表7 floorおよびceil関数の実行時間比較
SSEの指定実行時間(秒)
floorceil
指定なし0.2620.172
SSE20.2600.173
SSE30.2580.171
SSSE30.2580.174
SSE4.10.1560.072
SSE4.20.1560.072

リスト5 ceil関数の処理速度測定コード
    float* A;
    float* B;
    int n;
    int dim = 1024*1024*100;
    LARGE_INTEGER freq, begin, end;

    srand(1111);
    for( n = 0; n  dim; n++ ) {
        A[n] = (float)rand() / (float)(RAND_MAX);
    }

    QueryPerformanceFrequency( freq );
    QueryPerformanceCounter( begin );
    for( i = 0; i  dim; i++ ) {
        B[i] = ceil(A[i]);
    }
    QueryPerformanceCounter( end );
    printf( "ceil: %f sec.\n",  ( (double)(end.QuadPart - begin.QuadPart) / (double)freq.QuadPart ));

    QueryPerformanceFrequency( freq );
    QueryPerformanceCounter( begin );
    for( i = 0; i  dim; i++ ) {
        B[i] = floor(A[i]);
    }
    QueryPerformanceCounter( end );
    printf( "floor: %f sec.\n",  ( (double)(end.QuadPart - begin.QuadPart) / (double)freq.QuadPart ));


SSEに対応するインテル インテグレーテッド・パフォーマンス・プリミティブ(IPP)

 そのほか、インテル コンパイラー 11.1に付属するライブラリ「インテル インテグレーテッド・パフォーマンス・プリミティブ(IPP)」についても、AtomおよびCore i7対応が行われている。といっても、プログラムのコンパイル時やリンク時に特に意識する必要はなく、リンクしたプログラムの実行時にCPUが判別され、対応する関数やコードが自動的に実行される。また、IPPのランタイムライブラリDLLについても、x86汎用のものに加えて特定のCPU向けに最適化されたものが用意されている。

 たとえば、IPPのサンプルとしてインテルが提供している、H.264エンコーダ(「umc_h264_dec_con.exe」)で比較してみると、SSEを利用しないx86汎用のライブラリを使用した場合と、SSE4向けのライブラリを使用した場合とで10倍以上パフォーマンスが異なるという結果となった(表8)。テストに使用した環境は先ほどと同様、表4のとおりである。

表8 SSEを利用することによるH.264エンコーダの処理時間の違い
使用するライブラリエンコード時間(秒)
汎用版73
SSE4向け最適化版6.73

 このサンプルプログラムは米インテルのWebサイトからダウンロードできるもので、詳細については 『マルチメディア処理から信号処理まで幅広く活用できる高速ライブラリ「IPP」』でも解説しているのでそちらを確認してほしい。なお、比較に使用した動画ファイルはWindows Vistaに含まれるサンプルビデオ「湖.wmv」(縦横サイズは720×480、長さは約16秒間)をYUV形式に変換したものを用いている。

SSEを利用することで大幅なパフォーマンスの向上が可能

 SSEが初めて搭載されたPentium IIIが登場した1999年から約10年が経過した現在では、ほぼすべてのPCでSSEの利用が可能と言っても過言ではない。SSEを利用することで、多くの状況でパフォーマンスの向上が期待できるため、パフォーマンスが必要とされるアプリケーションではSSEの積極利用を検討するべきであろう。

 ただし、自動ベクトル化は万能ではなく、コードによってはうまく最適化が行われない場合もある。その場合、インテル コンパイラーの「/Qvec-report」(Windows版)もしくは「-vec-report」(LinuxおよびMac OS X版)オプションを利用することで、ベクトル化できない個所やその依存性などに関するレポートを確認できる。これらによって問題点を特定し、ソースを変更して原因を排除できれば自動ベクトル化が可能になる場合もある。自動ベクトル化と組み込み関数の使用を組み合わせ、より細かい最適化を行っていくと良いだろう。