Cell/B.E. & SpursEngine プログラミング
Home記事一覧BBS

SpursEngineでマンデルブロ集合のリアルタイムアニメーション

(更新)
2011/02/08 SPEプログラムの最内ループをループアンローリングすることによって実行速度がさらに約1.8倍速くなりました。(26fps→46fps)それでもasmVisで確認すると結構ストールが発生しているのでまだ性能向上の余地はありそうです。今回のプログラムでは構造上、極めて簡単にループアンローリングできました。(最内ループをマクロにして8回分展開しただけです。)実は手作業でループアンローリングしなくても、SPEプログラムのコンパイルオプションに-funroll-loops(自動ループアンローリングのオプション)を付ければ39fpsぐらいにはなるのですが、今回付け忘れていました。

2010/12/24 公開

SpursEngineのSPEを使ってマンデルブロ集合のリアルタイムアニメーションを行うプログラムを作りました。



動画ファイルのダウンロード:

wget http://cellspe.matrix.jp/files/spurs_mandel.mp4

ソースコードのダウンロード:spurs_mandel.tar.gz

普通のCPU向けリファレンスコードのダウンロード:mandel_cpu.tar.gz

実行方法

SpursEngineの開発環境は以下のページの方法でインストール済みであることとします。

PCにUbuntu 10.04 Lucid Lynxを「軽量」インストールする

PCにSpursEngine Linux SDKをインストールする

Fedora 14にSpursEngine Linux SDKをインストールする(64bit環境対応)

今回、追加でX Windowの開発環境もインストールします。

端末でコマンドを入力して設定作業を行います。

●Ubuntu 10.04の場合
(一行で)
sudo apt-get --no-install-recommends install libxext-dev

●Fedora 13, 14の場合
su -c 'yum install libX11-devel libXau-devel libxcb-devel libXext-devel'

●Fedora 64bit環境の場合
追加でインストールします。
su -c 'yum install libX11-devel.i686 libXau-devel.i686 libxcb-devel.i686 libXext-devel.i686'

ダウンロードしたソースコードspurs_mandel.tar.gzをホームディレクトリに置きます。

●解凍

tar xzf spurs_mandel.tar.gz

cd spurs_mandel

●コンパイル

make

●実行

./main

ソースコード

SPE側プログラム:spe.c
#include <stdio.h>
#include <spurs/spsa/spsa.h>
#include <spu_mfcio.h>
#include <spu_intrinsics.h>


#define COMMAND_QUIT 0xffffffff
// 最内側ループの回数
#define MAX_COUNT 1024


// パラメータ用データの共用体(4バイト)
typedef union
{
  int i;
  unsigned int ui;
  float f;
} mcl_cf_param_data_t;


// バッファ
unsigned char buffer[128 * 1024] __attribute__((aligned(128)));

// メッセージポートのデータ
uint32_t data_h2s[32] __attribute__((aligned(128)));
uint32_t data_s2h[32] __attribute__((aligned(128)));


// 最内ループのマクロ
#define INNERLOOP() {                                   \
    vb = spu_msub(spu_mul(vf2, va), vb, vy);            \
    va = spu_sub(va2, spu_add(vb2, vx));                \
    va2 = spu_mul(va, va);                              \
    vb2 = spu_mul(vb, vb);                              \
    vtmp = spu_add(va2, vb2);                           \
    flag = spu_cmpgt(vf4, vtmp);                        \
    vcount = spu_sub(vcount, spu_and(vui1, flag));      \
    flagui = spu_extract(spu_gather(flag), 0);          \
    if (flagui == 0) break;}


// マンデルブロ集合を計算
void mandel(void * param_arg)
{
  mcl_cf_param_data_t * param = param_arg;
  vector unsigned int vui1 = spu_splats(1u);
  vector unsigned int vmaxcount = spu_splats((unsigned int)MAX_COUNT);
  vector float vf2 = spu_splats(2.0f);
  vector float vf4 = spu_splats(4.0f);
  unsigned int i, j, k, xres, yres, width, height, page, startline, endline, color_shift, my_num, spes_num;
  // LS上のピクセルデータのポインタ(16バイト単位)
  vector unsigned int *pix;
  void *ptmp;
  // メインメモリの実効アドレス(ピクセルデータの書き込み先)
  unsigned long long addr_ea;
  float xmin, xmax, ymin, ymax, dx, dy;
  // エンディアン変換のためのシャッフルパターン
  vector unsigned char vsp_endian = {3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12};

  // パラメータ読み込み
  // マンデルブロのタスクのパラメータをセット
  // 画面の解像度
  xres = param[4].ui;
  yres = param[5].ui;
  // 計算範囲
  xmin = param[1].f - param[3].f * (float)xres / (float)yres;
  xmax = param[1].f + param[3].f * (float)xres / (float)yres;
  ymin = param[2].f - param[3].f;
  ymax = param[2].f + param[3].f;
  // 色シフト量
  color_shift = param[6].ui;
  // フレームバッファのアドレス
  addr_ea = param[7].ui;
  // SPEの通し番号
  my_num = param[8].ui;
  // 全SPE数
  spes_num = param[9].ui;

  width = xres;
  height = yres;
  startline = my_num;
  endline = yres;

  dx = (xmax - xmin) / (float)width;
  dy = (ymax - ymin) / (float)height;

  // vxmin = {xmin, xmin + dx, xmin + dx * 2, xmin + dx * 3};
  vector float vxmin = spu_splats(xmin);
  vxmin = spu_insert(xmin + dx, vxmin, 1);
  vxmin = spu_insert(xmin + dx * 2, vxmin, 2);
  vxmin = spu_insert(xmin + dx * 3, vxmin, 3);

  // vymin = {ymin, ymin, ymin, ymin};
  vector float vymin = spu_splats(ymin);

  // vdx = {dx * 4, dx * 4, dx * 4, dx * 4};
  vector float vdx = spu_splats(dx * 4);

  // vdy = {dy, dy, dy, dy};
  vector float vdy = spu_splats(dy);

  // ダブルバッファのページをリセット
  page = 0;

  // 書き込み先アドレスを求める
  addr_ea += (unsigned long long)xres * 4 * startline;

  for (i = startline; i < endline; i += spes_num)
  {
    vector float vy = spu_add(vymin, spu_mul(vdy, spu_convtf(spu_splats(i), 0)));

    // ピクセルバッファのポインタをLS上の汎用バッファの先頭にセット
    pix = (vector unsigned int *)buffer;

    // ダブルバッファのためのオフセットをプラス
    // vector unsigned int型のポインタで1024、つまり16KB
    pix += page * 1024;

    ptmp = pix;

    for (j = 0; j < width / 4; j++)
    {
      vector float vtmp;
      vector unsigned int flag;

      vector float vx = spu_add(vxmin, spu_mul(vdx, spu_convtf(spu_splats(j), 0)));

      vector float va = vx;
      vector float vb = vy;

      // va2 = va * va;
      vector float va2 = spu_mul(va, va);

      // vb2 = vb * vb;
      vector float vb2 = spu_mul(vb, vb);

      vector unsigned int vcount = vmaxcount;

      for (k = 0; k < MAX_COUNT; k+=8)
      {
        unsigned int flagui;
        // ループアンローリング
        INNERLOOP();
        INNERLOOP();
        INNERLOOP();
        INNERLOOP();
        INNERLOOP();
        INNERLOOP();
        INNERLOOP();
        INNERLOOP();
      }

      // 適当にカラーリングしてLS上のピクセルバッファにピクセルの値を書き込む
      // *pix = ((vcount + color_shift) << 12) + vcount;
      //*pix = spu_shuffle(spu_add(spu_sl(vcount, 12), vcount), spu_splats(0u), vsp_endian);
      //*pix = spu_shuffle(spu_add(spu_sl(spu_add(vcount, spu_splats(color_shift)), 12), vcount), spu_splats(0u), vsp_endian);
      // 発散しない部分にも色を付ける場合
      *pix = spu_shuffle(spu_sel(spu_add(spu_sl(spu_add(vcount, spu_splats(color_shift)), 12), vcount), spu_convtu(vtmp, 10), flag), spu_splats(0u), vsp_endian);

      // LS上のピクセルバッファのポインタを進める
      pix++;
    }

    // ダブルバッファの前回のDMAが完了するまで待つ
    mfc_write_tag_mask(1 << page);
    mfc_write_tag_update_all();
    mfc_read_tag_status();

    // ローカルメモリのフレームバッファに書き込み
    // 書き込みDMA実行。転送サイズは16バイト単位に補正
    mfc_put(ptmp, addr_ea, (width * 4) & ~15, page, 0, 0);

    // ダブルバッファを切り替え(0と1で交互に)
    page = page ^ 1;

    // 書き込み先の実効アドレスを1ライン分進める
    addr_ea += (unsigned long long)xres * 4 * spes_num;
  }

  // ダブルバッファを切り替え
  page = page ^ 1;

  // 最後のDMAが完了するまで待つ
  mfc_write_tag_mask(1 << page);
  mfc_write_tag_update_all();
  mfc_read_tag_status();
}


int main(vector unsigned int arg0, vector unsigned int arg1, vector unsigned int arg2)
{
  // このSPEの通し番号
  unsigned int my_num = spu_extract(arg0, 0);
  // 全SPE数
  unsigned int spes_num = spu_extract(arg0, 1);
  // ホストからのメッセージポート
  spsa_object_t mport_h2s = spu_extract(arg0, 2);
  // このSPEからホストへのメッセージポート
  spsa_object_t mport_s2h = spu_extract(arg0, 3);
  // メッセージポートのデータサイズ
  uint32_t mport_data_size = spu_extract(arg1, 0);
  // メッセージポートのシーケンス番号
  uint32_t seq_h2s = 0, seq_s2h = 0;

  while (1)
  {
    // メッセージ受信(パラメーター読み込み)
    spsa_wait_message_port(mport_h2s, SP3_MPORT_WAIT_NEXT_SEQ, seq_h2s, SP3_TIMEOUT_INDEFINITE, &seq_h2s, data_h2s, &mport_data_size);

    if (data_h2s[0] == COMMAND_QUIT)
    {
      // 終了コマンドが送られてきたら抜ける
      break;
    }

    data_h2s[8] = my_num;
    data_h2s[9] = spes_num;
    mandel(data_h2s);

    data_s2h[0] = data_h2s[0];

    // メッセージ送信(Hostにタスク完了を通知)
    spsa_signal_message_port(mport_s2h, SP3_MPORT_SIGNAL_ASYNC, seq_s2h, data_s2h, mport_data_size, &seq_s2h);
  }

  return 0;
}

ホスト側プログラム:main.cpp
#include <iostream>
#include <fstream>
#include <memory>
#include <sys/time.h>
#include <stdlib.h>
#include <pthread.h>
#include <X11/Xlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <X11/extensions/XShm.h>
#include <X11/Xutil.h>
#include <spurs/spha/spha.h>
#include <spurs/common/byte_order.h>
#include <spurs/sp3/sp3_cmdif.h>

using namespace std;


// 使用SPE数
#define NUM_OF_SPE 4
// 画像の幅
#define XRES 480
// 画像の高さ
#define YRES 272
#define BPP 4
#define COMMAND_QUIT 0xffffffff
#define LOOP 360
#define WINDOW_NAME "SpursEngine Mandelbrot Set"


// パラメータ用データの共用体(4バイト)
typedef union
{
  int i;
  unsigned int ui;
  float f;
} mcl_cf_param_data_t;


// SPE実行スレッド用構造体
typedef struct
{
  // セッション
  spha_session_t * session;
  // スレッド
  spha_thread_t * thread;
  // SPEのmain()に与えるパラメーター
  uint32_t * param;
  // パラメーターのサイズ
  uint32_t param_size;
} spe_thread_args_t;


// SPEプログラムファイル名
const char * spe_program_name = "spe.elf";

// メッセージポートのデータ
uint32_t data_h2s[32] __attribute__((aligned(128)));
uint32_t data_s2h[NUM_OF_SPE][32] __attribute__((aligned(128)));


// 時間計測用関数。Tick値を返す(マイクロ秒)
static inline long long mcl_get_tick_count(void)
{
  struct timeval timeprof;
  gettimeofday(&timeprof, NULL);
  return ((long long)timeprof.tv_sec * (long long)1000000 + (long long)timeprof.tv_usec);
}


// エラーチェック
// 戻り値: 0:正常 1:エラー
static inline int check_st(spha_status_t st, const char * message)
{
  int error = 0;
  if (st)
  {
    printf("Error: %s\n", message);
    printf("SPHA: %s\n", spha_util_result_code_name(st));
    printf("SP3: %s\n", spha_util_sp3_result_code_name(st));
    error = 1;
  }
  return error;
}


// SPE実行スレッド
void * spe_run_thread(void * arg)
{
  spe_thread_args_t * spearg = (spe_thread_args_t *)arg;

  // SPEスレッドを実行
  spha_resume_spe_thread(*(spearg->session), *(spearg->thread), SP3_SPE_THREAD_RESUME_FLAG_BLOCKING, SP3_SPE_THREAD_RESUME_DEFAULT_ENTRY, spearg->param, spearg->param_size, NULL, NULL, NULL);

  // スレッド終了
  pthread_exit(NULL);
}


int main(int argc, char ** argv)
{
  spha_session_t session;
  uint32_t access_attributes = SP3_OBJ_ATTR_HOST_READABLE | SP3_OBJ_ATTR_HOST_WRITABLE | SP3_OBJ_ATTR_HOST_EXECUTABLE | SP3_OBJ_ATTR_SPE_READABLE | SP3_OBJ_ATTR_SPE_WRITABLE | SP3_OBJ_ATTR_SPE_EXECUTABLE | SP3_OBJ_ATTR_SPE_MAPPED_READABLE | SP3_OBJ_ATTR_SPE_MAPPED_WRITABLE;
  float zoom, cx, cy, cz, sx, sy, sz, speed;
  unsigned long long start_tick;
  unsigned int color_shift, cs_on, page;
  cx = 0.50485;
  cy = -0.6099;
  cz = 2.0;
  sx = 0.0;
  sy = 0.0;
  sz = 0.0;
  zoom = 2.0;
  speed = 0.97;
  color_shift = 0;
  cs_on = 0;
  page = 0;

  Display * display;
  int screen;
  Window window;
  GC gc;
  XImage * image;
  XShmSegmentInfo shminfo;
  XSetWindowAttributes att;
  int depth;

  // Xサーバーに接続
  display = XOpenDisplay(NULL);

  // デフォルトスクリーンを取得
  screen = DefaultScreen(display);

  // スクリーンの深度を取得
  depth = DefaultDepth(display, screen);

  // ウィンドウを生成
  window = XCreateSimpleWindow(display, RootWindow(display, screen), 0, 0, XRES, YRES, 1, BlackPixel(display, screen), WhitePixel(display, screen));

  // ウィンドウマネージャをバイパス
  //att.override_redirect=True;
  //XChangeWindowAttributes(display, window, CWOverrideRedirect, &att);

  // ウィンドウ名をセット
  XStoreName(display, window, WINDOW_NAME);

  // 共有メモリー上のXImageを生成
  image = XShmCreateImage(display, DefaultVisual(display, screen), depth, ZPixmap, NULL, &shminfo, XRES, YRES);

  // フルカラーモードでなければ終了
  if (image->bitmap_pad != BPP * 8)
  {
    printf("Please execute in full-color mode.\n");
    exit(0);
  }

  // 共有メモリー・セグメントを生成
  shminfo.shmid = shmget(IPC_PRIVATE, image->bytes_per_line * image->height, IPC_CREAT|0777);

  // 共有メモリー・セグメントをアタッチ
  shminfo.shmaddr = image->data = (char*)shmat(shminfo.shmid, 0, 0);

  // Xサーバーからアクセス可能にする
  shminfo.readOnly = False;

  // 共有メモリーをアタッチすることをXサーバーに通知
  XShmAttach(display, &shminfo);

  // 出力バッファのsync
  XSync(display, False);

  // バッキングストアを有効にする
  att.backing_store = WhenMapped;
  XChangeWindowAttributes(display, window, CWBackingStore, &att);

  // GCを取得
  gc = DefaultGC(display, screen);

  // Copyする際にExposeイベントを送らない
  XSetGraphicsExposures(display, gc, False);

  // 描画色をセット
  XSetForeground(display, gc, BlackPixel(display, screen));

  // Exposeイベントを検出する
  XSelectInput(display, window, ExposureMask);

  // ウィンドウをマップ
  XMapWindow(display, window);

  // 出力バッファのflush
  XFlush(display);

  // セッションの作成
  check_st(spha_create_session(NULL, &session), "create");

  // 通信路を確立
  check_st(spha_connect_session(session), "connect");

  // リソースを予約
  spha_resource_description_t res;
  res.num_spe = NUM_OF_SPE;
  res.num_decm = 0;
  res.num_dech = 0;
  res.num_encm = 0;
  res.num_ench = 0;
  check_st(spha_reserve_resources(session, 100000, 0, &res), "reserve");

  // SPEプログラムファイルをオープン
  ifstream fp(spe_program_name, ios::binary);

  // プログラムサイズを調べる
  fp.seekg(0, ios::end);
  uint32_t spe_elf_size = fp.tellg();
  fp.seekg(0, ios::beg);

  // プログラムサイズを128の倍数に揃える
  spe_elf_size = (spe_elf_size + 127) & ~127;

  // ホスト側バッファを確保(128バイト・アライン)
  char * buffer_elf;
  int err = posix_memalign((void **)&buffer_elf, 128, spe_elf_size);
  if (err)
  {
    abort();
  }

  // SPEプログラムファイルをロード
  fp.read(buffer_elf, spe_elf_size);

  // SpursEngine側ローカルメモリ領域の確保
  // プログラム用
  spha_object_t buffer_obj_elf;
  spha_create_memory(session, access_attributes, 0, 0, spe_elf_size, 0, &buffer_obj_elf);
  // データ用
  uint32_t buffer_size = 0x1800000;
  spha_object_t buffer_obj;
  spha_create_memory(session, access_attributes, 0, 0, buffer_size, 0, &buffer_obj);

  // メモリオブジェクトをマップ
  uint32_t buffer_ea, buffer_ea_elf;
  spha_map_memory(session, buffer_obj_elf, 0, 0, 0, spe_elf_size, 0, &buffer_ea_elf);
  spha_map_memory(session, buffer_obj, 0, 0, 0, buffer_size, 0, &buffer_ea);

  // SpursEngine側ローカルメモリに転送
  uint32_t transfer_size = spe_elf_size;
  spha_data_transfer_to(session, buffer_obj_elf, 0, buffer_elf, &transfer_size);

  // SPEプログラムローダに与える引数をセット
  // エンディアン変換して渡す
  uint32_t loader_arg[4];
  // プログラムデータの実効アドレス
  loader_arg[0] = spurs_convert_ui32(buffer_ea_elf);
  // プログラムデータのサイズ
  loader_arg[1] = spurs_convert_ui32(spe_elf_size);
  loader_arg[2] = 0;
  loader_arg[3] = 0;

  // メッセージポート作成
  // mport: ポート名(ID)
  // seq: シーケンス番号
  // h2sはホストからSPEへの送信用
  // s2hはSPEからホストへの送信用
  spha_object_t mport_h2s, mport_s2h[NUM_OF_SPE];
  uint32_t mport_data_size = 128;
  uint32_t seq_h2s, seq_s2h[NUM_OF_SPE];
  spha_create_message_port(session, access_attributes, mport_data_size, &mport_h2s);
  seq_h2s = 0;
  for (int i = 0; i < NUM_OF_SPE; i++)
  {
    spha_create_message_port(session, access_attributes, mport_data_size, &mport_s2h[i]);
    seq_s2h[i] = 0;
  }

  // SPEのmain()に与えるパラメーター
  uint32_t arg[NUM_OF_SPE][12];

  spha_thread_t spe_th[NUM_OF_SPE];
  for (int i = 0; i < NUM_OF_SPE; i++)
  {
    // SPEスレッドを作成
    spha_create_spe_thread(session, access_attributes, 0, SP3_SPE_LOADER_DEFAULT, loader_arg, sizeof(loader_arg), &spe_th[i]);
  }

  spe_thread_args_t spe_arg[NUM_OF_SPE];
  pthread_t pth[NUM_OF_SPE];
  for (int i = 0; i < NUM_OF_SPE; i++)
  {
    // SPEの通し番号
    arg[i][0] = spurs_convert_ui32((uint32_t)i);
    // 全SPE数
    arg[i][1] = spurs_convert_ui32(NUM_OF_SPE);
    // メッセージポートの名前(ID)
    // (ポート名はエンディアン変換しない)
    arg[i][2] = mport_h2s;
    arg[i][3] = mport_s2h[i];
    // メッセージポートのデータのサイズ
    arg[i][4] = spurs_convert_ui32(mport_data_size);

    // SPEスレッドを実行
    spe_arg[i].session = &session;
    spe_arg[i].thread = &spe_th[i];
    spe_arg[i].param = arg[i];
    spe_arg[i].param_size = sizeof(arg);

    pthread_create(&pth[i], NULL, &spe_run_thread, &spe_arg[i]);
  }

  // 時間計測開始
  start_tick = mcl_get_tick_count();

  zoom = 2.0f;

  for (int j = 0; j < LOOP; j++)
  {
    zoom *= speed;
    color_shift++;
    // SPEに渡すパラメータをセット
    mcl_cf_param_data_t var;
    // コマンド
    var.ui = 0;
    data_h2s[0] = spurs_convert_ui32(var.ui);
    // 中心のx座標
    var.f = cx;
    data_h2s[1] = spurs_convert_ui32(var.ui);
    // 中心のy座標
    var.f = cy;
    data_h2s[2] = spurs_convert_ui32(var.ui);
    // 計算範囲
    var.f = zoom;
    data_h2s[3] = spurs_convert_ui32(var.ui);
    // 画面のx方向の解像度
    var.ui = XRES;
    data_h2s[4] = spurs_convert_ui32(var.ui);
    // 画面のy方向の解像度
    var.ui = YRES;
    data_h2s[5] = spurs_convert_ui32(var.ui);
    // 色シフト量
    var.ui = color_shift;
    data_h2s[6] = spurs_convert_ui32(var.ui);
    // フレームバッファのアドレス
    var.ui = buffer_ea + page * XRES * YRES * BPP;
    data_h2s[7] = spurs_convert_ui32(var.ui);

    // SPE演算開始
    spha_signal_message_port(session, mport_h2s, SP3_MPORT_SIGNAL_ASYNC, seq_h2s, &data_h2s, mport_data_size, &seq_h2s);

    // SpursEngineローカルメモリ上の
    // フレームバッファを切り替え(0と1で交互に)
    page = page ^ 1;

    // SpursEngine側ローカルメモリからフレームバッファに転送
    transfer_size = XRES * YRES * BPP;
    spha_data_transfer_from(session, buffer_obj, page * XRES * YRES * BPP, image->data, &transfer_size);

    // Imageを書き込む
    XShmPutImage(display, window, gc, image, 0, 0, 0, 0, XRES, YRES, False);
    XFlush(display);
    XSync(display, False);

    // 演算終了を待つ
    for (int i = 0; i < NUM_OF_SPE; i++)
    {
      spha_wait_message_port(session, mport_s2h[i], SP3_MPORT_WAIT_NEXT_SEQ, seq_s2h[i], SP3_TIMEOUT_INDEFINITE, &seq_s2h[i], &data_s2h[i], &mport_data_size);
    }
  }

  // 経過時間表示
  float calctime = (float)(mcl_get_tick_count() - start_tick) / 1000000.0f;
  float fps = (float)LOOP / calctime;
  printf("time: %f sec\n", calctime);
  printf("fps: %f\n", fps);

  // 終了コマンド送信
  data_h2s[0] = spurs_convert_ui32(COMMAND_QUIT);
  spha_signal_message_port(session, mport_h2s, SP3_MPORT_SIGNAL_ASYNC, seq_h2s, &data_h2s, mport_data_size, &seq_h2s);

  // SPE実行スレッドの終了を待つ
  for (int i = 0; i < NUM_OF_SPE; i++)
  {
    pthread_join(pth[i], NULL);
  }

  // SPEスレッドの破棄
  for (int i = 0; i < NUM_OF_SPE; i++)
  {
    spha_delete_spe_thread(session, spe_th[i]);
  }

  // メモリオブジェクトをアンマップ
  spha_unmap_memory(session, buffer_ea_elf, 0);
  spha_unmap_memory(session, buffer_ea, 0);

  // SpursEngine側ローカルメモリ領域を解放
  spha_delete_object(session, buffer_obj_elf);
  spha_delete_object(session, buffer_obj);

  // セッションの通信路を切断
  spha_close_session(session);

  // セッションの破棄
  spha_delete_session(session);

  // ホスト側バッファを解放
  free(buffer_elf);

  // 共有メモリーをデタッチすることをXサーバーに通知
  XShmDetach(display, &shminfo);

  // XImageを解放
  XDestroyImage(image);

  // 共有メモリー・セグメントをデタッチ
  shmdt(shminfo.shmaddr);
  shmctl(shminfo.shmid, IPC_RMID, 0);

  // ウィンドウを解放
  XDestroyWindow(display, window);

  // Xサーバーから切断
  XCloseDisplay(display);

  return 0;
}

プログラム解説

マンデルブロ集合の計算を最大4基のSPEを使って並列実行させます。

出力画像のY軸方向に1ラインごとに処理を分割し、上から0ライン目をSPE0に、1ライン目をSPE1に、2ライン目をSPE2に、3ライン目をSPE3に、4ライン目をSPE0に・・・というように割り振ってまんべんなく負荷が分散するようにします。(大きく4つのブロックに分けたりすると画面の中央部を割り振られたSPEに処理が集中してしまうため、このようにしています。)

ホストとSPEの通信、同期にはメッセージポートを使用しました。

ホストからSPEへの指示出し、パラメーター受け渡しは時間短縮のため1つのメッセージポート(mport_h2s)でまとめて行い、SPEからホストへの演算終了通知はSPEごとにメッセージポートを作って(mport_s2h[NUM_OF_SPE])それぞれ通知させています。

SPEでの演算を待っている間にホストがフレームバッファの転送や描画関連の処理をバックグラウンドで行えるよう、フレームバッファを2面分用意しています。

NUM_OF_SPEを1にしてコンパイルすれば、4ウィンドウまで同時に起動してそれぞれ別々にアニメーションさせることができます。

プログラミング上の注意点など

●同期のコストについて

SpursEngineはCell/B.E.に比べると同期のコストがかなり高いです。例えばメッセージポートの場合、輻輳が発生していない条件でも1つのポートあたりSPE側の送信に70マイクロ秒、受信に50マイクロ秒程度、ホスト側の送信に350マイクロ秒、受信に250マイクロ秒程度かかります。これは到達までの遅延時間を含まない、メッセージポートの関数の入口から出口までの正味の時間です。このため、例えば4つのSPEからのメッセージをホストが待った場合、それだけで1ミリ秒程度かかってしまいます。(これはSpursEngineの特性というよりはSpursEngineがPCI Express x1経由で接続されていることに起因していると思われますが。)

また、SPE同士の同期についても、ローレベルなシグナルやメールボックスの使用に必要なプロブレムステートエリアの取得APIが用意されていないため、やはり高価な同期コマンドを用いることになります。

そのため、SpursEngineのプログラミングでは、できる限り同期を排除したり、コア間での通信の粒度を極めて大きく設計する(少なくとも100ms以上)などの工夫が必要になるでしょう。

このプログラムのようにリアルタイムな(16ms程度以下の粒度の)アプリケーションの場合、いくらかのパフォーマンスへの影響は避けられないと思われます。

●エンディアン変換について

ホストがx86系の場合、ホストがリトルエンディアンでSPEはビッグエンディアンなので、基本的に数値やデータはエンディアン変換して受け渡しする事になるのですが、メッセージポートやメモリオブジェクトなどのオブジェクト名(ID)だけはエンディアン変換せずにそのまま渡さなければいけません。

●SPEスレッドの実行タイミングについて

複数のSPEに対してノンブロッキングでspha_resume_spe_thread()を呼び出し、spha_wait_spe_thread()でSPEスレッドの終了を待った場合、SPEプログラムが実行されるタイミングは一見spha_resume_spe_thread()の直後であるように思えますが、実際に動き出すのはいずれかのSPEに対してspha_wait_spe_thread()が発行された直後となります。

このため、ホスト側プログラムでspha_resume_spe_thread()とspha_wait_spe_thread()の間に処理を入れても期待通りには実行されません。

また、spha_wait_spe_thread()ではSP3_SPE_THREAD_NAME_ANYを指定しないと複数のSPEが同時に動かず、一つずつ逐次的に実行されてしまいます。

(これらの動作はUbuntu10.04、Fedora13,14で確認していますが、過去のバージョンの環境は未確認です。)

このため、SPEスレッドの実行はSPEごとにpthreadで新たなスレッドを作り、その中から呼び出すようにしたほうがいいでしょう。このプログラムでは別スレッドの中からspha_resume_spe_thread()をブロッキングで呼び出し、spha_wait_spe_thread()は使用しないようにしています。