HPC/並列プログラミングポータルでは、HPC(High Performance Computing)プログラミングや並列プログラミングに関する情報を集積・発信しています。 |
[記事一覧を見る]
インテル C++ Composer XE 2011には、表3のようなさまざまな最適化機能が搭載されている。
最適化機能 | 説明 |
---|---|
自動ベクトル化 | インテルCPUが備えるストリーミングSIMD拡張命令(SSE)を使用したベクトル演算により処理を高速化する |
自動並列化 | 安全に並列実行できるループを検出し、マルチスレッドで並列実行するコードを自動生成する |
ハイパフォーマンス最適化機構(HPO) | ループを解析し、キャッシュアクセスやメモリアクセスの最適化、SSEの使用、並列化、高速化のためのループ構造変更といった総合的な最適化を行う |
プロシージャ間の最適化(IPO) | 高頻度で呼び出される関数についてリンク時にインライン化を行うことで呼び出しオーバーヘッドの削減による高速化を計る |
プロファイルに基づく最適化 | プログラムを実際に実行した際の挙動を記録したデータ(プロファイル)を用いてコードを最適化する |
データ・プリフェッチ | データを先読みしてキャッシュに格納しておくことで計算処理のパフォーマンスを向上させる |
このなかでも、特に注目したい機能が自動ベクトル化と自動並列化機能、ハイパフォーマンス最適化機能である。
インテルCPUには、「ストリーミングSIMD拡張命令(SSE)」と呼ばれる演算命令が備えられている。SSEはXMMレジスタと呼ばれる専用レジスタを使用して各種演算を実行する機能で、FPU(浮動小数点処理ユニット)の代わりとして利用できるだけでなく、複数データに対する演算を同時に実行することが可能だ。XMMレジスタのサイズは128ビットなので、たとえば8バイトの倍精度浮動小数点データなら2個、4バイトの単精度浮動小数点データなら4個、1バイトのデータであれば16個を同時に処理できる。このように複数のデータを一括して処理する演算は「ベクトル処理」などとも呼ばれ、このような処理を行う命令は「パックド命令」などと呼ばれている。多くのデータを処理する場合、パックド命令を効率良く利用することで演算時間の短縮が期待できる。
SSEはPentium III以降のインテルCPUに搭載されており、Pentium 4以降のCPUではSSE2/3、SSSE3、SSE4と拡張が続けられている。また、2011年初頭に登場すると見られているインテルの次世代CPUアーキテクチャ「Sandy Bridge」では、256ビットレジスタを利用できる「Intel AVX」と呼ばれる拡張命令セットが搭載される予定だ。
さて、このSSE命令を利用する場合、効率的なコードを記述するためにはアセンブラでコードを記述する、もしくは専用のヘッダファイルをインクルードして「intrinsic命令」などと呼ばれるSSE命令を呼び出す関数を使用するなど、どのようにSSEを使用するかを明示的に指定する必要があった。いっぽうインテル C++ Composer XEでは明示的にSSEの利用を指定しなくとも、デフォルトでSSE/SSE2命令を利用したコードが出力されるようになっている。さらに、インテル C++ Composer XE 2011では複数のデータを連続して演算するような処理の場合、自動的にパックド命令を利用するコードを出力する。これが自動ベクトル化と呼ばれる機能である。
自動ベクトル化は、主にループなどを使用して配列に対し連続して同一の演算を行うような処理において適用される。たとえばリスト1は、そのような処理の典型的なコードだ。
int i; float a[128]; float b[128]; float d; : : d = 0.0; for (i = 0; i 128; i++) { d += a[i] * b[i]; } printf("%f\n", d); : :
これは「積和計算」などと呼ばれる処理で、科学・工業計算などの分野でよく利用されるものだ。このコードをインテル C++ Composer XE 2011でコンパイルすると、次のリスト2のようなアセンブラコードが出力される。
;;; d = 0.0; pxor xmm0, xmm0 ; xmm0を0に初期化 mov esi, eax .B1.7: xor edx, edx ; edxを0に初期化 pxor xmm1, xmm1 ; xmm1を0に初期化 pxor xmm0, xmm0 ; xmm0を0に初期化 .B1.8: ;;; for (i = 0; i 128; i++) { ;;; d += a[i] * b[i]; movaps xmm2, XMMWORD PTR [128+esp+edx*4] ; xmm2にa[0+edx]をコピー movaps xmm3, XMMWORD PTR [144+esp+edx*4] ; xmm3にa[4+edx]をコピー mulps xmm2, XMMWORD PTR [640+esp+edx*4] ; xmm2にb[0+edx]を乗算 mulps xmm3, XMMWORD PTR [656+esp+edx*4] ; xmm3にb[4+edx]を乗算 addps xmm1, xmm2 ; xmm1にxmm2を加算 addps xmm0, xmm3 ; xmm0にxmm3を加算 movaps xmm4, XMMWORD PTR [160+esp+edx*4] ; xmm4にa[8+edx]をコピー movaps xmm5, XMMWORD PTR [176+esp+edx*4] ; xmm5にa[12+edx]をコピー mulps xmm4, XMMWORD PTR [672+esp+edx*4] ; xmm4にb[8+edx]を乗算 mulps xmm5, XMMWORD PTR [688+esp+edx*4] ; xmm5にb[12+edx]を乗算 addps xmm1, xmm4 ; xmm1にxmm4を加算 addps xmm0, xmm5 ; xmm0にxmm5を加算 movaps xmm6, XMMWORD PTR [192+esp+edx*4] ; xmm6にa[16+edx]をコピー movaps xmm7, XMMWORD PTR [208+esp+edx*4] ; xmm7にa[20+edx]をコピー mulps xmm6, XMMWORD PTR [704+esp+edx*4] ; xmm6にb[16+edx]を乗算 mulps xmm7, XMMWORD PTR [720+esp+edx*4] ; xmm7にb[20+edx]を乗算 addps xmm1, xmm6 ; xmm1にxmm6を加算 addps xmm0, xmm7 ; xmm0にxmm7を加算 movaps xmm2, XMMWORD PTR [224+esp+edx*4] ; xmm2にa[24+edx]をコピー movaps xmm3, XMMWORD PTR [240+esp+edx*4] ; xmm3にa[28+edx]をコピー mulps xmm2, XMMWORD PTR [736+esp+edx*4] ; xmm2にb[24+edx]を乗算 mulps xmm3, XMMWORD PTR [752+esp+edx*4] ; xmm3にb[28+edx]を乗算 addps xmm1, xmm2 ; xmm1にxmm2を加算 addps xmm0, xmm3 ; xmm0にxmm3を加算 add edx, 32 ; edxに32を加算 cmp edx, 128 ; edx == 128か? jb .B1.8 ; edx == 128でなければ.B1.8へ戻る .B1.9: ;;; } ;;; ;;; printf("%f\n", d); mov DWORD PTR [esp], OFFSET FLAT: ??_C@_03A@?$CFf?6?$AA@ addps xmm1, xmm0 ; xmm1にxmm0を加算 ; xmm1の上位64ビットと下位64ビットを加算 movaps xmm0, xmm1 ; xmm1をxmm0にコピー movhlps xmm0, xmm1 ; xmm1の上位64ビットをxmm0の下位64ビットにコピー addps xmm1, xmm0 ; xmm1にxmm0を加算 movaps xmm2, xmm1; xmm1をxmm2にコピー ; xmm2の32~63ビットをxmm2の0~63ビットにコピー、xmm1の96~127ビットをxmm2の64~127ビットにコピー shufps xmm2, xmm1, 245 addss xmm1, xmm2 ; xmm2の下位32ビットをxmm1の下位32ビットに加算 cvtss2sd xmm1, xmm1; xmm1の下位32ビットをdouble型に変換してxmm1の下位64ビットに格納 movsd QWORD PTR [4+esp], xmm1 ; xmm1の下位64ビットをスタックに投入 call _printf
ここで、太字になっている命令がSSE命令だ。また、「xmm0」~「xmm7」というのは、SSEで使用される128ビット長レジスタ(XMMレジスタ)を表している。このコードではSSE命令を使用し、同時に4個のfloate型データの乗算を実行している(float型のサイズは32ビットなので、XMMレジスタに同時に4つを格納できる)。さらに8つのXMMレジスタを活用することで、データの不必要なコピーを最低限に抑え、最小の命令数で必要となる演算を行えるようなコードとなっている。
Visual Studioおよびインテル C++ Composer XE 2011でアセンブラコードを出力させるには、「/FAs」コンパイルオプションを使用する。このオプション付きでコンパイルを行うと、バイナリファイルの出力先と同じディレクトリにアセンブラコードが出力される。このオプションで出力されるアセンブラファイルの拡張子は「.asm」で、アセンブラコードのコメントとして対応するC/C++ソースコードが記述された、解析しやすいアセンブラコードが生成される。
なお、この設定はVisual Studio中ではプロジェクトのプロパティページ中「Output Files」(出力ファイル)の「Assembler Output」(アセンブリの出力)で行える(*図A)。
いっぽう、同様のコードをVisual Studio 2010で「最大限の最適化」設定(コンパイルオプションは/Ox)でコンパイルして生成されたアセンブラコードは、リスト3のようになる。
; 142 : d = 0.0; fldz ; 143 : for (i = 0; i 128; i++) { xor eax, eax fstp DWORD PTR _d$[esp+1088] npad 2 $LL3@main: ; 144 : d += a[i] * b[i]; fld DWORD PTR _b$[esp+eax+1088] add eax, 32 fmul DWORD PTR _a$[esp+eax+1056] fadd DWORD PTR _d$[esp+1088] fstp DWORD PTR tv1066[esp+1088] fld DWORD PTR tv1066[esp+1088] fld DWORD PTR _a$[esp+eax+1060] fmul DWORD PTR _b$[esp+eax+1060] faddp ST(1), ST(0) fstp DWORD PTR tv1063[esp+1088] fld DWORD PTR tv1063[esp+1088] fld DWORD PTR _a$[esp+eax+1064] fmul DWORD PTR _b$[esp+eax+1064] faddp ST(1), ST(0) fstp DWORD PTR tv1060[esp+1088] fld DWORD PTR tv1060[esp+1088] fld DWORD PTR _a$[esp+eax+1068] fmul DWORD PTR _b$[esp+eax+1068] faddp ST(1), ST(0) fstp DWORD PTR tv1057[esp+1088] fld DWORD PTR tv1057[esp+1088] fld DWORD PTR _a$[esp+eax+1072] fmul DWORD PTR _b$[esp+eax+1072] faddp ST(1), ST(0) fstp DWORD PTR tv1054[esp+1088] fld DWORD PTR tv1054[esp+1088] fld DWORD PTR _a$[esp+eax+1076] fmul DWORD PTR _b$[esp+eax+1076] faddp ST(1), ST(0) fstp DWORD PTR tv1051[esp+1088] fld DWORD PTR tv1051[esp+1088] fld DWORD PTR _a$[esp+eax+1080] fmul DWORD PTR _b$[esp+eax+1080] faddp ST(1), ST(0) fstp DWORD PTR tv1048[esp+1088] fld DWORD PTR tv1048[esp+1088] fld DWORD PTR _a$[esp+eax+1084] fmul DWORD PTR _b$[esp+eax+1084] faddp ST(1), ST(0) fstp DWORD PTR _d$[esp+1088] cmp eax, 512 jl $LL3@main ; 145 : } ; 146 : ; 147 : printf("%f\n", d); fld DWORD PTR _d$[esp+1088] sub esp, 8 fstp QWORD PTR [esp] push OFFSET $SG5712 call _printf
中で用いられている「fld」や「fstp」、「fmul」、「fadd」などはCPUのFPU(浮動小数点処理ユニット)に対しデータをストア/ロードしたり、乗算、加算などを行う命令だ。このように、Visual Studio 2010のデフォルト設定で生成したコードは、FPUを使用して処理を実行していることがうかがえる。それぞれのコードの実際の処理時間であるが、上記のコードをループさせて100万回実行させるプログラムでその実行時間を測定した結果、次の表4のようになった。
コンパイラ | 実行時間 |
---|---|
インテル C++ Composer XE 2011 | 9.325秒 |
Visual Studio 2008 | 9.722秒 |
ここで使用しているコードはシンプルであるため軽微な違いしか見られないものの、SSEを利用することによるパフォーマンス向上が確認できる。
[PageInfo]
LastUpdate: 2011-01-11 18:51:52, ModifiedBy: hiromichi-m
[Permissions]
view:all, edit:login users, delete/config:members