IBM XL C/C++ コンパイラ (以下 XLC と略す) は、Blue Gene 用や AIX 用など、システムアーキテクチャ毎にいくつかの種類が存在します。Cell/B.E. 対応版の XLC は、もともと Cell/B.E. SDK2.1 に収録されていたのですが、今回ベータ期間を終えて、めでたく XLC 製品群に統合されました。それが、"XL C/C++ Multicore Acceleration for Linux version 9.0" (以下、XLC MA と略す)です。
XLC MA は PPE 用と SPE 用の 2 つの異なるコンパイラから構成されていて、開発者は PPE 用の C/C++ コードと、SPE 用の C/C++ コードを作成し、対応するコンパイラでビルドすることになります。ビルド後は、PPE コードの中で SPE の実行イメージを Open してあげるか、SPE の実行形式ファイルをライブラリ化してリンクすることになります。この辺りの開発手順は、GCC などの GNU toolchain を使用して開発をした場合と同じですが、XLC の場合 PowerPC プロセッサ向けに最適化されたコードを生成してくれるため、GNU GCC などと比べ同じコードでも処理速度が改善される可能性があります。
一方、XLC MA とは別に、SDK3.0 Extra Package には "XL C/C++ Alpha Edition Single Source Compiler version 0.9" (以下、XLC SSC と略す) というパッケージが収録されています。このコンパイラはその名の通り、1 つのソースコードから、SPE, PPE, の両方を使用する実行形式ファイルを生成してくれるというもので、開発者がソースコードに #pragma 文として埋め込むことで SPE に処理させる部分をコンパイラに指定する OpenMP のプログラミングモデルに準拠しています。元々 OpenMP は、SMP(Shared memory Multi Processer) 構成のシステム向けに策定された並列化手法であり、MPI などと異なり、通常の逐次処理コードと、並列化されたコードを全く同一のコードで管理できるのが魅力です(あとで詳しく説明します)。
まず最初に XLC MA について、説明していきます。XLC MA は製品なので使用には有償のライセンスが必要なのですが、60 日間の無料評価版をこちらの Web ページからダウンロードできます。
利用方法は GNU GCC コンパイラと全く変わりありませんので、Cell/B.E. ユーザーであれば単に Makefile の書き換えのみを行うだけで、XLC MA で最適化されたコードを得ることができます。以下では、XLC 特有の重要なコンパイルオプションについて説明します。
まず一つ目の重要な最適化オプションは High Order Transformations オプション、-qhot です。qhot はループ箇所のハイレベルな最適化を行うオプションであり、書式は次のようになります。
-qhot[=[no]vector|arraypad[=n]|[no]simd]
重要な引数の意味は次の通りです。
- vector | novector
- ループ内の演算を MASS ライブラリ(数学関数ライブラリ) の呼び出しに変換します。ただし、丸め誤差の違いで標準数学関数の場合と結果が異なる可能性があるので注意が必要です。novector を引数で与えると、コードの MASS ライブラリコールへの置き換えを禁止します。
- simd | nosimd
- ループ内の演算で可能な部分をベクトル演算命令に変換します。PPEコードの場合は VMX 命令が、SPE コードの場合は SPU SIMD 命令が呼ばれることになります。nosimd を引数で与えると、ベクトル演算命令への置き換えを禁止します。
- arraypad | noarraypad
- PowerPC プロセッサのキャッシュアーキテクチャでは、2 の累乗の次元を持つ配列についてキャッシュの使用効率が落ちることがあります。このオプションではそのような効率低下を防ぐためにコンパイラが配列の次元を増やします。
arraypadオプションを単独で使った場合は、キャッシュ使用効率があがる可能性のある配列をコンパイラが自動で選択し、次元の増加を行います。arraypad = n(n は整数)などと arraypad 数値を与えることもできます。この場合コンパイラはコード内部の全ての配列に対して、あたえられた整数n を要素数とした配列の拡張 (Padding) を行います。整数値はソースコードの配列の中で最も大きな要素サイズの倍数 (たとえば、4、8、16など) に設定するとよいでしょう。
noarraypad を引数であたえると、上記のようなPaddingを行いません。
- level=0
-
-qhot=novector:nosimd:noarraypad
と同じです。
- level=1
-
-
-qhot -
-qhot=vector:simd
-
qhotオプションのPragma制御
ソースコード中に #pragma nosimd, #pragma novector 文を埋め込むことで、特定のループ毎に -qhot=vector と -qhot=simd の効果を消すことができます。例を見たほうが理解が早いでしょう。以下の例では、SIMD 化したくないループブロックに関して pramga を使っています。
...
#pragma nosimd
for(i=1;i<1000;i++){
a[i] = b[i] * c[i]
}
...
|
特定のアーキテクチャーに依存したコードを生成し、高速化します。-qarch は命令の生成対象となるアーキテクチャーを指定し、-qtune はコードの最適化対象とするターゲット・プラットフォームを指定する、ということになっていて、-qarch を指定していない時の-qtune の動作や、-qtune を指定していない時の -qarch の動作など、いろいろ規定はあるのですが、個々のケースに対応するのは大変なので、私はいつもアーキテクチャ依存最適化を行う際は両方指定しています。指定できるアーキテクチャは、XLC MA の場合、
- -qarch=auto -qtune=auto (ターゲットを自動判別)
- -qarch=cellppu -qtune=cellppu (PPUをターゲットとしてコンパイル)
- -qarch=cellspu -qtune=cellspu (SPUをターゲットとしてコンパイル)
- -qarch=edp -qtune=edp (PowerXCell SPU をターゲットとしてコンパイル)
の 4 種類です。このうち、auto は、現在コンパイルしている環境が実行環境と思ってアーキテクチャを自動で決定してくれます。そのため、x86 系システムの上で Cell/B.E. の開発を行うようなクロスコンパイル環境で開発をする場合は注意して下さい。明示的にアーキテクチャ名を指定する方がよいでしょう。
GNU GCC コンパイラでもおなじみの最適化オプションです。XLC MA では、-qnoopt, -O2, -O3, -O4, -O5, の 5 段階の最適化レベルを持っています。段階があがるにつれて、生成されるコードの高速化が期待できますが、コンパイルにかかる時間、メモリ使用量、生成されたコードのサイズ、が大きくなる傾向にあります。また、全く最適化を行わない場合とくらべ、高いレベルの最適化では計算順序の入れ替えなどにより、浮動小数点演算の結果が多少変わる可能性があります。
例えば、-O3 では、効率が上がる場合は a*b*c を a*c*b に置き換えます。また、以下の例では、b+c の演算結果はループの間中変化しないので、ループの前に一度だけ演算を行い、適当なレジスタにその結果を保持しておけば高速化ができます。-O3 ではそのような最適化がコンパイラによって行われますが、-O2 では、1. 浮動小数点演算であるため、コードの変更が危険とみなされる (浮動小数点例外などが発生する可能性があるので)。2. ループの全てのパスで発生するわけではない。という理由で、演算がループの外に出されません。
...
int i;
float a[100], b, c;
for (i = 0 ; i < 100 ; i++){
if (a[i] < a[i+1])
a[i] = b + c;
}
...
|
実は、-O4 の最適化は、-O3 -qarch=auto -qtune=auto -qcache -qhot -qipa と同じです。また、-O5 の最適化は、-O4 -qipa=level2 と同じです。-O4, -O5 のオプションは、計算結果がかなり変わってしまうことがあるのと、qarch が auto 設定になっているため、クロスコンパイル環境ではよくない結果になることがあります。よって、お勧めの最適化手順としては、
- 最適化オプション無しで十分にデバックする
- -O3 オプションを試してみる
- -qhot, -qarch -qtune, -qcache などを一つづつ試していく
という流れがよいと思います。
次はお待ちかねの Single Source Compiler について説明します。XLC SSCはOpen MP API V2.5 をサポートしたコンパイラであり、#pragma 文により生成されたスレッドは、自動的に各 SPE に分配されて処理されます。コンパイラがこのような 2 つの異なるアーキテクチャコードをどのようにして取り扱っているのか興味のある方は、こちらの技術論文を参照するとよいでしょう。本記事では、あくまでも開発者が"ツールとしてこの XLC SSC が使える"かどうか、調査していくという立場を取ります。
XLC SSC は Cell/B.E. SDK 3.0 Extra Package に含まれていますが、デフォルトではインストールされないので、使用する際は別途手動でインストールする必要があります。パッケージは以下の表にあげた 7 種類になります。また、開発環境が、Power 系か、Intel x86 系かで用意されているパッケージが異なるので注意して下さい。
| パッケージの種類 | IBM POWER サポート | Intel x86 サポート |
|---|---|---|
| C/C++ runtime (再配布可能) | cell-xlc-ssc-rte-0.9.0-f.ppc64.rpm | cell-xlc-ssc-rte-0.9.0-f.i386.rpm |
| C/C++ runtime links | cell-xlc-ssc-rte-lnk-0.9.0-f.ppc64.rpm | cell-xlc-ssc-rte-lnk-0.9.0-f.i386.rpm |
| C/C++ libraries | cell-xlc-ssc-lib-0.9.0-f.ppc64.rpm | cell-xlc-ssc-lib-0.9.0-f.i386.rpm |
| C/C++ OMP libraries | cell-xlc-ssc-omp-0.9.0-f.ppc64.rpm | cell-xlc-ssc-omp-0.9.0-f.i386.rpm |
| C/C++ compiler | cell-xlc-ssc-cmp-0.9.0-f.ppc64.rpm | cell-xlc-ssc-cmp-0.9.0-f.i386.rpm |
| C/C++ help and documentation | cell-xlc-ssc-help-0.9.0-f.ppc64.rpm | cell-xlc-ssc-help-0.9.0-f.i386.rpm |
| C/C++ manpages | cell-xlc-ssc-man-0.9.0-f.ppc64.rpm | cell-xlc-ssc-man-0.9.0-f.i386.rpm |
XLC SSC の実行コマンドは以下の 3 つです。
- cbexlc
- cbexlc++
- cvexlC
このうち、cbexlc++, cbexlC は、C ソース、C++ ソース、どちらもコンパイル出来ますが、cbexlc では C++ ソースはコンパイルできません。
コンパイルオプションは基本的には XLC MA と同じです。ただし、一度のコンパイルで PPE, SPE, 2 つのアーキテクチャの命令を生成するため、いくつかのオプションが追加されています。以下に代表的なものを挙げておきます。
- -qarch=cell -qtune=cell
- qarch, qtune オプションは特定のアーキテクチャ依存のチューニングを行うものですが、XLC SSC では PPE, SPE, 2 つのアーキテクチャのチューニングを行うための、"cell" というアーキテクチャ名をオプションに与えることができます。
- -qarch=celledp -qtune=celledp
- 同様に、IBM PowerXcell 8i プロセッサの PPE, SPE, 2 つのアーキテクチャのチューニングを行うためのオプションです。
- -qnosmp
- XLC SSC にはデフォルトで
-qsmpオプションがついていて、OpenMP による並列化が可能ですが、明示的に OpenMP pragma を無視したい場合に、-qnosmpを使います。
OpenMP 自体の説明は次回の記事で詳しく行うとして、まずは XLC SSC の威力をサンプルコードで検証してみましょう。"Software Development Kit for Multicore Acceleration Version3.0 Programming Tutorial" に掲載されている Euler Particle Simulation のソースコードを題材として使ってみることにします。
リスト 1. Euler 法のサンプルプログラム (OpenMP による並列化)
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <omp.h>
#include "euler.h"
struct timeval tv1, tv2;
int elapsed_microsecs;
float elapsed_time;
vec4D pos[PARTICLES]; // particle positions
vec4D vel[PARTICLES]; // particle velocities
vec4D force; // current force being applied to the particles
float inv_mass[PARTICLES]; // inverse mass of the particles
float dt = 0.001f; // step in time
int main(int argc, char *argv[])
{
int i;
int m;
float time;
float dt_inv_mass;
double result;
float xpos;
float ypos;
int nthreads;
if(argc!=2){
printf("usage: >euler <thread num>\n");
return 1;
}
//initialize
force.x = 0.0;
force.y = 0.0;
force.z = -9.80; // [m/s]:gravity constant
srand(111);
for (i=0; i<PARTICLES; i++) {
pos[i].x = 0.0;
pos[i].y = 0.0;
pos[i].z = 0.0;
vel[i].x = ((float)rand()/(RAND_MAX + 1.0)*2.0 - 1.0);
vel[i].y = ((float)rand()/(RAND_MAX + 1.0)*2.0 - 1.0);
vel[i].z = ((float)rand()/(RAND_MAX + 1.0)*2.0 - 1.0);// [m/s]
inv_mass[i] = 0.001; // [kg]
}//line 50
nthreads = atoi(argv[1]);
omp_set_num_threads(nthreads);
gettimeofday(&tv1,NULL);
#pragma omp parallel default(none) private(i,time,dt_inv_mass) \
shared(pos,vel,dt,force,inv_mass,nthreads)
{
nthreads = omp_get_num_threads();
#pragma omp for
// For each particle
for (i=0; i<PARTICLES; i++) {
// For each step in time
for(time=0.0;time<END_OF_TIME;time += dt){
pos[i].x = vel[i].x * dt + pos[i].x;
pos[i].y = vel[i].y * dt + pos[i].y;
pos[i].z = vel[i].z * dt + pos[i].z;
dt_inv_mass = dt * inv_mass[i];
vel[i].x = dt_inv_mass * force.x + vel[i].x;
vel[i].y = dt_inv_mass * force.y + vel[i].y;
vel[i].z = dt_inv_mass * force.z + vel[i].z;
}
}
}
gettimeofday(&tv2,NULL);
elapsed_microsecs =
(int)(tv2.tv_sec - tv1.tv_sec) * 1000000 +
(int)(tv2.tv_usec - tv1.tv_usec);
elapsed_time = (float) elapsed_microsecs * 0.000001;
printf("Nthread = %d, Elapsed time = %f \n",nthreads,elapsed_time);
return (0);
}
|
Software Development Kit for Multicore Acceleration Version3.0 Programming Tutorial" をみていただくとわかりますが、リスト 1. は Euler 法とよばれる粒子シミュレーションアルゴリズムをそのまま抜き出したものです。ただし Tutorial のものと異なり、
- 粒子の初期値を定めています
- 引数にスレッドの数を与えられるようにしています
- ループの前後で
gettimeofday関数で経過時間の測定を行っています
XLC SSC のための並列化特有の部分は 51 行目以下で、51 行目の関数 omp_set_num_threads(nthreads) によって、OpenMP によって並列化されるスレッドの個数を指定しています。この関数は、omp.h をインクルードして使用します。57 行目で OpenMP に規定された pragma 文により、並列化部分の変数の種類を定義し、61 行目でループブロックの並列化をしています。
もし、リスト 1 のコードを OpenMP に対応していないコンパイラでビルドした場合、#pragma 文は単に無視されるだけです。よって、通常の逐次処理のコードも、並列化されたマルチスレッドのコードも、ひとつのソースコードで管理できるということになります。これは、OpenMP の大きな特徴の一つです。
さて、ざっと見てきましたがどうでしょうか?簡単なプログラムなので、違和感はないと思います。Euler 法のさらに詳しい説明は Software Development Kit for Multicore Acceleration Version3.0 Programming Tutorial" を参照下さい。また、OpenMP の詳しい説明は次回の記事で詳しく説明します。いまのところは、そういうものだと思って、先に進みましょう。
リスト 1 でインクルードしている euler.h は以下のようになります。
リスト 2. Euler 法のヘッダープログラム
#ifndef _EULER_H_
#define _EULER_H_
#define END_OF_TIME 300
#define PARTICLES 10000
/* 4-D vector definition. */
typedef struct {
float x, y, z, w;
} vec4D;
#endif /* _EULER_H_ */
|
本記事での実験では END_OF_TIME の値を 300 秒に、また、PARTICLES の値を 10000 個にしています。時間や粒子の数を適切にして、タイムステップ毎に粒子の場所をファイルに出力し、gnuplot などの可視化ツールを使用すれば、図 1. のようなきれいな粒子の飛跡を得ることができます。もちろん、表示系を自分で工夫すれば、シミュレーションが進むにつれて飛跡が伸びていくようなリアルタイム表示のアプリケーションにすることも可能です。
図 1. Euler 法シミュレーションの可視化例 (100particle, 300sec)
Makefile は以下のようになります。
リスト 3. Euler 法の Makefile
CC=/opt/ibmcmp/xlc/ssc/0.9/bin/cbexlc
CFLAGS=-O3 -qarch=cell -qtune=cell
all: euler
euler: euler.c
$(CC) $(CFLAGS) -o $@ $(INCLUDE) euler.c $(CLIB)
clean:
rm -f euler *.o *.d
|
2 行目をみればわかるように、最適化オプションとして -O3 と、アーキテクチャ依存最適化の指定を行っています。
上記のプログラムをXLC SSCでコンパイルし、さっそく速度計測をしてみましょう。ここでは、IBM BladeCenter QS20 (Cell/B.E.プロセッサ3.2GHz, 1GB memory)で速度計測をしてみることにします。図 2. がその結果です。横軸のスレッドの数に応じて処理にかかった経過時間が減少しているのがよくわかります。スレッド数が17のときに、経過時間は最小になり、それ以上の数を引数に与えてもomp_get_thread_numで返されるスレッド生成数が増えないことがわかると思います。この17という数は、SPE x 8 + QS20上のもう一つのCell/B.E.プロセッサのSPE x 8 + メインスレッドのPPE = 17 だと考えられます。
図 2. Euler 法プログラムの測定結果
XLC MA では、重要ななコンパイルオプションについて、その概要を説明しました。Cell/B.E. ソフトウエア開発では、GNU GCC だけではなく、XLC MA も是非一度試して見て下さい。労せずしてコードの高速化に成功するかもしれません。
XLC SSC では、OpenMP によるお手軽な並列化手法を、Euler 法というプログラムを題材にして見てみました。ほんの少しの修正で、スカラーコード (PPE1 コアによるシングルスレッドコード) にくらべ、17 コアを使ってちょうど 17 倍の速度向上を得ることができました。Dual Source コンパイラでカリカリにチューニングした場合はもっとよい数字が得られるかもしれませんが、17 倍というのは、かかった労力 (たった数行の追加!)を考えれば悪くない数字です。
次回「XLC で行こう! 第 2 回」では、OpenMP のプログラミング記法についてさらに詳しく説明していく予定です。
学ぶために
- Jonathon Bartlett のシリーズ 「Cell BE プロセッサーでのハイパフォーマンス・アプリケーションのプログラミング」 (developerWorks, January 2007 to present) には以下の記事が収録されています。PS3 ユーザーは参考にして下さい。
- 第 1 回: PLAYSTATION 3 での Linux 入門
- programming the PS3's SPE
- 第 3 回: 相乗演算処理装置の紹介
- SPU performance programming
- 第 5 回: C/C++ での SPU のプログラミング
- 第 6 回: DMA 転送での賢いバッファー管理
- The IBM Semiconductor Solutions Technical Library Cell Broadband Engine documentation セクションには、ダウンロードマニュアルや、仕様書などたくさんの有益な情報があります。
- 全ての Cell/B.E. 情報 --関連記事、ディスカッションフォーラム、CellSDK、その他のダウンロード-- は IBM developerWorks の Cell Broadband Engine resource center: your definitive resource for all things Cell/B.E. にあります。
製品や技術を入手するために
- Cell/B.E. を手に入れるにはこちら: Contact IBM about custom Cell/B.E.-based or custom-processor based solutions.
- XL C/C++ Multicore Acceleration for Linux のWebページ
-
XL C/C++ for Multicore Acceleration for Linux compilers インフォーメーションセンター
議論するために
-
ディスカッションフォーラムに参加してください。
- 質問はIBM developerWorks Power Architecture Cell Broadband Engine discussion forum へ投稿してください。
