Visual C++*、GCC*、インテル® C++ コンパイラーなど、広く利用されているコンパイラーには様々な最適化オプションが用意されています。プロセッサーのコアの増加に伴いプログラムのマルチスレッド化が注目されていますが、本来プロセッサーコアに備わっているもう 1 つの並列性(命令レベルの並列性)を高めることで、さらにパフォーマンスを改善できます。
この連載では、命令レベルの並列性を実現する SIMD 命令を利用する最適化について説明します。SIMD 命令とは、Single Instruction Multiple Data、つまり1つの命令で複数のデータを処理することで、命令レベルの並列性を高めます。IA プロセッサーでは SIMD は、SSE 系の命令で実装されています。
第1回 SIMD 命令とプロセッサーの関係
皆さんはアプリケーションを開発する際に、そのアプリケーションがどのようなシステムで実行されるか想定されるでしょう。最初に考えられるのは、ターゲットとする OS のバージョンであるかと思いますが、今どき Windows 95 や Windows 98 をサポート対象にする開発者は、いませんよね。 次の表は、歴代の Windows のプロセッサーに関するシステム要件です。
OS のバージョン | プロセッサーのシステム要件 |
---|---|
Windows 2000 | 133MHz 以上の Pentium 互換 |
Windows XP Professional | 300MHz 以上の Pentium/Celeron および AMD K6/Athlon/Duron ファミリーまたは、これらの互換プロセッサー |
Windows Vista Home Basic | 800 MHz、32 ビット (x86) のプロセッサー、または 800 MHz、64 ビット (x64) のプロセッサー (Home エディション以外は 1GHz のクロックを要求) |
Windows 7 | 1 GHz 以上の 32 ビット (x86) プロセッサーまたは 64 ビット (x64) プロセッサー |
この OS のシステム要件が求めるプロセッサーが、SIMD 命令を使用する最適化オプションに影響します。例えば Windows Vista Home Basic までをサポートすると想定した場合、800MHz 以上のプロセッサーが求められていますから、対象となるプロセッサーは一部の Pentium III(450MHz – 1.4Gseisei Hz)と Pentium 4(1.3GHz – 3.8GHz)以上となります。まあ、少しぐらい動作クロックが違っても問題はないのですが。
この2つのプロセッサーには、サポートする SIMD 命令セットに違いがあり(Pentium III – SSE、 Pentium 4 – SSE2)、コンパイラーは対象とするプロセッサーが実行できる命令セットを使用したバイナリーを生成しなければいけません。この場合、SSE2 を利用したバイナリーを生成すると、Pentium III では実行できないアプリケーションになってしまいます(おそらくサポートされない命令が実行された、という例外が発生する)。
しかし、現実的に現在 Pentium III を搭載するシステムはほとんど利用されていないといっていいかもしれません。Pentium III は 1999年から 2003年まで製造されたプロセッサーであり、その寿命をまっとうしていると考えることができるからです。次の表は、歴代の SSE 命令とその発表年度、そして各命令セットがもつ機能をまとめたものです。
これらの命令セットの利用はアプリケーションの性能を劇的に改善することがあります。バイナリーを生成する際、現実的には Pentium 4 がサポートする SSE2 とそれ以降の命令セットを利用することになります。複数の命令セットに対応したバイナリーを用意し、それらを対象とするプロセッサーで実行するには次のような方法があります;
- アプリケーション自身が、OS がサポートするプロセッサー機能フラグを確認し、実行する命令セットが異なるバイナリーを切り分ける
- アプリケーション自身が、プロセッサーの状態フラグを確認し、実行する命令セットが異なるバイナリーを切り分ける
- コンパイラーがランタイムにプロセッサーを判断するコードを生成し、自動的に命令セットを切り分ける
SSE 系の命令を利用するには、OS がその環境をサポートしていなければいけません。OS でコンテキストスイッチが行われた場合、SSE 系の命令が利用する XMM レジスターを退避、そして復旧する必要があるためです。SSE から SSE4.2 までは同じレジスターを利用するので、Windows XP, Vista, 7 およびそのサーバーバージョンであれば利用することができます。AVX に関してはWindows 7 SP1 が必須となります。
これまで Windows SDK には OS が SSE 命令をサポートするか否かを直接確認する API がありませんでしたが、Windows 7 向けの SDK「Microsoft Windows SDK for Windows 7 and .NET Framework 3.5 SP1」では、CPU が SSE2 や AVX をサポートするかどうか確認する API GetEnabledExtendedFeatures が提供されています。この API は指定された機能が有効であると「真」を返します。
GetEnabledExtendedFeatures( XSTATE_MASK_LEGACY_SSE ); // SSE2 GetEnabledExtendedFeatures( XSTATE_MASK_GSSE ); // AVX
しかし、GetEnabledExtendedFeatures では SSE2 と AVX 以外のどの命令がサポートされるかわかりません。そこで、自分自身でプロセッサーの状態フラグを確認して、どの命令セットが有効であるか判断します。
IA プロセッサーには、自身がサポートする機能を通知する「CPUID」命令が搭載されており、EAX レジスターに「1」をセットして「CPUID」命令を実行すると、CPU の状態が EDX と ECX レジスターに返ります。EDX と ECX のそれぞれのビットには、サポートされる命令セットが示されます。次のコードを実行すると、サポートされる命令セットが表示されます。
このソースは 64 ビット版の Visual C++ ではコンパイルできません(インラインアセンブラーをサポートしないため)ので、32 ビット版の Visual C++ かインテル® コンパイラーでコンパイルしてください。
int main(){ int REG_edx, REG_ecx, REG_eax; _asm { mov eax, 1 // パラメータに 1 をセット cupid // 問い合わせ mov REG_edx, edx // 状態がECXとEDXに格納される mov REG_ecx, ecx // ので、メモリーへ移動 } if(REG_edx && 0x800000) printf("MMX "); // EDX bit 23 if(REG_edx && 0x2000000) printf("SSE "); // EDX bit 25 if(REG_edx && 0x4000000) printf("SSE2 "); // EDX bit 26 if(REG_ecx && 0x00000001) printf("SSE3 "); // ECX bit 0 if(REG_ecx && 0x00000200) printf("SSSE3 "); // ECX bit 9 if(REG_ecx && 0x00080000) printf("SSE4.1 "); // ECX bit 19 if(REG_ecx && 0x00100000) printf("SSE4.2 "); // ECX bit 20 if(REG_ecx && 0x00200000) printf("AESNI "); // ECX bit 25 if(REG_ecx && 0x00000002) printf("PCLMULQDQ "); // ECX bit 1 _asm { // AVX の判定には複数のビットを見る必要がある mov eax, 1 // 再度CPUIDを実行する必要はないが、 cupid // ここでは実行している and ecx, 018000000h // ECX の bit 27, 28 をマスク cmp ecx, 018000000h // bit 27 & 28 がオンならサポート jne not_support mov ecx, 0 xgetbv // xgetbv が使えるか? and eax, 06h // 使えれば eax の 06h がオン mov REG_eax, eax not_support: } if(REG_eax == 0x6) printf("AVX "); // EAX bit 1&2 }
このような方法を利用すれば、アプリケーション自身がランタイムでプロセッサーが利用できる SSE 命令を判定できます。しかし、最も簡単な方法はコンパイラーがサポートする 3番目の機能を利用する方法です。コンパイラーがソースコードを解析して自動的にSIMD命令を利用してくれれば、開発者の負担は大幅に軽減されます。
インテルコンパイラーの場合、世代の異なる複数の SIMD 命令(MMX、SSE から AVX まで)を利用したマルチバージョンのバイナリーを生成することができます。実行時に自動的にプロセッサーを判断し、利用できる命令セットのバージョンが実行されます。
次回は、SIMD 命令の効果について説明します。