アセンブラコードで見るC++ Composer XEの強力な最適化機能 4ページ

自動的にマルチスレッド化されたコードを出力する「自動並列化」

 近年ではマルチコアCPUの普及が進んでいる。そのような状況で、処理を複数のスレッドに分割して実行する並列処理はパフォーマンス向上に効果的である。一般的にはループで複数繰り返される処理を並列化することが多いが、インテル C++ Composer XE 2011には、このような処理を自動的に並列化する自動並列化機能が搭載されている。

 たとえば、次のリスト6は自動並列化が適用されるコードの例だ。

リスト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)の実行順序を入れ替えたりしても、結果は不変である。

リスト7 リスト6のループは分割可能

  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)。

リスト8 自動並列化が行われたアセンブラコード:スレッドの作成部

;;; 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

リスト9 「__kmpc_ok_to_fork」の戻り値が0の際に実行されるコード(抜粋)

.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
 :
 :
 :

リスト10 「__kmpc_fork_call」の引数として渡されるアドレス以下のコード(抜粋)

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)。

表5 自動並列化による実行時間の変化
自動並列化の有無 実行時間(3回の平均) バイナリサイズ
自動並列化有効 3.701秒 73288バイト
自動並列化無効 7.238秒 12288バイト

 なお、自動並列化により生成されたバイナリはスレッド生成など並列化のためのコードも含むため、バイナリサイズが大きくなる傾向があるようだ。