行列計算のプログラムを作成した際に、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機能を使用しました。処理の概要は以下となります:
- PPU --> SPU : spe_context_run()によるSPUプログラムの起動
- PPU <-- SPU : SPUのbusy状態を通知(spu_write_out_mboxを使用)
- PPU <-- SPU : SPUの処理終了後idle状態を通知(spu_write_out_mbox)
- PPU --> SPU : SPUに次の処理を依頼(spe_in_mbox_writeを使用)
- 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倍以上性能が向上したことが分かります。
最近のコメント