![]() |
HPC/並列プログラミングポータルでは、HPC(High Performance Computing)プログラミングや並列プログラミングに関する情報を集積・発信しています。 |
[記事一覧を見る]
インテル C++ Composer XEには、強力な最適化機能を備えるコンパイラが含まれている。インテルCPUが備えるSSEやAVXといった機能を効率的に利用するコードや、マルチコアによる並列処理を行うコードを自動的に生成できるのが特徴だ。本記事ではインテル C++ Composer XEが持つさまざまな最適化機能を紹介するとともに、コンパイラが出力するアセンブラコードをチェックしてその効果を探っていく。
近年のCPUの進化に伴い、コンパイラの最適化機能が注目されるようになってきた。パイプライン化やキャッシュの大容量化、分岐予測や投機的な実行といった機能の採用など、CPUの内部アーキテクチャは進化を続けている。そのため、コンパイラにはCPUのリソースを効率良く利用できるバイナリコードを生成することが求められるようになっている。
現在、WindowsやLinuxで利用できるコンパイラは商用のものからフリーのものまでさまざまなものがあるが、その中でもインテルCPU向けに高パフォーマンスのコードを生成することで定評があるのがインテルの「インテル C++ Composer XE 2011」(旧製品名:「インテル C++ コンパイラー」)だ(製品ラインアップについては『高度な最適化機能や並列プログラミングサポートを提供する新開発ツール「インテル Parallel Studio XE 2011」』で紹介している)。Oracleのデータベース製品のビルドなどにも採用されるなど、パフォーマンスが求められるアプリケーションでの採用実績も多い。
しかし、コンパイラによるパフォーマンスの違いについては、一般にはあまり気にされていないのが現状だろう。一般的なデスクトップアプリケーションの分野ではCPUによる処理時間よりも各種I/O処理時間やユーザーの入力待ち時間が多いため、コンパイラを変更することによるパフォーマンス向上はあまり感じられない。しかし、たとえば大規模な数値演算を行うような科学・技術計算やシミュレーション、大容量の動画や音声、画像のエンコード/デコードなどを行うマルチメディアアプリケーションなどの分野では、コンパイラを変えるだけで数パーセントから十数パーセントものパフォーマンス向上が見られることもある。
たとえば次の図1は、行列計算や画像処理、ファイルの圧縮/展開(解凍)を行うプログラムを、Visual Studio 2010とインテル C++ Composer XE 2011でそれぞれコンパイルし、その実行時間をVisual Studio 2010を100%として比較したものだ。測定条件の詳細については表1、2にまとめているが、インテル C++ Composer XE 2011の利用により、以下の条件ではおおむね5~10%程度実行時間が短縮されるという結果となった。
プログラム | 説明 |
---|---|
bzip2(展開) | bzip2(http://bzip.org/)により、Firefox 3.6.12のソースコード(firefox-3.6.12.source.tar.bz2)を展開する際の処理時間を測定 |
bzip2(圧縮) | bzip2により、Firefox 3.6.12のソースコード(firefox-3.6.12.source.tar.bz2)を圧縮する際の処理時間を測定 |
画像処理 | 1024×768サイズの画像に対し、15×15の平均化フィルタを適用する時間を測定 |
行列演算 | double型の500×500行列の10乗を計算する時間を測定 |
項目 | 条件 |
---|---|
CPU | Core 2 Duo E6550(2.33GHz) |
メモリ | 2GB |
インテル C++ Composer XE 2011のコンパイルオプション | /O3 /Oi /Og /GA /Quse-intel-optimized-headers |
Visual Studio 2010のコンパイルオプション | /Ox /Oi /GL |
※特に表記のない限り、以下のテストやパフォーマンス測定でもこの環境を使用している このようにパフォーマンスの高いプログラムを生み出せるインテル C++ Composer XEであるが、実際にどのように最適化を行っているのかは、そのスペックだけからはあまり知ることはできない。そこで本記事では、インテル C++ Composer XEが出力するアセンブラコードをチェックしながら、その最適化機能について紹介していく。
インテル 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を利用することによるパフォーマンス向上が確認できる。
さて、ここではSSEを使用した場合と使用しない場合でのパフォーマンス差をチェックしたが、実はVisual Studioでもオプション設定を行うことでSSEを利用するコードを出力できる。Visual Studio 2005/2008ではMMX/SSE/SSE2命令を使用した最適化が、Visual Studio 2010ではAVXを使用した最適化がサポートされている。たとえばSSE2を利用する場合、「/arch:sse2」オプションを使用すればよい。実際にVisual Studio 2010でSSE2を利用するように設定して生成したアセンブラコードが次のリスト4だ。
; 142 : d = 0.0; xorps xmm0, xmm0 ; 143 : for (i = 0; i 128; i++) { xor eax, eax npad 3 $LL3@main: ; 144 : d += a[i] * b[i]; movss xmm1, DWORD PTR _b$[esp+eax+1088] movss xmm2, DWORD PTR _a$[esp+eax+1088] cvtps2pd xmm1, xmm1 cvtss2sd xmm0, xmm0 cvtps2pd xmm2, xmm2 mulsd xmm1, xmm2 addsd xmm1, xmm0 movss xmm2, DWORD PTR _b$[esp+eax+1092] xorps xmm0, xmm0 cvtpd2ps xmm0, xmm1 movss xmm1, DWORD PTR _a$[esp+eax+1092] cvtps2pd xmm0, xmm0 cvtps2pd xmm1, xmm1 cvtps2pd xmm2, xmm2 mulsd xmm1, xmm2 addsd xmm0, xmm1 movss xmm1, DWORD PTR _a$[esp+eax+1096] movss xmm2, DWORD PTR _b$[esp+eax+1096] cvtpd2ps xmm0, xmm0 cvtss2sd xmm0, xmm0 cvtps2pd xmm1, xmm1 cvtps2pd xmm2, xmm2 mulsd xmm1, xmm2 movss xmm2, DWORD PTR _b$[esp+eax+1100] addsd xmm0, xmm1 movss xmm1, DWORD PTR _a$[esp+eax+1100] cvtpd2ps xmm0, xmm0 cvtss2sd xmm0, xmm0 cvtps2pd xmm1, xmm1 cvtps2pd xmm2, xmm2 mulsd xmm1, xmm2 movss xmm2, DWORD PTR _b$[esp+eax+1104] addsd xmm0, xmm1 movss xmm1, DWORD PTR _a$[esp+eax+1104] cvtpd2ps xmm0, xmm0 cvtss2sd xmm0, xmm0 cvtps2pd xmm1, xmm1 cvtps2pd xmm2, xmm2 mulsd xmm1, xmm2 movss xmm2, DWORD PTR _b$[esp+eax+1108] addsd xmm0, xmm1 movss xmm1, DWORD PTR _a$[esp+eax+1108] cvtpd2ps xmm0, xmm0 cvtss2sd xmm0, xmm0 cvtps2pd xmm1, xmm1 cvtps2pd xmm2, xmm2 mulsd xmm1, xmm2 movss xmm2, DWORD PTR _b$[esp+eax+1112] addsd xmm0, xmm1 movss xmm1, DWORD PTR _a$[esp+eax+1112] cvtpd2ps xmm0, xmm0 cvtss2sd xmm0, xmm0 cvtps2pd xmm1, xmm1 cvtps2pd xmm2, xmm2 mulsd xmm1, xmm2 movss xmm2, DWORD PTR _b$[esp+eax+1116] addsd xmm0, xmm1 movss xmm1, DWORD PTR _a$[esp+eax+1116] cvtpd2ps xmm0, xmm0 cvtss2sd xmm0, xmm0 cvtps2pd xmm1, xmm1 cvtps2pd xmm2, xmm2 add eax, 32 mulsd xmm1, xmm2 addsd xmm0, xmm1 cvtpd2ps xmm0, xmm0 cmp eax, 512 jl $LL3@main ; 145 : } ; 146 : ; 147 : printf("%f\n", d); sub esp, 8 cvtss2sd xmm0, xmm0 movsd QWORD PTR [esp], xmm0 push OFFSET $SG5712 call _printf
こちらの場合、「xmm0」~「xmm2」があることからも分かるとおり、確かにSSEを利用したコードが出力されている(太字がSSE命令)。ただし、インテル C++ Composer XE 2011で生成されたコードとは異なり、乗算/加算にはパックド命令は使用されておらず、同時に1つのデータしか処理していない。実行時間も9.800秒と、SSE2を利用しない場合よりもわずかではあるが劣る結果となった。
なお、Visual StudioではSSE/SSE2、およびインテル AVXのみがサポートされているが、インテル C++ Composer XE 2011ではSSE2/SSE3/SSSE3/SSE4.1/SSE4.2およびインテル AVXがサポートされている。インテル AVXが実装されたCPUはまだリリースされていないものの、コードの生成自体は可能だ。実際にリスト1のコードをインテル AVX対応CPU向けの設定でコンパイルして生成されたアセンブラコードが次のリスト5である。
;;; d = 0.0; ;;; for (i = 0; i 128; i++) { ;;; d += a[i] * b[i]; lea eax, DWORD PTR [1184+esp] and eax, 31 mov DWORD PTR [128+esp], edi mov esi, eax mov edi, edx .B1.7: mov ecx, esi .B1.12: vxorps ymm0, ymm0, ymm0 vxorpd ymm1, ymm1, ymm1 .B1.13: vmovupd xmm2, XMMWORD PTR [136+esp+ecx*8] vmovupd xmm5, XMMWORD PTR [168+esp+ecx*8] vinsertf128 ymm3, ymm2, XMMWORD PTR [152+esp+ecx*8], 1 vinsertf128 ymm6, ymm5, XMMWORD PTR [184+esp+ecx*8], 1 vmulpd ymm4, ymm3, YMMWORD PTR [1184+esp+ecx*8] vmulpd ymm7, ymm6, YMMWORD PTR [1216+esp+ecx*8] vmovupd xmm2, XMMWORD PTR [200+esp+ecx*8] vmovupd xmm5, XMMWORD PTR [232+esp+ecx*8] vaddpd ymm0, ymm0, ymm4 vaddpd ymm1, ymm1, ymm7 vinsertf128 ymm3, ymm2, XMMWORD PTR [216+esp+ecx*8], 1 vinsertf128 ymm6, ymm5, XMMWORD PTR [248+esp+ecx*8], 1 vmulpd ymm4, ymm3, YMMWORD PTR [1248+esp+ecx*8] vmulpd ymm7, ymm6, YMMWORD PTR [1280+esp+ecx*8] vaddpd ymm0, ymm0, ymm4 vaddpd ymm1, ymm1, ymm7 add ecx, 16 cmp ecx, 128 jb .B1.13 .B1.14: vaddpd ymm0, ymm0, ymm1 vextractf128 xmm1, ymm0, 1 vaddpd xmm2, xmm0, xmm1 vhaddpd xmm3, xmm2, xmm2 .B1.18: ;;; } ;;; ;;; printf("%f\n", d); mov DWORD PTR [esp], OFFSET FLAT: ??_C@_03A@?$CFf?6?$AA@ vmovsd QWORD PTR [4+esp], xmm3 vzeroupper call _printf
アセンブラ中で「v」で始まる命令(リスト中太字の部分)がインテル AVX命令、「ymm0」~「ymm7」はインテル AVXで新たに追加された256ビット長のYMMレジスタである。現在ではAVX対応CPUがないためパフォーマンスは測定できないが、AVX命令を駆使したコードが出力されていることは確認できる。
Linux/Windows環境で広く使われているコンパイラの1つに、GCCがある。最新版のGCC(GCC 4.5.0)ではSSEサポートが行われており、たとえば「-march=core2 -msse4 -mfpmath=sse」といったオプションを指定することで、SSEを使用するコードを出力できる。たとえば次のリストAは、リスト1のコードを上記のオプション付きでGCCでコンパイルして生成したアセンブラコードである。
このコードではxmm0、xmm1レジスタや「movss」「「mulss」「addss」といったSSE命令が使用されていることが分かる。ただし、こちらもVisual Studioの例と同じくパックド命令は使用されておらず、インテル C++ Composer XE 2011のようなベクトル化は行われていない。
L26: movl $0x00000000, %eax movl %eax, 1560(%esp) movl $0, 1564(%esp) jmp L24 L25: movl 1564(%esp), %eax movss 1044(%esp,%eax,4), %xmm1 movl 1564(%esp), %eax movss 532(%esp,%eax,4), %xmm0 mulss %xmm1, %xmm0 movss 1560(%esp), %xmm1 addss %xmm1, %xmm0 movss %xmm0, 1560(%esp) incl 1564(%esp) L24: cmpl $127, 1564(%esp) setle %al testb %al, %al jne L25 cvtss2sd 1560(%esp), %xmm0 movsd %xmm0, 4(%esp) movl $LC1, (%esp) call _printf incl 1556(%esp) L23: cmpl $999999, 1556(%esp) setle %al testb %al, %al jne L26 movl $0, %eax leave
近年ではマルチコアCPUの普及が進んでいる。そのような状況で、処理を複数のスレッドに分割して実行する並列処理はパフォーマンス向上に効果的である。一般的にはループで複数繰り返される処理を並列化することが多いが、インテル C++ Composer XE 2011には、このような処理を自動的に並列化する自動並列化機能が搭載されている。
たとえば、次のリスト6は自動並列化が適用されるコードの例だ。
#define MSIZE 1000 for (i = 0; i MSIZE; i++) { for (j = 0; j MSIZE; j++) { double d = 0.0; int k; for (k = 0; k MSIZE; k++) { ……(1) d += a[i*MSIZE+k] * b[k*MSIZE+j]; } c[i*MSIZE+j] = d; } } printf("%f\n", c[0]);
こちらも科学・工業計算分野で良く用いられる行列演算処理で、行列の積を求める、というものだ。このコードは先に示したリスト1のコードと似ているように見えるが、(1)のループ内で配列aに対しては1要素ずつ順にアクセスが行われるのに対し、配列bについては定数MSIZE(ここでは1000)おきの要素にアクセスが行われる。そのため、リスト1の場合のようなベクトル化は困難である。さらに、MSIZEの値が大きいとキャッシュミスも頻出するため、速度が低下しやすい処理となっている。このような処理の高速化に有効なのが、インテル C++ Composer XE 2011の自動並列化機能である。
リスト6のコードは、(1)の部分をたとえば次のように分割したり、また分割後(2)と(3)の実行順序を入れ替えたりしても、結果は不変である。
for (k = 0; k MSIZE/2; k++) { ……(2) d += a[i*MSIZE+k] * b[k*MSIZE+j]; } for (k = MSIZE/2; k MSIZE; k++) { ……(3) d += a[i*MSIZE+k] * b[k*MSIZE+j]; }
そこで、ループをリスト7のように分割し、(2)と(3)の部分を複数のCPUコアを用いて同時に実行することで、最大で2倍近くの性能向上が期待できる。インテル C++ Composer XE 2011の自動並列化機能を用いれば、特に追加のコードを記述することなしにこのような並列化を行うことが可能だ。
自動並列化はデフォルトでは有効になっていないが、コンパイラオプション「/Qparallel」(Windows版の場合)もしくは「-parallel」(Linux版の場合)を付加するだけで利用できる。これらのオプション付きでコンパイルを実行するとソースコード中で自動並列化可能な個所が検出され、自動的に並列実行されるコートが出力される。たとえばリスト6のコードを自動並列化を有効にしてコンパイルした場合、生成されるアセンブラコードは次のリスト8~10のようになる。
まずリスト8は、並列化を行う準備を行っている部分のコードとなる。ここでは、「__kmpc_ok_to_fork」という関数を呼び出し、その戻り値によって実行する処理を分岐させている。この関数はドキュメント等には記載されていないが、「libiomp5md.dll」というライブラリに含まれているもので、インテル C++ Composer XE 2011でOpenMPを使用する際に用いられるものだ。この関数はその名前から、スレッドの生成を行うか否かを判断しているものと思われる。もし0が帰ってきた場合はラベル「.B1.19:」へとジャンプする。いっぽう0以外が帰ってきた場合は、引数を準備して「__kmpc_fork_call」を実行し、実行完了後ラベル「.B1.35」にジャンプするようになっている。
ちなみに、ラベル「.B1.19」以下のコードと、「__kmpc_fork_call」の引数として渡されるアドレス(ラベル「L__main_102__hpo_threaded_loop0_2.34:」)以下のコードは、どちらもほぼ同様の処理を行っている。アセンブラコードに含まれるコメントから、並列実行が可能な場合と、そうでない場合でと実行するコードを分けているようだ(リスト9、10)。
;;; for (i = 0; i MSIZE; i++) { ; .2.7_2_hpo_loc_struct_pack.36以下に格納されているデータを引数に ; __kmpc_ok_to_fork関数を呼び出す。 ; 戻り値はeaxレジスタに格納される push OFFSET FLAT: .2.7_2_hpo_loc_struct_pack.36 call ___kmpc_ok_to_fork .B1.78: add esp, 4 .B1.17: test eax, eax ; eax == 0かどうかチェック je .B1.19 ; eax == 0なら.B1.19へジャンプ .B1.18: ; 引数を準備して___kmpc_fork_callを呼び出し mov eax, DWORD PTR [144+esp] lea ecx, DWORD PTR [140+esp] mov DWORD PTR [128+esp], eax lea edx, DWORD PTR [128+esp] mov DWORD PTR [132+esp], ebx lea eax, DWORD PTR [132+esp] mov DWORD PTR [136+esp], esi lea edi, DWORD PTR [136+esp] mov DWORD PTR [140+esp], 0 push ecx push edi push eax push edx push OFFSET FLAT: L__main_102__hpo_threaded_loop0_2.34 push 4 push OFFSET FLAT: .2.7_2_hpo_loc_struct_pack.36 call ___kmpc_fork_call .B1.79: add esp, 28 jmp .B1.35
.B1.19: xor eax, eax mov DWORD PTR [136+esp], eax xor edi, edi mov DWORD PTR [128+esp], esi mov DWORD PTR [132+esp], ebx .B1.20: mov eax, DWORD PTR [132+esp] ;;; for (j = 0; j MSIZE; j++) { xor edx, edx mov ecx, DWORD PTR [128+esp] ;;; double d = 0.0; ;;; int k; ;;; for (k = 0; k MSIZE; k++) { mov DWORD PTR [140+esp], edi add eax, edi mov ebx, eax add ecx, edi and ebx, 15 mov esi, ebx and esi, 7 mov DWORD PTR [156+esp], ebx mov DWORD PTR [148+esp], esi mov DWORD PTR [152+esp], ecx : : :
L__main_102__hpo_threaded_loop0_2.34:: .B1.41: push ebp mov ebp, esp : : : .B1.45: mov esi, DWORD PTR [156+esp] ;;; for (j = 0; j MSIZE; j++) { に相当すると思われるコード mov edx, ebx mov eax, DWORD PTR [140+esp] ;;; double d = 0.0; ;;; int k; ;;; for (k = 0; k MSIZE; k++) { ;;; に相当すると思われるコード mov ecx, DWORD PTR [136+esp] add eax, esi add ecx, esi mov esi, eax and esi, 15 mov edi, esi and edi, 7 mov DWORD PTR [172+esp], esi mov DWORD PTR [168+esp], edi mov DWORD PTR [164+esp], ecx : : :
自動並列化の効果であるが、今回の例のように単純に処理を分割できる場合大きな高速化が期待できる。実際、プログラムを実行したところほぼ半分の時間で処理を完了できている(表5)。
自動並列化の有無 | 実行時間(3回の平均) | バイナリサイズ |
---|---|---|
自動並列化有効 | 3.701秒 | 73288バイト |
自動並列化無効 | 7.238秒 | 12288バイト |
なお、自動並列化により生成されたバイナリはスレッド生成など並列化のためのコードも含むため、バイナリサイズが大きくなる傾向があるようだ。
昨今のCPUでは、どのようにメモリアクセスを行うかによって大幅にパフォーマンスが変動する。CPUの動作速度は近年では2~3GHz程度が主流であるが、CPUと外部を接続するバスの速度はその2分の1から4分の1程度であり、さらにメモリの動作速度がそれよりも小さい場合もある。そのため、昨今のCPUでは大容量のキャッシュを搭載し、メモリアクセスを抑えるように設計されている。しかし、それでもキャッシュ容量は数MBというサイズであり、大容量のデータを扱うアプリケーションの場合、メモリへの頻繁なアクセスは避けられない。
インテル C++ Composer XE 2011のハイパフォーマンス並列最適化機能(HPO)は、このようなキャッシュアクセスやメモリアクセスなども考慮し、また可能な限りSSEや並列化などを行えるよう、ループや命令の順序を変更して最適化する機能である。
インテル C++ Composer XE 2011では、最適化レベルとして「Od」および「O1」、「O2」、「O3」が用意されており、「O3」を選択することでループ構造の変更も含めた最大限の最適化が行われるようになる(表6)。
最適化レベル | 説明 |
---|---|
Od | 最適化なし |
O1 | サイズを最小化 |
O2 | 速度を最大化 |
O3 | 速度を最大化+最大限の最適化 |
たとえば次の図2および図3は、冒頭で紹介した画像処理プログラム中で、実際にぼかし処理を行っている個所のコードと、そのアセンブラコードをインテル VTune Amplifier XE 2011で表示させたものだ。左側にCコード、右側に対応するアセンブラコードが表示されているが、最適化レベルO2の場合と、O3との場合ではアセンブラコードが大幅に異なり、O2の場合はCコードと対応するアセンブラコードがほぼ同じ順番で並んでいるのに対し、O3の場合はコードの順番が多くの個所で入れ替わっていることが分かる。
実際、このコード変更により、もっとも内側のループ内の処理の実行時間も変わっており、最適化が有効なことが分かる。
なお、Visual Studio 2010でコンパイルしたコードに対して解析を行った場合は図4のようになった。アセンブラコードが異なるばかりか、時間がかかっている個所自体が異なるものになっていることが分かる。
以上ではインテル C++ Composer XE 2011の最適化機能に焦点を当ててその機能を紹介してきたが、インテル C++ Composer XE 2011では自動的な最適化機能だけでなく、手動によるパフォーマンスチューニングや、プログラムの実装に有用な機能も搭載されている。たとえば「ガイド付き自動並列化」機能は、自動並列化では並列化できないループについて、診断メッセージを表示する機能だ。具体的にどこをどのように修正すれば自動並列化が行えるかが表示される。従来も並列化できないループについてはその旨が表示されたが、ガイド付き自動並列化ではその原因や個所なども表示され、より修正やチューニングが容易になった。
また、Cilk PlusやOpenMP 3.0といった並列化技術もサポートされている。これはそれぞれ別記事(「インテル Parallel Composerの新機能――並列プログラムを容易に実装できる「インテル Cilk Plus」入門」および「ソフトウェア高速化の鍵は「並列化」:いま注目される並列化技術を知る」)で紹介しているためそちらを確認して頂きたいが、これらを用いることでより容易に並列プログラムを実装できるようになる。そのほか、インテル Inspector XEと連携し、プログラムのセキュリティ問題を解析する「スタティック・セキュリティー解析機能」や、プログラム中に実行時の挙動を計測する命令を埋め込み、実行時の挙動を収集してその結果を最適化に反映させる「プロファイルに基づく最適化機能」(PGO)といった機能も搭載されている。
高いパフォーマンスを求められるプログラムが要求される開発者や、並列プログラミングを行っている開発者にとって、インテル C++ Composer XE 2011のこれらの機能は非常に有用だろう。フル機能を30日間無償で利用できる体験版も用意されているので、まずはその機能、性能を試して見てほしい。
[PageInfo]
LastUpdate: 2011-01-11 18:52:09, ModifiedBy: hiromichi-m
[Permissions]
view:all, edit:login users, delete/config:members