カテゴリー「Cellプログラミング」の記事

PS3 Linux終了

不本意ながら、我が家のPS3はシステムソフトウェア バージョン3.21にアップデートを行い、ゲーム&BDプレイヤー専用機となりました・・

最近は電子工作にはまっており、PS3関連のアップデートはありませんでしたが、Sonyのサポート打ち切りに伴い、PS3 Linux関連のアップデートは今後できなくなりました。事の発端となったハッカー氏のwebでは色んな話題や罵詈雑言が飛び交っていますが、私は変なことは考えずに、PS3 Linuxをこの機会に終了としました。

PS3を発売後真っ先に買った理由の半分以上はCellでの並列プログラミング体験でしたので、非常に残念です。

Cell Open CV

OpenCV on the Cellのインストールに関する質問をいただいたため、ちょっと試してみました。けっこうおもしろいです。

OpenCVとはインテルが開発・公開している画像処理のライブラリで、Linuxの他Windowsでも動作します。今回始めて知りましたが、画像の回転、エッジ検出などの処理を行う汎用ライブラリです。ドキュメントはここです。

OpenCV on the Cellは、オリジナルのOpenCVをCellに最適化したもので、SPEに処理を分散・並列処理することが可能となっています。

インストール

FixStarsさんのOpenCV on the Cellに従ってインストールします。

Sourcforge project pageから、以下のrpmをダウンロードします。

  • opencv-cell-1.0.0-2.ppc.rpm
  • opencv-cell-devel-1.0.0-2.ppc.rpm
  • cvcell-0.1.0-2.ppc.rpm
  • cvcell-demo-0.1.0-2.ppc.rpm

# rpm -ivh <rpmファイル名>でインストールすると、以下のディレクトリにファイルが展開されます。
 /opt/cvcell/bin -- コンパイルされたサンプルプログラム
 /usr/share/opencv-cell -- サンプルソースなど

.bash_profileを編集して、以下の環境変数を追加します。
 CVCELL_SPENUM=6
 LD_LIBRARY_PATH=/opt/cvcell/lib:$LD_LIBRARY_PATH
 export CVCELL_SPENUM
 export LD_LIBRARY_PATH


デモプログラムの実行

/opt/cvcell/binにある顔認識プログラムを動かしてみます。
 $ ./facedetect.exe --cascade=haarcascade_frontalface_alt.xml lena.jpg
以下の結果が表示されれば成功です。

Screenshotresult_3

--cascade=haarcascade_frontalface_alt.xmlは、正面顔を学習させた識別器の指定です。haarcascade_xxのファイルは、/opt/cvcell/binもしくは /usr/share/opencv-cell/haarcascadesにインストールされます。

OpenCVを使用したプログラムの概要は、このWeb記事が参考になりました。


サンプルプログラムのコンパイル

/usr/share/opencv-cell/samples/cにある、facedetect.cをコンパイルしてみました。おそらく、デモプルグラムとして使用したfacedetect.exeのソースだと思います。そのままコンパイルすることもできるのですが、ヘッダファイルのインクルードでエラーが出るため、ソースを若干変更しました。修正版のソースはここです

コンパイルはSDK 3.1で行いました。標準のppu-gcc 4.1.1を使用した場合、リンク時にエラーが出てしまったため、プロトタイプとして配布されているppu-gcc 4.3を追加インストールして以下のようにコンパイル・リンクしました。

$ ppu-gcc43 -O3 -Wall -c -m32 -mabi=altivec -maltivec -o facedetect.o facedetect.c
$ ppu-gcc43 -m32 -o facedetect  facedetect.o -lcv -lcxcore -lhighgui -lcvaux

修正版のソースでは、デフォルトでcascadeファイルとして/usr/share/opencv-cell/haarcascades/haarcascade_frontalface_alt.xmlを指定するようにしてあります。そのため、以下のように画像ファイルを指定することで顔認識ができます。
 $ facedetect <image file>

PC LinuxにもOpenCVをインストールし、同一ソースをコンパイルして顔認識の速度を比較してみました。画像データーは、OpenCVに添付のlena.jpgです。結果は以下の通りです:

  • PS3 : 100.058ms
  • PC(C2Q 9450)  : 233.94ms

PCはCore2 Quad (3GHz)ですが、OpenCVがマルチスレッドに非対応のため、1コアしか動作しません。一方Cell版OpenCVは自動的に6 SPEに処理分散するため、PCの半分以下で処理ができました。

顔認識の処理では、OpenCVのAPIである"cvHaarDetectObjects"関数をPPE側で呼び出してオブジェクト検出を起動していますが、自動的にSPEを起動してくれるためSPE毎のデーター分割を考える必要がなく、プログラム的には非常に楽ちんです。

プログラムの実行中に/spuを見ると、以下のように複数SPEが動作していることが分かります。

# ls /spu
spethread-4184-268519504  spethread-4184-268521104  spethread-4184-268522704
spethread-4184-268520304  spethread-4184-268521904  spethread-4184-268523504

オブジェクト検出処理を行う以下のコードのパラメーターをいじると認識率が変化します。
CvSeq* faces = cvHaarDetectObjects( small_img, cascade, storage,
        1.1, 2, 0/*CV_HAAR_DO_CANNY_PRUNING*/,
         cvSize(30, 30) );


2008/11/24追記

PC LinuxでのOpenCVの並列化ですが、ひょっとしてOpenMPと組み合わせることでできないかと考え、試したのですが処理性能に変化はありませんでした。

Webで検索すると、OpenCVのビルドオプションに"--with-openmp"があるようですが、私はrpmパッケージでインストールしているため、ソースからのコンパイルまでは試していません。

2008/11/29追記

Webカメラ(Logicool Qcam S 7500)を接続したところ、無事カメラを認識してくれfacedetect.cのサンプルコードでカメラ画像の連続顔認識も動作しました。Fedora 9のカーネル2.6.27は再コンパイルなしでWebカメラを認識できましたが、カメラを使うアプリケーション(facedetect)はrootでないとキャプチャーデバイスをオープンできませんでした。この点は謎です。

処理性能に関しては、PS3では120〜122ms程度で1フレームを処理できます。PC(C2Q 9450)では250ms程度の処理時間がかかるため、PS3の方が高速に処理できていることが分かります。

Core2 Quadの最大演算性能

連休に自宅PCのCPUをCore2 Quad Q9450に更新しました。今回はCellプログラミングネタではありませんが、Core2 Quadの演算性能測定結果を掲載します。

測定方法はCellの最大演算性能で、Pentium4の性能測定に使用したGoto BLASによる行列積の計算です。

Goto BLASのビルド

BLAS(Basic Linear Algebra Subprograms)とは、シミュレーションなどで使用する行列演算やベクトル演算のライブラリー集です。Fedora 7にはBLAS・ATLAS、Cell SDK 3.0にはLAPACKというパッケージが提供されています。Goto BLASは独自の最適化を行っており、CPU性能の限界が引き出せることで有名なため、Goto BLASを性能測定用に使用しました。

Goto BLASはその名の通り、テキサス大学 Texas Advanced Computing Center (TACC)の後藤和茂さんによって開発されたパッケージで、TACCのWebサイトからソースコードがダウンロードできます(アカウント登録が必要)。

以下の手順で、Goto BLASのビルドを行います。 ビルドの方法はこちらを参考にさせていただきました。

  • ソースをダウンロードしたら適当なディレクトリに展開します
    (例えば; /usr/src/GotoBLAS)
  • quickbuild.32bitを実行
  • libgoto_penrynp-r1.26.a、libgoto_penrynp-r1.26.soがソースディレクトリに生成されます(ファイル名のpenrynpの部分はコンパイルを行うCPU種別によって変化します)
  • libgoto_xxx.a, libgoto_xxx.soを/usr/libにコピー(ユーザープログラムとリンクする際にデフォルトのライブラリパスが使用できるようにするため)
  • /usr/libに移動してlibgoto.a, libgoto.soにシンボリックリンクを作成
      # ln -s libgoto_penrynp-r1.26.a libgoto.a
      # ln -s libgoto_penrynp-r1.26.so libgoto.so

CBLASのビルド

BLASは元々FORTRAN用に作成されているため、Cから呼び出すこと(リンクすること)ができません。CからBLASを利用するためのInterfaceがCBLASになります。CBLASのソースを、BLAS Technical Forumの「Reference implementation for the C interface to the Legacy BLAS」からダウンロードして、以下の手順でビルドします。

  • 事前にFORTRANコンパイラをインストールする必要があります。今回は、gfortran(GNU Fortran)を使用しました
  • ソースを適当なディレクトリに展開(例えば; /usr/src/CBLAS)
  • ソースディレクトリに移動して以下のリンクを作成
    # ln -s Makefile.LINUX Makefile.in
  • Makefine.LINUXの以下を編集
      BLLIB = /usr/lib/libgoto.a   --- GotoBLASのパスを指定
      CBDIR = /usr/src/CBLAS   -- CBLASのソースディレクトリ
      FC = gfortran  -- FORTRANコンパイラの指定
  • make alllib
    make allを指定すると、ライブラリの生成に加えて、テストプログラムのビルドを行うのですが、こちらは途中でエラー終了してしまいました。libgotoをリンクするところでエラーになっているのですが、Goto BLASをマルチスレッド版でビルドしているのに対して、テストプログラムがシングルスレッドのためエラーになっているように見えます。そのため、make alllibでライブラリが生成できればよしとします。
  • ./lib/LINUX/cblas_LINUX.a を/usr/libにコピー
  • /usr/libに移動して、# ln -s cblas_LINUX.a libcblas.a にてシンボリックリンクを作成
  • /usr/src/CBLAS/src/cblas.hを/usr/includeにコピー

行列積計算

Cellの最大演算性能でPentium4の性能測定に使用したプログラムを実行します(ソースファイルをダウンロード )。Goto BLASを使用する際のポイントを以下に示します。

  • マルチコアCPUを使用した場合、Goto BLASが自動的にコア数分のスレッドを生成します。並列処理を加味した計算データーの分割を考えなくてもよいため、非常に楽ちんです
  • スレッド数を制限したい場合は、環境変数 GOTO_NUM_THREADS=1 等を設定します(数字は最大スレッド数)
  • プログラムにcblas.hをインクルードし、プロトタイプ宣言に従ってBLASライブラリを呼び出します。行列積を求める関数名はcblas_sgemmです。
  • リンクの際は、ライブラリとしてcbras, goto, pthreadを指定します (リンカオプションに-lcblas -lgoto -lpthreadを指定)
  • マルチスレッド版のGoto BLASを使用する場合、pthreadの指定が必要です

計算結果

単精度浮動小数点4096 x 4096要素の行列積を5回繰り返した際の性能値は以下となりました。CPUはCore2 Quad Q9450(3GHzで動作)です。

コア数(スレッド数)GFLOPS
1 22.50
2 44.75
3 64.66
4 88.50
  • コアあたりの演算性能は22 GLOPSとなり、コア数(スレッド数)に比例してリニアに性能が伸びました
  • コア数を増やした際に、メモリアクセス競合によって性能が低下するようなことはありませんでした。行列積は計算量に対してメモリアクセスが比較的少なく、複数のコアが同時にメモリアクセスを行った際に、片方のコアで計算がストールするシーンがないものと思われます
  • Pentium 4(3.4GHz)で同様の計算を行った際の性能値が11.22GFLOPSでしたので、Core 2でコアあたりの性能は2倍に向上しました
  • CellはSPUあたり23.21GFLOPS、6 SPU時139.36GFLOPSの性能が出せるため、この手のピーク性能競争では、SPUコア数が多い分Cellが優位を保っています

PS

コア数を変化させた際の性能特性を計りたいために、まだまだ高価なCore 2 Quad Q9450を購入してしまいました(これまで買ったCPUでもっとも高価、、)。安くなったQ6600のと比較では最後まで悩んだのですが、そこは新しいもの好きの血が騒ぎ、結局は9450となりました、、

MB(P5K-E)、CPU FAN(サントラスト薙刀)はPentium4時代に買ったものを継続使用しているのですが、4コアフル稼働時でもファンの回転速度がP4フル稼働時より低くなることから、性能に加えて、発熱の点でも改善された印象があります。(ただ、温度センサーの動作がP4, Core2では異なるため厳密な比較はできませんが)。FANの騒音は低負荷時・負荷時とも確実に下がっており、この点は非常にうれしいです。

日常の利用シーンではP4に対して8倍(2倍x4コア)の性能向上を実感できることはないのですが、騒音低減、いざというときの予備性能(?)で、自己満足度は100%です。

Cellの最大演算性能

Cellの最大演算性能は以下の式で計算できます。

  • SPU (SPE)あたりの論理性能(FLOPS) = クロック x 4データー x 2FLOPS

この考え方は、東芝レビューの「次世代プロセッサCell Broadband Engine」に記載がありました。SPUでは、SIMD演算を使用して1クロックサイクルで4つの積和演算(乗算と加算)を行うことができるため、クロックサイクルあたり8演算が実行できることになります。

PS3に搭載されているCellのクロック周波数は3.2GHzとされています。したがってSPU (SPE)あたりの論理性能は 3.2G x 8 = 25.6GFLOPSになります。

PS3 Linuxでは最大6SPUが使用できるため、Cellの論理性能は、25.6 x 6 = 153.6GFLOPSになります。以前から、実際の性能がどの程度になるかを実測したいと思っていました。

論理性能と実性能

現実的な大規模計算では、データーをメモリーから演算器に読み込む必要や計算結果をメモリーに書き戻す必要があるため、メモリーのリード・ライトが大量に発生します。例えば、4096 x 4096要素をもつ単精度浮動小数点の行列のデーター量は64Mbyteになります。全データーはCPU内のキャッシュに乗らないため、CPUに比べて何百倍も遅いメインメモリーとのデーター転送が発生し、データーフェッチ中に計算が止まる空白期間が発生することで実性能が低下する可能性があります。

そのため、私は、メモリー性能がボトルネックになり、現実的なアプリケーションで論理性能が発揮できることはないと思っていました。

Cellの最大性能が測定できるアプリケーション

実は、身近なところに絶好のアプリケーションがありました。Cell SDKに入っている、行列積を求める「matrix_mul」です。ソースは "/opt/cell/sdk/src/demos/matrix_mul"にあり、make一発で簡単にコンパイルできます。

論理性能を引き出すための条件とは

Matrix_mulのREADME.txtファイルには、「本プログラムは高度な最適化を行っており、理論値に近いピーク性能を出せる」と書いてあります。うむむ、本当か? 論理性能近くを出すためには、以下の条件を満足する必要があるはずです。

  1. 計算に必要な行列A, B, Cはメインメモリーに保持されるため、SPUへのデーターのロードと計算結果のストアーを行うためのDMAが必要となる。DMAと計算処理を平行して行い、演算を切れ目なく行う必要がある
  2. 1回のDMAで転送できる最大データー量は16KBとなるため、行列をブロック単位に分割し、小刻みにデーター転送しながらを計算を行う。切れ目なく計算を行うためには、1ブロックの計算時間以内に、前ブロックの計算結果のストア(DMA)と次ブロックの計算に必要なデーターのロード(DMA)が完了する必要がある
  3. 命令の実行順序に配慮して、1 SIMD命令/クロックとなるように最適化を行う
  4. ループで消費される処理ステップを極力減らす(ループアンローリング)
  5. 計算データーのセットアップ(レジスタへのロードなど)で消費される処理ステップを極力減らす

1項に関しては、CellはSPUの処理とMFCを使用したDMAが並列動作するため可能です。

2項に関しては、ダブルバッファーを使用することで可能となりますが、DMA転送するデーター量と処理時間の関係を微妙にチューニングする必要がありそうです。計算処理が単純すぎると、DMA時間>計算時間となり、計算処理がストールする時間が発生してしまいます。

3項に関しては、SPUの命令実行はインオーダーのみで、パイプラインの流れを最適化するためにハードレベルで命令の実行順序を入れ替えてくれることがないため、ソフトレベルの最適化がが必須となります。

matrix_mulの実行

matrix_mulをコンパイルしたら実際に実行してみます。このプログラムはM x Mの行列積を計算します。そのため、入力データーになる行列a, bと、計算結果を格納する行列cをメモリー上に確保する必要があります。私の環境では、runlevel 3でPS3 linuxを起動した際の空きメモリーが約140Mbyteです。

4096 x 4096の計算を行うためには、64M x 3 = 192Mbyteのメモリーが必要となり、空きメモリーが不足するため、2048 x 2048 と 3072 x 3072で計算を行うことにします。

matrix_mulは上記のように大きなメモリーを使用するため、TLB(仮想アドレスから実メモリーアドレスへの変換テーブル)のミスヒットによるメモリーアクセス性能低下を避けるため、linuxカーネルのhugetlbfsという機能を使用することができます。Fedora 7の最新カーネルではhugetlbfsは以下のように有効になっていますが、メモリーの割り当ては行われていません。

 # cat /proc/meminfo
 MemTotal:       216780 kB
 MemFree:       139960 kB
   ・
   ・
 HugePages_Total:     0
 HugePages_Free:     0
 HugePages_Rsvd:     0
 Hugepagesize:    16384 kB

hugetlbfsを有効にするためには、まずはファイルシステムのマウントを行います。

 # mount -t hugetlbfs nodev /huge

次にHugepageに割り当てるページ数を指定する必要があります。3072 x 3072の計算を行うためには、7ページが必要となるため、以下の設定で8ページを確保します。

 # echo 8 > /proc/sys/vm/nr_hugepages

Hugepageを確保した分空きメモリーが減少します。

# cat /proc/meminfo
MemTotal:       216780 kB
MemFree:         8788 kB
   ・
   ・
HugePages_Total:     8
HugePages_Free:     8
HugePages_Rsvd:     0
Hugepagesize:    16384 kB

いよいよ、m = 3072で計算を行います(-i 5は計算の繰り返し回数です)

 # ./matrix_mul -i 5 -m 3072 -s 6 -p
 Initializing Arrays ... done
 Running test ... done
 Performance Statistics:
   number of SPEs     = 6
   execution time     = 2.08 seconds
   computation rate   = 139.36 GFlops/sec
   data transfer rate = 8.80 GBytes/sec

結果は、139.36 GFLOPSですsign03 なんと、論理性能の91%が出せたことになります。

性能測定結果のまとめ

matrix_mulを3回実行した平均値を以下に示します。

要素数SPU数GFLOPS論理性能比
3072 6 139.36 91%
3072 1 23.23 91%
2048 6 138.51 90%
2048 1 23.21 91%

上記に示すように、SPUx1に対して、SPUx6はきっちり6倍の性能となりました。ちなみに、Hugepageを使用しない場合でも、まったく同じ測定結果となりました。この程度の要素数ではHugepageを使用するメリットは得られないようです。

論理性能はレジスタ間で計算を繰り返した場合のような非現実的な条件でしか出せないと思っていたのですが、DMAを繰り返すような現実的な条件でここまでの性能が出せるとは驚きです。SPUx6が同時に動作してもリニアに性能が伸びる点もすごいです。

前述したCellの論理性能を引き出すための条件が成立するアプリケーションレンジは狭く、セルはピーキーでプログラムしづらいCPUであることに変わりはないのですが、性能を追求したアーキテクチャーになっていることがよく分かりました。45nm(Penrynコア)のCore2 quadが発売されたらMy PCのCPU交換を行いたいと思っており、その際はまたベンチマークをやってみたいです。

ただ、matirx_mulのソースを眺めたのですが、SPUコードの計算部分は何をやっているのかサッパリ分かりません。行列積計算の並列化に関してはいろんな論文があるようなので、そこからお勉強が必要になりそうです。

2008/2/11追記

行列積の計算は、ピーク性能が出しやすいベンチマークであるとのご指摘をいただきました。Webで調べてみると、確かに、行列積はデーターの再利用を行いやすく論理性能の8〜9割を出せるライブラリーがあるようです。BLASのsgmmライブラリーを使用してMy PC (Pentrimum4 530@3.4GHz)で4096 x 4096の行列積を計算してみたところ、以下の結果となりました。

ライブラリー種別GFLOPS
ATLAS 6.61
GotoBLAS(1 thread) 11.22
GotoBLAS(2 thread) 11.14

P4 3.4GHzの論理性能が13GFLOPS (3.0GHzで12GFLOPSという数字を見たことがあります)とすると、GotoBLASは論理性能の85%以上を出せていることになり非常に優秀です。計算も以下のライブラリを呼び出すだけなので楽ちんです(リンクがうまくいかず苦戦しましたが)。

    // 配列a,b,cを格納するメモリーを確保
    a = (float *)memalign(256, msize*msize*sizeof(float));
    b = (float *)memalign(256, msize*msize*sizeof(float));
    c = (float *)memalign(256, msize*msize*sizeof(float));

    // 行列の初期化
    for(i=0; i<msize; i++) {
        for(j=0; j<msize; j++) {
            a[i*msize+j] = rand_0_to_1();
            b[i*msize+j] = rand_0_to_1();
            c[i*msize+j] = 0.0f;
        }
    }

    // BLASライブラリの呼び出し →  C = α A・B + βC
    cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, msize, msize, msize,
        1.0f, a, msize, b, msize, 0.0f, c, msize);

P4 530はHTが使えるため、GotoBlasでは2スレッドを使用した並列処理もできましたが性能は同じでした。SSEの演算機が2組あるマルチコアCPUなら2スレッド時の性能がもっと上がると思います。

行列積だとアーキテクチャーが異なっても論理性能の8割以上は出せるということですね。普段科学技術計算などやることがないので(本職はIPネットワークSEです)、今回は大変勉強になりました。

Cellプログラムのデバッグ

今回は、Cellプログラムのデバッグ方法についてまとめてみました。

私は通常、PC LinuxのEclipse IDEでCellプログラムを作成・コンパイルし、PCのシミュレーター上で、IDEのGUIベースでデバッグを行っています。PS3上でgdbを使ってデバッグを行っても同様のことが出きるのですが、GUIベースで変数の状態確認やステップ動作が出来た方が私的には楽ちんです。PS3ではメモリー量の制約からIDEを動かすことはできません。

ただ、Eclipse IDEでCellプログラムのデバッグを行う際には、以下の問題がありました。

  1. SDK 3.0のIDEになってから、デバッグを開始する際にに、次のエラーメッセージが出る
       Error while mapping shared library sections:
       /lib/libgcc_s.so.1: No such file or directory.
  2. SDK 3.0にしてから、SPEプログラムのトレースができない場合がある(ブレークポイントを設定しても停止しないなど)
  3. シミュレーターではレスポンスが悪い(数千回ループを回すような計算はかなり待たされる)
  4. グローバル宣言された大きな配列をwatchするとtime outエラーが発生する

個々の問題について、解決方法を試行錯誤してみました。

1. Error while mapping shared library sectionsの対策

IDEでデバッグを行う際に、PC側の/opt/cell/sysroot/lib配下のシェアードライブラリをサーチしますが、その際にlibgcc_s.so.1が見つからないというエラーのようです。PS3のppc Linuxからこのファイルをコピーしてやると今度はライブラリーのバージョンが違うというエラーが出てきます。ということで、SDK 3.0のシミュレーター環境についてきたライブラリーファイルが必要なようです。シミュレーター環境のファイルは以下の手順で取得できます。

 # mount -o loop -t ext2 /opt/ibm/systemsim-cell/images/cell/sysroot_disk  /mnt/sim

/mnt/simは適当なマウント用のディレクトリを作成します。/mnt/sim/libに移動して以下のファイルを/opt/cell/sysroot/libにコピーします。
 libgcc_s-4.1.2-20070503.so.1
 libgcc_s.so.1

3項の解決策を行うことでも本問題は解決します。

2. SPEプログラムのトレースができない問題の対策

本当の原因は不明なのですが、以下の措置で問題が解決しています。

  • SDK 2.1で作成したプログラムは、ソースファイルをバックアップした後、一旦プロジェクトごと削除し、プロジェクトを再度作成しコンパイル・リンクをやり直す
  • あるプログラムは、pthread_create()のみを呼び出して、pthread_join()呼び出しが抜けていたので、pthread_join()の追加で正常動作

3. レスポンス改善

PC上のEclipse IDEからPS3上で動作するターゲットプログラムを制御することで、シミュレーターを使用せずにデバッグができることが分かりました。

  • 図の「シミュレーターでのデバッグ」に示すように、PC上でデバッグを行う場合、シミュレーター上で動作するgdb_serverがデバッグ対象のターゲットプログラムを起動します。
  • x86 Linux上で動作するppu-gdbがgdb_serverと通信し、デバッグを制御します
  • また、Eclipse IDEとppu-gdbが通信することで、デバッグセッションの状態をIDEのGUI画面に表示します(IDEが裏でgdbのコマンドをたたいて、結果をGUI画面に表示するイメージです)

Gdb_server_4

ということは、シミュレーターの代わりに、「PS3を使用したデバッグ」に示すように、PS3上でgdb_serverとターゲットプログラムを動かしてTCP/IPで通信できるようにしてあげれば、シミュレーターと同様の動作ができるはずです。実際に以下の設定で動作が可能となりました。

  • PS3にもSDK3.0がインストールされていること
  • /opt/cell/sysrootを異なるディレクトリ名にリネーム
  • PS3のルートディレクトリ(/)を/opt/cell/sysrootにnfsを使用してマウント
    リンク時に参照される/opt/cell/sysroot/配下のライブラリーと、PS3で実行時に動的にリンクされるシェアードライブラリーが同一でないとデバッグがうまく動かない場合があるため、この操作が必要になります
  • すでに作成済みのプログラムは再度リンクをやり直します
  • IDEのCell EnvironmentからCell Boxを右クリックしてcreatを選択
  • HostにPS3のIPアドレス、user/passwordにPS3のアカウント名・パスワードを設定
  • 作成したCell Boxを選択してStart Environmentのボタンをクリック
  • Debugのダイアログボックスを開き、Targetに作成したCellBoxを設定
  • デバックを開始

これで見違えるほど高速にデバッグができるようになります。何よりもシミュレーターの起動時間がなくなる点が大きいです。

(2008/4/3追記)
PC側・PS3側にFirewallの設定が行われていると、debuggerが使用するTCPポート(デフォルトは10000)がブロックされデバッグを起動できない場合があります(デバッグ起動中に、No route to host等の表示のあと強制終了してしまう)。Fedoraの場合、system-config-securitylevelを使用してFirewallを設定した場合、「その他のポート」欄にTCPポート10000などを追加する必要があります(下記の残問題1が発生した場合を加味して、10000-10100などのレンジを指定することも可能です)。私は、ブロードバンドルーター(これ自体がfirewallになる)の内側で使用しているため、Linuxのfirewallは無効にしてあります。

4. グローバル宣言された大きな配列をwatch

この問題も、3項に示すPS3上でデバッグターゲットを動かす方法で解決します。

残問題(2008/1/21追記)

きりんさんのコメントにもあったのですが、以下の問題はまだ未解決です。

  1. "Remote TCP port already in use"のエラー
    シミュレーター・CellBox双方で遭遇しました。この記事を書く段階になって発生しなくなったため、投稿には含め なかった次第です。この問題は、デバッグセッションが異常終了した際に、gdbとgdb_serverの通信で使用しているTCPポート=10000が開 放されないために発生するようです。デバッグプロパティ→ Debugger→ Port numberを別の値に変えるとデバッグができますが、異常終了する毎に新しい値に変える必要があり、何だか気持ちがよくないです。
  2. speプログラムの"No source file named”
    当方の環境でも出ます(SPE埋め込み・分離双方で)。DebugプロパティーのSource部分をいじってみても改善しません。3項を除いてspeプログラムのトレースはできているため、現状放置です
  3. SPEプログラムのトレース(次のケースでは依然としてトレースがうまく動作しません)
    spe_context_runを実行するthreadをloopで何回も呼び出すと、SIGTRAPによりgdbが停止したり、speプログラム内をトレー スできない(ブレークポイントが無視される)問題があります。spe_context_runを実行するthreadを1回しか呼び出さない場合、SPE埋め込み(Embedded)を含めてspeプログラム内もトレースできています。
  4. C/C++ Buildだけエラーダイアログが出る
    CellBoxを使用する前、SDK2.1→3.0に更新した直後に遭遇した記憶があります

SPEプログラムのビルド方法

前回のCell Mailboxに関する投稿で使用したサンプルプログラムから、SPE(SPU)プログラムのビルド方法を変更しています。今回はこの点について記載します。

これまでの方法

SPE(SPU)プログラムを独立した実行ファイルとしてビルドし、PPE(PPU)プログラムから、1)SPEイメージのオープン、2)SPE Contextの生成、3)SPEへのプログラムロード、4)SPE Contextの起動、5)SPE Contextの廃棄、6)SPEイメージのクローズ、を実行していました。

サンプルコード的には以下となります。

int main(int argc, char **argv)
{
    int ret;

    spe_context_ptr_t spe_ctx;
    spe_program_handle_t *spe_prog;
    unsigned int entry;
    spe_stop_info_t stop_info;

// 1)SPEイメージのロード
// spe_image_open関数の戻り値を、spe_program_handle_tへのポインターに
// 代入する
    spe_prog = spe_image_open("hello_spe");
    if (!spe_prog) {
        perror("spe_image_open");
        exit(1);
    }

// 2)SPE Contextの生成
    spe_ctx = spe_context_create(0, NULL);
    if (!spe_ctx) {
        perror("spe_context_create");
        exit(1);
    }

// 3)SPEへのプログラムロード
    ret = spe_program_load(spe_ctx, spe_prog);
    if (ret) {
        perror("spe_program_load");
        exit(1);
    }

    entry = SPE_DEFAULT_ENTRY;
// 4)SPE Contextの起動
// ただし、SPEが停止するまで呼び出し元をブロックするため
// 別threadを生成しその中でspe_context_run()を呼び出すのが一般的
    ret = spe_context_run(spe_ctx, &entry, 0, NULL, NULL, &stop_info);
    if (ret < 0) {
        perror("spe_context_run");
        exit(1);
    }

// 5)SPE Contextの廃棄
    ret = spe_context_destroy(spe_ctx);
    if (ret) {
        perror("spe_context_destroy");
        exit(1);
    }

// 6)SPEイメージのクローズ
    ret = spe_image_close(spe_prog);
    if (ret) {
        perror("spe_image_close");
        exit(1);
    }

    return 0;
}

新しい方法 ー Embeded SPE Object

SPEプログラムを、Cell Embedded SPE Objectというフォーマットでビルドし、PPEプログラム中に埋め込む(embedする)ことができます。こうすることで、これまでの方法で必要とされた1)6)(SPEイメージファイルのopen/close)が不要となります。

また、ファイルが1つになるため、PCのクロス開発環境でビルドしたプログラムをPS3に転送する手間(大した手間ではありませんが、、)や、Eclipse IDEでデバッグを行う際に必要となるCellのシミュレーターにSPEプログラムを転送する設定が割愛できます(こちらは結構重宝します)。

PPEプログラムにEmbedded SPE Objectを埋め込むためには、ppu-embedspuというツールを使用するか、Eclipse IDEで以下の設定を行います。
 PPEプログラムのProperty→  C/C++ Build→ PPU GNU 32bit Embed SPU
   → Input → 埋め込むSPEプログラムを指定

また、プログラムコード的には埋め込んだSPEプログラムとのリンクを取るために以下のコーディングが必要です。

// SPEプログラムのシンボル名を宣言
// 通常はSPEプログラムのオブジェクト名 = シンボル名となる
extern spe_program_handle_t hello_spe;

int main(int argc, char **argv)
{
    int ret;

    spe_context_ptr_t spe_ctx;
    unsigned int entry;
    spe_stop_info_t stop_info;

    printf("Hello, SPE!\n");

// a)SPE Contextの生成
   spe_ctx = spe_context_create(0, NULL);
    if (!spe_ctx) {
        perror("spe_context_create");
        exit(1);
    }

// b)SPEへのプログラムロード
// リンクしたSPEプログラムのシンボル名を指定する
   ret = spe_program_load(spe_ctx, &hello_spe);
    if (ret) {
        perror("spe_program_load");
        exit(1);
    }

    entry = SPE_DEFAULT_ENTRY;
// c)SPE Contextの起動
    ret = spe_context_run(spe_ctx, &entry, 0, NULL, NULL, &stop_info);
    if (ret < 0) {
        perror("spe_context_run");
        exit(1);
    }

// d)SPE Contextの廃棄
    ret = spe_context_destroy(spe_ctx);
    if (ret) {
        perror("spe_context_destroy");
        exit(1);
    }

    return 0;
}

余談 ー SPE vs SPU、PPE vs PPU

Cellのドキュメントを見ていると、SPEという用語とSPUという用語が出てきます。同様にPPEとPPUも出てきます。「PlayStation3 Linux完全攻略ガイド」によると、SPE/PPEは単一の処理エレメントを示す場合に使用し、SPU/PPUは複数の処理エレメントの総称であるとされていますが、用語の使い分けがうまくいきません。このブログの中でもSPE/SPUの使い分けがうまくできず、用語的に混乱している部分があります。

2007/7/20 追記
Cell SDKに添付のドキュメント「Cell Broadband Engine Architecture」をチェックしたところ、PPE/SPEは以下の定義となっていました。

  • SPEs, which are the combination of an SPU, a local storage area, an MFC, and an RMT → 即ち、SPU(演算コア)・LS・MFC・RMTから構成されるプロセッサ全体をSPEと呼ぶ
  • The PPEs are 64-bit PowerPC processor units (PPUs) with associated caches that conform to PowerPC Architecture → こちらも、PPU(演算コアとL1キャッシュ)・L2キャッシュから構成されるプロセッサ全体をPPEと呼ぶことになります

すなわち、SPEとSPUの関係は、SPEがSPUを包含する関係であり、「PlayStation3 Linux完全攻略ガイド」の説明は誤りということになります。

もっと言ってしまうと、ライブラリやツールでも以下の用語が見受けられます。

  • libsep2では、spe_context_createなどのように、関数名がspeで始まる
  • spu_mfcio.hでは、spu_write_out_mboxのように、マクロ名がspuで始まる
  • コンパイラなどのツール名は、ppu-gcc, spu-gccのようにppu/spuで始まる

上記も「Cell Broadband Engine Architecture」ドキュメントの定義に従うと、以下の解釈が成り立つように思えます;

  • libspeは、Synergistic Processor全体を制御するためspeで始まる
  • mailboxは、PPEとSPUがMFCを介して通信するため主体がspuである
    詳細は、
    「Developing Code For Cell - Mailboxes」のP7を参照下さい
  • コンパイラは演算コア用のコードを生成するためPPU/SPU?

PPE/SPEの用語の方が示す範囲が広いのでこちらを使っておけば無難なように思えますが、あえてPPU/SPUの用語を使用するべきケースがまだよく分かりません。なんでこんなややこしい名前にしたんだろう、、

Cellのmailbox機能

行列計算のプログラムを作成した際に、SPUプログラムを繰り返し呼び出す場合、spe_context_run()関数の呼び出しは効率が悪いということを書きました。そのため、今回はCellのMailbox機能を使用してPPU - SPU間の同期処理を実験してみました。

例えば、SPUにデーターの変換処理を依頼する場合、PPUのメモリー上に変換前のデーターを準備し、spe_context_run()関数の引数としてデーターの開始番地をSPUに通知するのが一般的です。処理すべきデーターがひとかたまりの場合、SPUをいったん起動した後、PPUはSPUの処理完了(SPUプログラムの終了)を待つだけとなるためspe_context_run()関数のオーバーヘッドは問題になりません。

一方以下のケースでは、PPU-SPU間で同期処理が必要になります:

  • データーがPPUのメモリーに入りきらないため、データーのかたまり単位に、繰り返しSPUに処理を依頼する必要がある → 画像変換など
  • 入力データーが変化する毎に、繰り返しSPUに処理を依頼する必要がある → ゲームの座標変換など?

このケースで毎回spe_context_run()関数呼び出すとオーバーヘッドが大きくなります。上記のように、繰り返しSPUに処理を依頼する場合の高速化が今回のテーマです。

これまでの処理

SPUに処理を依頼する毎に、spe_context_run()関数を使用してSPU側のプログラム自体を新規に起動していました。例えば、以下の処理イメージとなります。

  for (i = 0; i < 50000; i++) {  //5万回SPUプログラムを起動する
    // entryはspe_context_runで書き換わるため毎回設定要
    entry = SPE_DEFAULT_ENTRY;
    spe_context_run(spe_ctx1, &entry, 0, NULL, NULL, &stop_info);
  }

Mailbox機能の使用

Cellでは、PPU-SPU間で簡単なメッセージのやりとりを行うことで、PPU-SPU間の同期を行うためのmailbox機能があります。今回の例では、spe_context_run()はSPUプログラムの開始時に一回だけ呼び出し、以後SPUに処理を依頼する際にはmailbox機能を使用しました。処理の概要は以下となります:

  1. PPU --> SPU : spe_context_run()によるSPUプログラムの起動
  2. PPU <-- SPU : SPUのbusy状態を通知(spu_write_out_mboxを使用)
  3. PPU <-- SPU : SPUの処理終了後idle状態を通知(spu_write_out_mbox)
  4. PPU --> SPU : SPUに次の処理を依頼(spe_in_mbox_writeを使用)
  5. SPUプログラムは2の状態に戻る

2,3,4ではmailboxメッセージを送信する側の処理を記載していますが、受信側の処理も同様に必要となります。以下に送信受信の処理について記載します。

SPU->PPUへの状態の通知

マクロ・関数名にout_mboxという名前がついている場合、SPU->PPUへの送信 mbox(SPU outbound: SPUから見て送信)を意味します。

SPU(送信)側では、spu_mfcio.hに定義されている以下のマクロを使用します。

spu_write_out_mbox (uint32_t data)
→ spu outbound mobxの書き込み(即ち送信)を意味します
  dataにPPUに通知するSPUの状態(Busy/Idle)を設定します。

PPU(受信)側では、libspe2.hに定義されている以下のライブラリー関数を使用しました。ライブラリー関数を使用する以外に、Cellのmailbox関連レジスター(MMIO)を直接読み出すさらに高速な方法もあります。

spe_out_mbox_status(spe_context_ptr_t spe)
→spuが送信したmboxキューの受信待ち状態を確認
 spe: メッセージを読み取るSPE context

spe_out_mbox_read (spe_context_ptr_t spe, unsigned int *mbox_data,
int count)
→spuが送信したmboxメッセージの読み取り
 spe: メッセージを読み取るSPE context
 mbox_data: メッセージを格納する変数
 count: 一回の関数コールで読み出すメッセージ数

PPU->SPUへの処理依頼

マクロ・関数名にin_mboxという名前がついている場合、PPU->SPUへの送信 mbox(SPU inbound: SPUから見て受信)を意味します。

PPU(送信)側では、libspe2.hに定義されている以下のライブラリー関数を使用しました。

spe_in_mbox_write (spe_context_ptr_t spe, unsigned int *mbox_data,
int count, unsigned int behavior)
→spu inbound mobxの書き込み(即ち送信)を意味します
 spe: メッセージを送信するSPE context
 mbox_data: 送信メッセージを格納する変数
 count: 一回の関数コールで読み出すメッセージ数
 behavior: mbox書き込み処理完了まで待ち合わせを行うかの指定

SPU(受信)側では、spu_mfcio.hに定義されている以下のマクロを使用します。

spu_stat_in_mbox
→SPUの受信mboxエントリー数を返します

spu_read_in_mbox
→受信したmboxメッセージを返します。受信キューが空の場合、メッセージを受信するまでSPUが停止します。そのため、事前にspu_stat_in_mboxマクロにて受信メッセージがあることを確認します

プログラムの解説

プログラムの全体はこちらです:「spe_mbox.zip」をダウンロード

PPU側のプログラム

mainの以下の部分でSPUログラムを実行するためのスレッドを生成

    arg1.spe_ctx = spe_ctx1;
    ret = pthread_create(&thread1, NULL, run_spe_thread, &arg1);
    pthread_join(thread1, NULL);

run_spe_thread関数でspe_context_run()を呼び出しています。spe_context_run()を呼び出すと、SPUプログラムが終了するまで呼び出し側の処理は停止するため、以下のように子スレッドを生成して、子スレッド内でmboxのハンドリングを行いました。

    // SPUとの同期を制御する子threadを起動
    pthread_t thread2;
    ret = pthread_create(&thread2, NULL, rum_mobx_thread, arg);

    // SPUプログラムを起動
    entry = SPE_DEFAULT_ENTRY;
    ret = spe_context_run(arg->spe_ctx, &entry, 0, NULL, NULL, &stop_info);

run_mobx_thread関数でSPUとのメッセージハンドリングを実施。mboxを使用して5万回SPUプログラムを起動します。

void *run_mobx_thread(void *thread_arg)
{
    thread_arg_t *arg = (thread_arg_t *) thread_arg;
    int i;

    for (i = 0; i < 50000; i++) {
        // SPUのアイドル待ち
        wait_spu_idle(arg->spe_ctx);

        // SPUの再起動
        send_spe_cmd(arg->spe_ctx, SPU_RUN_CMD);
    }

    // SPUの停止
    wait_spu_idle(arg->spe_ctx);
    send_spe_cmd(arg->spe_ctx, SPU_STOP_CMD);
    return 0;   
}

inline void wait_spu_idle(spe_context_ptr_t spe_ctx){
    int ret;
    unsigned int mbox_data;
   
    do {
        do {
        } while (!spe_out_mbox_status(spe_ctx));

        ret = spe_out_mbox_read(spe_ctx, &mbox_data, 1);
        if (ret < 0) {
            perror("SPE Mbox read error");
            exit(1);       
        }
    } while (mbox_data != SPU_IDLE);
   
    return;
}

inline void send_spe_cmd(spe_context_ptr_t spe_ctx, unsigned int cmd) {
    int ret;
    unsigned int mbox_data;

    mbox_data = cmd;
    ret = spe_in_mbox_write(spe_ctx, &mbox_data, 1, SPE_MBOX_ALL_BLOCKING);
    if (ret < 0) {
        perror("SPE Mbox write error");
        exit(1);       
    }
    return;
}

SPU側のプログラム

int main(unsigned long long spe, unsigned long long argp)
{
    unsigned int spu_state;
    unsigned int ppu_cmd;
    printf("SPU started\n");

    do {
        spu_state= SPU_BUSY;
        spu_write_out_mbox(spu_state);
        // SPUの処理 →今回はやることなし

        spu_state=SPU_IDLE;
        spu_write_out_mbox(spu_state);

        // PPUからの起動待ち
        do {
        } while (!spu_stat_in_mbox());

        // PPUからのコマンドがSTOP以外の場合は処理を先頭に戻って処理を繰り返す
        ppu_cmd = spu_read_in_mbox();
    } while (ppu_cmd == SPU_RUN_CMD);

    printf("SPU stopped\n");
    return 0;
}

性能測定

プログラムの実行時間を以下に示します:

  • spe_context_runを5万回呼び出すプログラム: 6.9s
  • 今回のプログラム(mailbox使用): 0.58s

10倍以上性能が向上したことが分かります。

Cellプログラミング-DMAの高速化

前回の投稿では、Cell vs Pentium4の行列計算プログラムを、SIMD命令を使用して高速化しましたが、今回はCellのPPE - SPE間のDMA転送を最適化することでさらなる高速化を行いました。DMA転送を最適化する手法として、ダブルバッファー方式を使っています。Cellにおけるダブルバッファーの使い方として、IBM developerWorksの記事を参考にしました。

Cellでは、メインメモリーとSPE-LS間のDMA転送はMFC(Memory Flow Controller)が実行します。DMA要求はMFCにキューイングされ、SPEのプログラム実行中に並列に動作が可能です。とは言え、DMAが終了しないと次に進めない処理の場合は、以下の命令を発行してDMA終了を待ち合わせる必要があります。

  mfc_write_tag_mask(TAG_mask);
  spu_mfcstat(MFC_TAG_UPDATE_ALL);    // DMA完了までSPEプログラムが停止

SPEプログラムが停止してしまう待ち時間の間に別の仕事をSPEに行わせるテクニックがダブルバッファーということになります。ダブルバッファー方式ではDMAバッファーを2面用意し、2面のバッファーを交互に使用することで、DMA中にSPEが無駄に停止しないようにします。以前のシングルバッファーとダブルバッファー方式での処理の流れを以下に示します。

Double_buffer

プログラム的には、以下のようにダブルバッファーの処理をコーディングしています。

do {

    // Process buffer 0 (A面)
    process_data(0, start_buff_a);       // DMA buffer分の計算を実施
    start_buff_a += SPU_BUFFER/sizeof(float)*2;  // 次回の計算開始点を更新

    transfer_data(0, p_sub_a, p_sub_b, p_sub_c, remain);  // 計算結果のDMA
    p_sub_a += SPU_BUFFER;    // DMAアドレスの更新
    p_sub_b += SPU_BUFFER;
    p_sub_c += SPU_BUFFER;
    remain -= SPU_BUFFER;     // 計算すべき配列要素の残分を更新

    // Process buffer 1 (B面)
    process_data(1, start_buff_b);
    start_buff_b += SPU_BUFFER/sizeof(float)*2;

    transfer_data(1, p_sub_a, p_sub_b, p_sub_c, remain);
    p_sub_a += SPU_BUFFER;
    p_sub_b += SPU_BUFFER;
    p_sub_c += SPU_BUFFER;
    remain -= SPU_BUFFER;
} while(remain > 0);

ダブルバッファ版のソースコード全体はこちらです

ダブルバッファー導入によって計算時間は以下となりました。

Cell vs Pentium4 -SIMD演算
項目P4(SIMD)Cell
(Single buf)
1 SPE
Cell
(Single buf)
2 SPE
Cell
(Doube buf)
1 SPE
Cell
(Doube buf)
2 SPE
CPU P4 530
3.18GHz
Cell BE
3.2GHz
コンパイラ gcc 4.1.1 IBM xlc 0.8.2
最適化レベル O3 O3 O3 O3 O3
計算時間(s) 6.67 1.19 1.03 0.60 0.68

ダブルバッファー化により2倍近くの高速化ができており、効果は絶大です。

また、その他のチューニングとして以下の変更を行っています。

以前のプログラムでは、SPE内のDMAバッファーをmain関数内で宣言していました。即ち、SPEのスタックフレームとしてDMAバッファーを確保していたことになります。今回DMAバッファーをSPEプログラムの外部変数として宣言するように変更しました。この変更だけでも若干の性能改善ができています。SPEではアクセス頻度が高いデーターはスタックフレーム内には置かない方がよいと言えそうです。

考察

もともと1 SPEと2 SPEで計算時間の差がなかったのですが、ダブルバッファー化によって、とうとう1 SPEの方が計算時間が短いという結果になってしまいました。この理由については、以下に示すように、PPEからSPEプログラム(行列計算の本体)を呼び出す処理の効率が悪いと考えられます。

  • 現状のプログラムでは、先ずlinuxのpthread_create関数を使用してSPE分のlinuxスレッドを生成しています
  • さらに、linuxスレッドから、spe_context_run関数を使用してSPEプログラムを起動しています
  • 最後に、pthread_join関数を呼び出して、SPEに分割した計算の終了を待ち合わせています
  • ある程度の測定時間を得るために、1024 x 512の行列計算を1000回ループさせていますが、ループ毎に、pthread_create, spe_context_run, pthread_joinが発生しておりこの分のオーバーヘッドが発生します
  • SPEを2 unit使用すると、上記のオーバーヘッドがSPE 1 unitの2倍となるため、スレッドやspe_context管理のオーバーヘッドがSPE並列処理による高速化分を打ち消してしまったと思われます

SPEの処理を頻繁に起動するプログラムでは、その都度spe_context_runを呼び出すのではなく、PPE - SPE間のmailbox機能を使ってSPEの起動・停止を制御するなどしてより最適化を図る必要がありそうです。

また、今回のプログラムのようにSPEの処理単位が小さい処理はSPEに分割して並列度を上げても効果がないということが分かりました。このあたりは並列プログラミングの醍醐味でありかつ難しいところです。

Cell vs Pentium4 - SIMD演算編(その2)

前回に続いて行列計算プログラムのチューニングを行いました。

まずは、Pentium4版ですが、以下のように行列の初期化部分をSSE命令に置き換えました。ソース全体はこちらです。

void init_mat_sse ()
{
  int i;
  __m128  *a = (__m128 *) mat_a;
  __m128  *b = (__m128 *) mat_b;
  __m128  *c = (__m128 *) mat_c;
  __m128  offset = {4.0, 4.0, 4.0, 4.0};

  a[0] = _mm_setr_ps(0.0, 1.0, 2.0, 3.0);
  b[0] = _mm_add_ps(a[0], a[0]);
  c[0] = _mm_setzero_ps();
 
  for (i = 1; i < NUM_ROW*NUM_COL/4; i++)
  {
    a[i] = _mm_add_ps(a[i-1], offset);
    b[i] = _mm_add_ps(a[i], a[i]);
    c[i] =  _mm_setzero_ps();
  }
}

mat_a[i][j]は、以下のように数字を1づつ増やしているだけです。
  mat_a[0][0] = 0.0
  mat_a[0][1] = 1.0
  mat_a[0][2] = 2.0
  mat_a[0][3] = 3.0
  mat_a[0][4] = 4.0
       ・
       ・
  mat_a[0][511] = 511.0
  mat_a[1][0] = 512.0

SSE (SIMD)命令を使用した行列の初期化ですが、まず初期値として、単精度小数x4のベクター型ポインター aを宣言して、行列データーの格納位置であるmat_aの先頭アドレスを代入します。これで、a[0]を指定することで、mat_a[0][0]〜mat_a[0][3]の4つの要素にアクセスすることができます。

先ず、a[0] = _mm_setr_ps(0.0, 1.0, 2.0, 3.0) 命令で、mat_a[0][0]〜mat_a[0][3]に{0.0, 1.0, 2.0, 3.0}の初期値を代入します。
次に、a[1]はmat_a[0][4]〜mat_a[0][7]に対応し {4.0, 5.0, 6.0, 7.0}の値を取りますが、この値を作るためにはa[0]の各要素に4.0を足せばよいことになります。この計算は、a[i] = _mm_add_ps(a[i-1], offset) 命令で実現できます。

mat_bはmat_aの2倍の値を代入するだけです。b = a x 2でもよいのですが、b = a + aと足し算にした方が処理が早いかもと思い、b[i] = _mm_add_ps(a[i], a[i]) としています(たぶん、掛け算命令を使用しても差はないです)。

このプログラムを実行すると、なんと、計算時間が18.74秒から 6.67秒に短縮できてしまいました。現時点でのCellの計算時間は12.78秒のため、2倍以上の高速化が必要です。前回行列の足し算部分をSSE化した際は全く高速化できなかったのに対して、今回は大幅な高速化ができました。メモリーストアー命令を多用する初期化処理はSSEによるSIMD化が有効な結果となりました。

Cell版のプログラムでも、PPEで行っていた行列の初期化を同様にAltiVec (PowerPCのSIMD命令)に書き換えるつもりでしたが、この程度ではPentium4の性能に届きそうにありません。Pentium4恐るべしです。対抗策として、これまではPPEで行列の初期値を作成してSPEで足し算を実行していたのですが、初期値作成と足し算の両方をSPEで行うようにします。従って、プログラムのフローは以下となります。

Matirxaddsimd2

Cell版プログラムのソースはこちらから

SPEで行っている行列の初期化と足し算部分は以下の通りです。

vector float *vec_a  = (vector float *) mat_a_LS;  // SPE LS内のbuffer
vector float *vec_b  = (vector float *) mat_b_LS;
vector float *vec_c  = (vector float *) mat_c_LS;
vector float offset = (vector float) {4.0, 4.0, 4.0, 4.0};
vector float zero = (vector float) {0.0, 0.0, 0.0, 0.0};

for (i = 0; i < SPU_BUFFER/16; i++) {   // ループ回数の計算は手抜き
     // Initialize Matrix
     vec_a[i] = spu_add(a, offset);
     vec_b[i] = spu_add(vec_a[i], vec_a[i]);
     vec_c[i] = zero;            // P4版との性能比較上あえて初期化する

     // Compute
     vec_c[i] = spu_add(vec_a[i], vec_b[i]);
     a = vec_a[i];
}

さて性能はどうでしょうか。過去のデーターもあわせてまとめてみました。

Cell vs Pentium4 -SIMD演算
項目P4(半SIMD)P4(全SIMD)Cell(半SIMD)
2 SPE
Cell(全SIMD)
1 SPE
Cell(全SIMD)
2 SPE
CPU P4 530 3.18GHz Cell BE 3.2GHz
コンパイラ gcc 4.1.1 IBM xlc 0.8.2
最適化レベル O3 O3 O3 O3 O3
計算時間(s) 18.74 6.67 12.78 1.19 1.03
  • P4半SIMD: 行列計算部分のみSIMD化
  • P4全SIMD: 行列初期値生成・計算の両方をSIMD化
  • Cell半SIMD: 行列計算部分のみSPEでSIMD化
  • Cell全SIMD: 行列初期値生成・計算の両方をSPEでSIMD化

今回SPEでの完全SIMD化によって、P4の性能に完全勝利できました。
スペース関係で掲載しませんが、Cell用のgccを使用した場合でも2秒以下で計算が可能であり、Cell用コンパイラの最適化効果で勝ったとも言わせません。時間がかかりましたが、なんとか当初の目的を達成です。

考察

Cellはスカラー性能は平凡ですが、やはりSPEを駆使したSIMD(ベクトル)演算性能には目をみはるものがあることが分かりました。今回の性能測定からGFLOPS性能を計算すると以下となります。

GFLOPS性能
項目P4(全SIMD)Cell(全SIMD)
1 SPE
Cell(全SIMD)
2 SPE
GFLOPS 0.31 1.76 2.03

GFLOPSの計算方法は以下です。

  • 行列の初期化で3回・足し算で1回、合計要素あたり4回の浮動小数点演算を行うと考える(初期化の1処理は単なる代入ですが、計算の範疇に加えます)
  • 要素数は1024 x 512
  • ループの繰り返し回数が 1000回

即ち合計計算回数 = 4 x 1024 x 512 x 1000 = 2,097,152,000  となります
この計算回数を実行時間で割った値がGFLOPS値です。

上記の実測値に対してSPEの論理性能は、spu_add命令がクロックサイクル毎に実行できるとした場合以下の数値になると思います。

  • 一回の演算毎に、1)演算器へのデーターのロード、2)計算、3)メモリーへの計算結果のストアーと、3サイクルが必用。ロードに関しては、レジスタ内の値を使用できるるため2サイクルとする
  • SIMDによって、2サイクルで単精度浮動小数点の4要素が計算できる
  • SPEのクロックサイクルは3.2GHzである

SPEの性能 = 4 * 3.2 / 2 = 6.4GFLOPS

2007/5/24追記:
上記のGFLOPS計算に関して、一つ気がつきました。SPEも内部でパイプライン処理を行っているはずなので、ロード・計算・ストアをパイプライン処理できかつLS(Local Store)とのロード・ストアが1クロックサイクルで実行できればSPEの論理性能は4*3.2 = 12.8GFLOPSになります。

実際の演算では、計算結果のDMA転送やループ処理のオーバーヘッドがあるため、今回の性能値である1.76GFLPSはそこそこの数値だと思われます。今回のプログラムは、計算結果をメインメモリーにDMA転送するする処理の比率が大きく、メインメモリーの帯域ネックによって性能が上がらないものと思われます。それでもPS3はメインメモリーにXDRを奢っておりメモリー帯域が25.6GB/sあるため、My PCのDDRメモリー6.4GB/sに比べると4倍の帯域を持っており、この分が性能差に現れたと思われます。

演算性能に関しては、Pentium4に完勝しましたが、SPEの論理性能は25.6GFLOPSとも言われており、この値と実測値がに大きな乖離があります。論理性能は恐らく、以下の計算から求めているのだと思います。

  • 積和のように、1クロックで2演算できる命令を考える
  • SIMDでは、1クロックに4要素処理できるため、クロックあたり8回演算したことと等価になる
  • SPEのクロックが3.2GHzのため、GFLOPS = 8 x 3.2 = 25.6GFLOPS

上記の計算は、演算データーのロードや演算結果のストアを無視しており、強いて言えばレジスタ内の演算をループを使わずに繰り返した際のピーク性能となります。従って、実際の計算においてこの論理性能が発揮できることはありえないということになります。

2007/5/24追記:
こちらについても、ロード・計算・ストアをパイプライン処理で1クロックサイクル毎に実行できれば論理的には可能ですね。そのため、データーがLS内に全て格納でき、メモリー間で転送が不要であるようなデーターの局所性が極めて高いプログラムならピーク性能近くを出せるかもしれません。いつか試してみたいです。

ただ、Cellに限ったことではなく、Intelを含めて全てのSIMDプロセッサの演算性能は上記のようなロード・ストアを無視したピーク性能で計算していると思われます。従って、実性能はさておき、比較の上では意味があるのだと思います。

Pentium4の論理性能は、3GHzで12GFLOPSとされていますが、論理性能 vs 実性能比でもCellがPentium4を上回ったことになります。

Cell vs Pentium4 - SIMD演算編

前回に引き続き、行列演算のサンプルプログラムを使ったCellと Pentium4の性能比較を行います。今回は以下を目標にサンプルプログラムを改造しました。

  • SIMD (Single Instruction Multiple Data)演算を使用して処理性能を上げる
  • Cellでは複数のSPEに処理を分割して並列動作させることでさらに性能を上げる

SIMD演算では、以下のように複数の要素(単精度浮動小数点の場合4要素)に対して単一の命令で計算を行うことができます。例えば、以下の足し算を1つの命令で実行できます。
  c[0] = a[0] + b[0]
  c[1] = a[1] + b[1]
  c[2] = a[2] + b[2]
  c[3] = a[3] + b[3]

SPEのSIMD演算では、上記の計算を以下のSIMD命令で置き換えることが可能です。
  c = spu_add(a, b)

これまでなら、ループを4回まわして計算したところを、1回の計算でできてしまうわけですから行列計算の高速化にはうってつけです。前回の性能比較に使用した1024 x 512の行列の足し算プログラムをCellとPentium4用に作成しました。今回使用したソースコードを以下に示します。
 Cell用ソース「matrix-add-simd.zip」をダウンロード
 P4用ソース「matrix-add.sse.zip」をダウンロード
Pentium4は単純に行列の足し算部分をSIMD(SSE)命令に置き換えただけですが、Cellの場合はちょっとやっかいです。

SIMD演算を使用したいだけなら、PPEのAltiVec(PowerPCのSIMD演算機能)を使用する手もありますが、今回はSPEへの演算のオフロードと並列化が目的です。そのためには、SPEに処理を分割したり、PPEとSPEはメモリーを共有していないので計算用のデーターをSPEにDMA転送したりする必用があります。PLAYSTATION Linux完全攻略ガイドに掲載されているサンプルプログラムを参考にCell版のプログラムを作成しました。Cell版プログラムの概要は以下となります。

Matirxaddsimd

プログラムの実行結果は以下となりました。

Cell vs Pentium4 -SIMD演算
項目Cell(gcc)-1SPECell(gcc)-2SPECell(xlc)-1SPECell(xlc)-2SPPP4(gcc)
CPU Cell BE 3.2GHz P4 530 3.18GHz
コンパイラ ppu-gcc 4.1.1
spu-gcc 4.1.1
IBM xlc 0.8.2 gcc 4.1.1
最適化レベル O3 O3 O3 O3 O3
計算時間(s) 31.15 30.80 13.06 12.78 6.64
18.74

例によってppu-gccでコンパイルしたコードの実行結果は遅いのですが、XLCの実行結果をもってしてもP4に勝てませんはPentium4を上回りました。だた、SPUx1, SPUx2で実行時間に殆ど差がありません(表には記載していませんが、SPUx4では僅かに速度が低下してしまいます)。

(5/17追記) Pentium4 SSE版にバグがありました。行列の初期化が本来必要な量の1/4しかできていませんでした。どおりで早いはずです。バグ修正版の測定データーに上記の表を更新しました。Pentium4はスカラー演算版でも18.81sで計算を行っていたので、SSEを使用してもほとんど高速化しないということになります。この結果はちょっと謎ですが、Pentium4の場合、単精度浮動小数点の足し算レベルではFPUとSSEで性能差がないということになります。

今回のプログラムは、行列の初期値を生成するために以下のコードをPPEで実行しています。

void init_mat()
{
    int i, j;

    for (i = 0; i < NUM_ROW; i++)
    {
        for (j = 0; j < NUM_COL; j++)
        {
            mat_a[i][j] = (float)(i * NUM_COL + j);
            mat_b[i][j] = (float)((i * NUM_COL + j)*2);
            mat_c[i][j] = 0.0f;      
        }
    }
}

即ち、上記のコードで生成した行列から、mat_c[i, j] = mat_a[i, j] + mat_b[i, j]の足し算をSPUで実行します。この処理を1000回ループして実行時間を測定しているため、PPEでの初期値生成を1000回行っていることになるのですが、PPE側の初期値計算量がオーバーヘッドになっていると思われます。同様にメインメモリ - SPE間のDMAもオーバーヘッドになっている可能性があります。

Cell SDK 2.1に入っているCell SimulatorにSPEの稼働状況をグラフ化する機能があるため、この機能を使用してSPEの動作状況を調べてみました。結果は以下となります。

グラフより、時系列的に見ると、以下の順番でSPEが稼働していることが分かります。

 ①SPE0とSPE1 → ②SPE6とSPE7 → ③ SPE4とSPE5 → ④SPE2とSPE3

2つのSPEは同時に起動されている(即ち平行して動いている)と思われます。ただし、毎回異なるSPEが起動される理由は謎です(PS3用Cellのsimulatorではないため、8個のSPEが存在するのは仕様だと思います)。

Sim1_4

グラフにあるように、SPEが起動されてから次の起動までの間が、PPEにて行列の初期化を行っている時間と思われます。この時間を短縮するためにはPPEで初期値データーを生成しながらSPEにデーターを供給するように、PPEとSPEを並列動作させる必用がありそうです(もしくは、初期値生成もSPEにオフロードするか)。初期値の生成と計算を並列動作させるには大幅なプログラム修正がが必用なので今回はここまでであきらめました。

PS3のCellはSPEの計算性能を単純に足し算すると、ピーク性能で218GFLOPSをマークするとされていますが、DMAのオーバーヘッドや今回のように計算の元ネタデーター生成能力を考えると、Cellの演算性能を引き出すのは骨が折れそうです。いわゆる、Cellの性能を引き出すプログラミングは難しいというやつですね。

(5/17追記)Pentium4版プログラムのバグを修正した結果、Cell用コンパイラの最適化にすがると、現時点でもPentium4を越えることができました。Cell版プログラムの性能ネックはPPEで行っている行列の初期化にあることは分かっており、こちらにAltiVecを使用すると劇的に性能が向上することも分かっています。Pentium4は今回の結果から、行列の初期化部分をSSE化しても性能向上はないことが予想され、その場合はCellの完全勝利が可能となります。Cellの性能向上版を使用した比較は次回掲載したいと思います。

今回のプログラムではまった点

Cのプログラムを書いたのが久々だったので、ポインタや型変換などCのプログラムではまるポイントは今回一通りはまったのですが、以下の点が私的には盲点でした。

1. 構造体のパディング

Cコンパイラは、構造体をデーターアクセスの効率がよいワード境界にアライメントするために、構造体メンバーにパッドを付与します(パッドの付与のしかたは、ターゲットとするCPUのアーキテクチャーによって異なります)。そのため、構造体メンバーのバイト長を足し算した結果と、sizeof(構造体名)の値が異なる場合があることが分かりました:
例えば以下の構造体のデーターサイズは、CellやP4では28byteでなく32byteになります。すなわち、Cellではデーターが8byte境界にアライメントされるようです。
Pentium4の場合、データーサイズは28となり、4byte境界にアライメントされることが分かります。
 -- 2007/5/14 P4のアライメント条件を修正 --

typedef struct {
    unsigned long long    ea_a;   // 8 byte
    unsigned long long    ea_b;   // 8 byte
    unsigned long long    ea_c;   // 8 byte
    unsigned int size;  // 4 byte
} DMA_params_t;

2. DMAバスエラーのsimulation

CellのDMAバイト数は16バイトの倍数にする必用があるのですが、上記の構造体に誤ったパディングを設定してデータ長が40byteになったためDMA転送が失敗していました。実機で動かすとDMAのコードで「バスエラー」と表示されプログラムが異常終了するのですぐ分かるのですが、PC Linux上で動作するEclipse + simulatorでは異常終了が発生しないので問題に気がつくのに時間を要しました。EclipseのデバッグwindowにDMA statusの表示があるのでここを注視すれば分かったのだと思いますが、simulatorを過信してはいかんという事例ですね。

3. Cell SDK gccのデフォルコンパイルモード

Cellは32bitおよび64bitのコードを実行できます。YDLやFCに入っているgccはデフォルトで32bitのコードを生成しますが、SDK 2.1のppu-gccはデフォルトで64bitのコードを生成します。そのため、longのデーター長が、YDL/FCのgccでは32bit、SDKのppu-gccでは64bitになります。今回のプログラムでは問題になりませんでしたが、long変数のデーター長に依存したプログラムを書く場合は注意が必要です(コンパイラオプションに明示的に-m32をつけて32bitコードを指定する必用があります)。

より以前の記事一覧

2017年10月
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31        
無料ブログはココログ