レベル: 中級 浅原 明広 (asahara@fixstars.com), 株式会社FIXSTARS 技術開発本部長 / AKD48
2008年 9月 19日 前回の記事では、Cell/B.E.用のIBM XLCコンパイラには、"XL C/C++ Multi-core Acceleration for Linux version 9.0"(XLC MA)と、"XL C/C++ Alpha Edition Single Source Compiler version 0.9"(XLC SSC)の2種類あることを述べ、両者の簡単な使用方法を説明しました。第二回となるこの記事では、XLC SSCに焦点をあて、Open MPコードの基本的な書き方について説明し、行列の掛け算を題材にしてデータサイズと並列化効率の関係性について調べてみます。
前回の記事に対するフォロー
こんにちは。風の音にも秋の訪れを感じるようになりましたが、ご機嫌いかがでしょうか?「XLCで行こう」の第二回目です。ありがたいことに 前回の記事公開後、読者の皆さんからいくつかのお問い合わせがありました。そこで今回は、まず最初にその質問への回答を兼ねて、前回の記事に対するフォローから始めたいと思います。
1. XLC SSCに関しての詳しい資料は無いのか?
残念ですが、現状ではXLC SSCに関する公式資料は"Using the single-source compiler"のみで、IBMから公開されている他の資料はありません。"Using ..."も十分な内容を含んでいるとはとても言えないので、今後改善していきたいと考えてます。本記事が皆様の理解の助けになれば幸いです。
2. OpenMPで記述したコードをOpenMP非対応コンパイラにかけるとエラーになってしまう。
前回の記事でも述べたとおり、OpenMPでは、並列用、逐次用、2つのソースコードを用意することなく、コンパイラがOpenMPに対応していなければ通常のシーケンシャル処理を行う実行形式を生成し、OpenMP対応コンパイラであれば並列化された高速な実行形式を生成します。ただし、OpenMP特有の omp_set_num_threads, omp_get_thread_num などの関数がソースに含まれている場合は、OpenMP非対応のコンパイラではエラーになってしまいます。これを回避するためには、_OPENMPマクロを使って非OpenMPコンパイラでの関数定義を書いておけばよいのです。例として、以下のリスト 1, 2 に、円周率を近似的に求めるためのコードと、そのMakefileを示します。この2ファイルを同一ディレクトリに置いてmake コマンドを打てば、シングルスレッド(sample_pi_ma) とマルチスレッド(sample_pi_ssc) の2つの実行形式ができるはずです。
リスト 1. OpenMP非対応コンパイラも考慮したコード
001 #include <stdio.h>
002 #include <stdlib.h>
003 #ifdef _OPENMP
004 #include <omp.h>
005 #else
006 #define omp_get_num_threads() 0
007 #define omp_set_num_threads(x) x
008 #endif
009
010 #define TH_NUM 4
011 #define STEP 100000000
012
013 int main()
014 {
015 int i,total_th;
016 double x,pi,step;
017 double sum=0.0;
018
019 step = 1.0/(double) STEP;
020 omp_set_num_threads(TH_NUM);
021
022 #pragma omp parallel private(x)
023 {
024 total_th = omp_get_num_threads();
025 #pragma omp for reduction(+:sum)
026 for(i=0;i < STEP;i++){
027 x = (i+0.5)*step;
028 sum = sum + 4.0/(1.0+x*x);
029 }
030 }
031 pi = step * sum;
032 printf("Total Threads = %d. PI = %lf \n",total_th,pi);
033
034 return (0);
035 }
|
リスト 2. 上記コードのためのMakefile
001 XLC_SSC=/opt/ibmcmp/xlc/ssc/0.9/bin/cbexlc
002 SSC_FLAGS=-O3 -qarch=cell -qtune=cell
003
004 XLC_MA=/opt/ibmcmp/xlc/cbe/9.0/bin/ppuxlc
005 MA_FLAGS=-O3 -qarch=cellppu -qtune=cellppu
006
007 all: sample_pi_ssc sample_pi_ma
008
009 sample_pi_ssc: sample_pi.c
010 $(XLC_SSC) $(SSC_FLAGS) -o $@ $> $(INCLUDE) $(CLIB)
011 sample_pi_ma: sample_pi.c
012 $(XLC_MA) $(MA_FLAGS) -o $@ $> $(INCLUDE) $(CLIB)
013 clean:
014 rm -f sample_pi_ssc sample_pi_ma *.o *.d
|
3. 並列化領域でCell/B.E.の組み込み関数が使えないのか?
おそらくすでに試された方も多いと思いますが、現在のバージョンでは #pragma によって規定された並列化領域内部では、PPU AltiVec 関数であるvec_addや、SPU SIMD 関数である、spu_add など、Cell/B.E. 組み込み関数を使うことができず、コンパイルエラーとなります。残念ながら。。。
OpenMP 入門
それでは「XLCで行こう!」第二回を始めましょう。まずは前回全く説明せずにいたOpenMPそのものについて説明します。ただし、本記事はOpenMPの解説記事ではなく、あくまでもXLC Single Source Compiler の記事ですから、ここでは基本的な解説のみに留めます。より詳しく知りたい方は書籍等をご利用下さい。文末の「参考文献」にOpenMPに関する代表的な書籍を挙げておきました。
まずOpenMPの解説を兼ねて、上記 "リスト 1. 円周率を求めるコード" を行順に見ていきましょう。
* 4行目 #include < omp.h >
"omp実行時ライブラリ関数"を使うために必要なインクルードファイルです。よく使用する"omp実行時ライブラリ関数"には以下の種類があります。
- void omp_set_num_threads(int num_threads);
この関数の呼び出し以降の並列領域におけるスレッド数を規定します。この関数は、コードの逐次実行部分から呼び出された場合にのみ有効です。
- int omp_get_max_threads(void);
この関数が呼ばれた時点で並列領域のスレッド数として設定されている値を返します。並列領域でのスレッド数は通常、1. 上記omp_set_num_threads関数で設定された値、2. 関数による設定が無い場合、環境変数OMP_NUM_THREADSの値、3. さらに環境変数の定義が無い場合、システムで利用可能なプロセッサ数が自動的に設定されます。
- int omp_get_num_threads(void);
現在並列領域で生成されているスレッドの数を返します。
- int omp_get_thread_num(void);
この関数を実行したスレッドのスレッド番号を返します。スレッド番号は0から"omp_get_num_threads()が返す値 - 1" までの値になります。通常並列領域で使用しますが、逐次領域で呼んだ場合はマスタースレッドを表すスレッド番号0を返します。
リスト 1. では、20行目、24行目でomp実行時ライブラリ関数を使っています。もちろん、これらの関数を使用しない場合は、omp.hをインクルードする必要ありません。
* 22行目 #pragma omp parallel private(x)
#pragma omp parallelは、この行に続くブロックを並列領域とする指示文です。OpenMPにおける指示文はこのように、#pragma で記述され、そのため、OpenMP非対応コンパイラでは単に無視されます。ここでいう、"ブロック"というのは、C言語では"{}"で囲まれる領域になります。
#pragma omp paralle 指示文には、細かい制御をするための節(Clause) を付けることができます。22行目の例ではprivate(x) という節が付属していますが、これは変数xを各スレッド固有の変数として使用する、という意味になります。その他、parallel指示文でよく使用される節をいくつか挙げておきます。
private(変数名)
この指定を受けた変数は、各スレッドのローカル変数となり、並列領域の開始時に生成され、終了時に破棄されます。複数個の変数を指定する場合はカンマで区切ります。
shared(変数名)
この指定を受けた変数は、スレッド間共通の変数となります。
default(none)
実は#pragma omp parallel 節では特に上記private, shared 節をつけない場合のデフォルトが暗黙で規定されています。暗黙の規定ではループのインデックスとなっている変数(今回のサンプルの場合、26行目でループのインデックスとなっている i ) はプライベート、その他の変数はすべて共有変数とみなされます。そのようなデフォルトの定義が気に入らない場合、"default none" をつけて、プライベート変数、共有変数を全て自分で指定します。例えば、リスト 1. の22行目は、すべての変数を自分で定義する場合は以下のようになります。
#pragma omp parallel default(none) private(x,i) shared(step,total_th,sum)
|
不必要な共有変数は予期せぬ性能低下を引き起こしますので、なるべくdefault(none)を使い、private, sharedの各変数設定を自分でやった方がよいと思います。
firstprivate(変数名)
この指定を受けた変数が、各スレッドのローカル変数となるところまではprivate節となんら変わりませんが、firstprivate節の場合、並列化直前の変数の値が各スレッドに渡されます。逆にいえば、通常のprivate節では各スレッドに渡ったprivate変数の初期値は未定義ですので注意が必要です。
* 25行目 #pragma omp for reduction(+:sum)
25行目はワークシェアリング構文#pragma omp forと、それに続く節reduction(+:sum)から成っています。
ワークシェアリング構文#pragma omp forは、Forループをスレッドで並列化処理するための構文です。22行目ですでに並列化の指示を行っていますが、並列領域内の演算は通常各スレッドで全く同じものが実行されます。つまり、もし#pragma omp for文が無かったとすると、並列化された各スレッドが同じ数のループをこなすことになり、全く速度の向上は得られません。今、我々が期待しているのは、ループの数をスレッドで分割し、処理することです。そこで、#pragma omp forの出番です。この構文を使用すれば、ループ処理に関して、例えばStep数が100、スレッド数が4の場合、25ループずつ各スレッド分割されて計算されます。
reduction(+:sum)節は、対象変数の各スレッドにおけるローカルな計算結果を一つの値にまとめる働きをします。「まとめ方」には、和"+", 積"*", 差"-"などがあり、他にもreduction対象変数が整数の場合、論理演算(& , | , ^ , など)が可能です。本サンプルでは、reduction 対象変数sum について、各スレッドで局所的に計算したあと、最後に局所和を足し合わせていることになります。上記サンプルコードで、もしこのreduction節を付け忘れていたらどうなってしまうでしょうか?是非手を動かして確認していただきたいと思いますが、reduction節が無い場合はsumはデフォルトでは共有変数とみなされます。各スレッドが非同期にsum の値の読み出し、書き込みを行うため、全くおかしな値になってしまいます。
以上、OpenMPの基本的な部分をサンプルコードを題材にして説明しました。このコードの流れを図示すると、図 1. のようになります(TH_NUM=4, STEP=100の場合)。
図 1.サンプルコードの概念図
データサイズと並列化効率
さて、OpenMPの基本事項が理解できたところで、簡単なテストプログラムを使ってXLC SSCの特性をいろいろと調べてみましょう。プログラムは、行列とベクトルの掛け算を取り上げます。リスト 3, 4 をご覧下さい。
リスト 3. 行列とベクトルの掛け算
001 //
002 // Matrix (b) times Vector (c) product
003 //
004 #include <stdio.h>
005 #include <stdlib.h>
006 #include <sys/time.h>
007 #include <malloc_align.h>
008 #include <free_align.h>
009 #ifdef _OPENMP
010 #include <omp.h>
011 #else
012 #define omp_get_num_threads() 0
013 #define omp_get_max_threads() 0
014 #define omp_set_num_threads(x) x
015 #endif
016
017 struct timeval tv1, tv2;
018 int elapsed_microsecs;
019 float elapsed_time;
020
021 int main(int argc, char *argv[])
022 {
023 int i,j,m,n;
024 float *a;
025 float *b;
026 float *c;
027 int thread_num;
028
029 if(argc!=3){
030 printf("usage: >overhead <thread num> <matrix size>\n");
031 return 1;
032 }
033
034 thread_num = atoi(argv[1]);
035 omp_set_num_threads(thread_num);
036 thread_num = omp_get_max_threads();
037
038 m = n = atoi(argv[2]);
039 //printf("Number of threads: %d, Matrix size: %dx%d\n",thread_num,m,n);
040
041 a = (float *)_malloc_align(sizeof(float)*m,7);
042 b = (float *)_malloc_align(sizeof(float)*m*n,7);
043 c = (float *)_malloc_align(sizeof(float)*m,7);
044
045 // Initialize
046
047 srand(111);
048
049 for (i=0; i<m; i++) {
050 c[i] = ((float)rand()/(RAND_MAX + 1.0)*2.0 - 1.0);
051 for (j=0; j<n; j++) {
052 b[j+i*n] = ((float)rand()/(RAND_MAX + 1.0)*2.0 - 1.0);
053 }
054 }
055
056 gettimeofday(&tv1,NULL);
057
058 #pragma omp parallel for default(none) private(i,j) shared(m,n,a,b,c)
059 for (i=0; i<m; i++) {
060 a[i] = b[i*n] * c[0];
061 for(j=1;j<n;j++){
062 a[i] += b[i*n+j] *c[j];
063 }
064 }
065
066 gettimeofday(&tv2,NULL);
067
068 elapsed_microsecs =
069 (int)(tv2.tv_sec - tv1.tv_sec) * 1000000 +
070 (int)(tv2.tv_usec - tv1.tv_usec);
071 elapsed_time = (float) elapsed_microsecs * 0.000001;
072
073 printf("Elapsed_time = %lf\n",elapsed_time);
074
075
076 _free_align(a);
077 _free_align(b);
078 _free_align(c);
079
080 return (0);
081 }
|
リスト 4. 上記テストコードのMakefile
001 XLC_SSC=/opt/ibmcmp/xlc/ssc/0.9/bin/cbexlc
002 SSC_FLAGS=-O3 -qarch=cell -qtune=cell
003
004 XLC_MA=/opt/ibmcmp/xlc/cbe/9.0/bin/ppuxlc
005 MA_FLAGS=-O3 -qarch=cellppu -qtune=cellppu
006
007 INCLUDE = -I /opt/cell/sdk/usr/include
008
009 all: overhead_test_ssc overhead_test_ma
010
011 overhead_test_ssc: overhead_test.c
012 $(XLC_SSC) $(SSC_FLAGS) -o $@ $< $(INCLUDE) $(CLIB)
013 overhead_test_ma: overhead_test.c
014 $(XLC_MA) $(MA_FLAGS) -o $@ $< $(INCLUDE) $(CLIB)
015 clean:
016 rm -f overhead_test_ssc overhead_test_ma *.o *.d
|
このプログラムはコマンドライン引数からスレッドの数(thread_num)、行列のサイズ(m)を入力し、大きさm×m の行列bと、大きさmのベクトルaとの掛け算を行うものです。簡単の為、行列はm=nの正方行列にしています。以下でソースコードを順に見ていきましょう。
* 35行目 omp_set_num_threads(thread_num);
OpenMP並列領域でのスレッド数を引数から与えられたthread_numにセットしています。
* 41行目 a = (float *)_malloc_align(sizeof(float)*m,7);
配列の大きさは引数で与えられるので、プログラム内部で動的にメモリ領域を確保しています。XLC SSCでは、明示的にベクトル演算を行うことはできませんが、SPUによるSIMD演算が効率よく行われるように、通常のmallocではなく、Cell/B.E. SDKに入っているmalloc_align.h でマクロ定義されている、_malloc_align を使います。
* 58行目 #pragma omp parallel for default(none) private(i,j) shared(m,n,a,b,c)
この指示文以下のブロックが並列領域になります。i,jをプライベート変数、m,n,a,b,cを共有変数にしています。
リスト 1. の円周率のコードでは、#pragma omp parallel で並列領域の定義を行い、#pragma omp for で、for文の分割処理を指示しました。この2つの指示文はまとめて結合ワークシェアリング構文を#pragma omp parallel for使って簡潔に記述することが可能です。
* 76行目 _free_align(a);
領域の開放です。free_align.hで定義されている、アラインされた領域の開放のためのマクロを使っています。
どうでしょうか?前節を読んだ後であれば、すんなりと理解できるソースコードだと思います。Makefileの方には特にコメントはありませんが、_malloc_align, _free_align を使うために、インクルードファイルの置き場所として/opt/cell/sdk/usr/include ディレクトリを指定していますので、注意してください。
では、まず最初にOpenMP指示文を使用することで生じる並列化のオーバーヘッドが、Cell/B.E. + XLC SSCの場合どのぐらい大きいのか調べていきたいと思います。spe_context_create → pthread_create のオーバーヘッドは非常に大きく、そのため、通常のCell/B.E.プログラミングでは、なるべくスレッドの生成、終了を行わないようにデザインすることが重要なTipsとなっています。XLC SSCでは、そのあたりがプログラマから完全に隠されていますが、はたしてどのぐらいのオーバーヘッドになるのか、気になるところです。
評価の指標として、Using OpenMP: Portable Shared Memory Parallel Programming p158 (5.3) 式を使ってみます。
この式はつまり、"OpenMP非対応コンパイラ(我々の場合、XLC MA)でコンパイルし、実行した場合の計算時間" に対する、"OpenMP対応コンパイラ(我々の場合、XLC SSC)でコンパイルし、Thread数1で実行した場合の計算時間" の増加の割合を求めています。これにより、OpenMP並列化のための準備作業にどのぐらいのオーバーヘッドがあるのかをみるわけです。ここで、行列のサイズを10x10(=1kb)~15000x15000(=900MB)に変えながら上式の値を求めて図示したものが、図 2 です。横軸は対数目盛りです。
図 2. 行列のサイズよるオーバーヘッド値の変化
XLC SSCの結果だけですと、果たして結果が良いのか悪いのかわかりませんから、比較のためにIBM BladeCenter HS21(Dual Core Xeon 3.2GHz x 2, Memory 2GB)で、GNU GCCコンパイラで同様の測定をし、重ねてみました。0.01MB以下のMemory Footprint では、Xeon + GCC に比べCell/B.E.+XLC SSCのオーバーヘッドは大きいようです。ただし、図からは、Memory Footprintが0.1MB以上になると、並列化によるオーバーヘッドはほとんど無視できることがわかります。ちなみに、0.1MB以上でオーバーヘッドが無視できるという傾向は、Using OpenMP: Portable Shared Memory Parallel Programmingに掲載されている、Dual Core UltraSPARC IV processor x 24 のSMPシステムでも見えています。XLC SSCに限らず、OpenMPを使う際はこのあたりに注意する必要がありそうです。
なお、Cell/B.E.の結果で、1000MB付近のオーバーヘッドが大きくなっている理由、また、1MB~100MBの領域でややオーバーヘッドが大きい理由は明確にはわかっておりませんが、測定したプラットフォームがIBM BladeCenter QS20であったため、メインメモリが1GBと小さく、大きな配列ではスワップが発生しているのが関係しているかもしれません。QS21やQS22であれば、異なる結果になる可能性があります。
まとめ
今回はOpenMPに関する基本的事項の解説と、計算量の変化に応じて並列化のオーバーヘッドがどのように変化するかを実際に調べてみました。結論として、演算のメモリサイズが0.1MB以上であれば、効率よく並列化できることがわかりました。
次回の「XLCで行こう!」第三回では、引き続きリスト 3.のサンプルコードを使って、もうすこし詳しくXLC SSCの特性を調べて行きたいと思っています。
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | 
|  | 浅原明広は、Cell/B.E.関連の開発業務に携わっているエンジニアですが、ここ一年ぐらいはプロジェクトマネージメントを主な業務としているため、コードを書く機会がめっきり減ってしまいました。というわけで、このDeveloper Worksのための検証コード作りは、私にとって結構貴重な時間になっています。ちなみに文章とは関係ありませんが、写真は料理中のものです。中華料理が得意です。 |
記事の評価
|