CTKによるコア間通信と同期

出典: CTK: Cell ToolKit Library

CTKユーザマニュアル」に戻る

目次

PPEとSPEの間でメッセージをやり取りする

  • 今度は、SPEプログラムを毎回起動して1つだけ文字列を出力させるのではなく、PPEプログラムから指示があるたびに指定された文字列を表示させるようにしてみましょう。
  • この例では、SPEプログラムの初期化と終了はプログラムの最初と最後の一回だけになります。PPEプログラムは、SPEプログラムの初期化が終わったらメインループに入り、必要があれば(例えばユーザなどから指示が入力されるたびに)都度SPEに文字列の出力を依頼します。

PPEソースコードの骨格

#include <stdio.h>
#include <ctk.h>
#include "spe-hello-embed.h"  /* generated header file for CEOSF */

int main() {
    ctk_spe_thread_t spe;
    ctk_spe_thread_create(&spe, &spe_hello, NULL, NULL, 0);

    while (1) {
        ユーザ入力などのイベントを待つ
        if (ユーザ入力が終了指示だったら)
            SPEに終了を通知してループを抜ける
        イベントに応じてSPEに仕事を依頼
    }

    ctk_spe_thread_wait(spe, NULL);
    return 0;
}
  • SPE側のプログラムも同様に、main関数はPPEからの指示を待つループで構成されることになります。ここでは、プログラムを簡単にするために決まった文字列があらかじめいくつか用意されていて、PPEから指示されたメッセージ番号(cmd)に従ってその文字列を出力することにします。

SPEソースコードの骨格

#include <stdio.h>
#include <ctk_spu.h>

const char *msgs[] = {
    "Hello, World!",                /* Message 1 */
    "My first cell programming",    /* Message 2 */
    "It's not such difficult" };    /* Message 3 */

int main(unsigned long long speid, unsigned long long argp) {
    while (1) {
        PPEからの指示待ち - cmdを受け取る

        /* cmdが'q'だったらループを抜けて終了 */
        if (cmd == 'q' || cmd == 'Q')
            break;

        /* 指定された番号の文字列を出力 */
        printf("%s\n", msgs[cmd]);
    }
    return 0;
}
  • CTKを使ってSPEとPPEの間で制御メッセージなどの小さいデータをやり取りするには、「メールボックス」「シグナル通知」「CTKキュー」などの方法があります。
    • 「メールボックス」では32ビットのデータをPPEとSPEの間で双方向に送受することができます。
    • 「シグナル通知」では32ビットのデータをSPEに通知することができます。SPEからPPEへの通信には使えませんが、メモリにマップされたレジスタを介することでSPE間でシグナル通信しあうことが可能です。
    • 「CTKキュー」では64ビットのデータをPPEとSPEの間、あるいはSPE間で双方向に送受することができます。(実際には、CTKには任意サイズのデータを送受できるよりジェネリックなキュー(genericq)も用意されています)
  • 「メールボックス」と「シグナル通知」はCell/B.E.の機構を使ったメッセージングの方法で、CTKに依存する方法ではありません。「CTKキュー」はCTKが提供する同期キューを使ったライブラリAPIを使う方法で、内部ではアトミック転送という方法を使ったDMA転送により実現しています。
  • どの方法を使う場合でも、メッセージングを行う場合は(DMA転送の場合とは異なり)通信を行う両側のプログラムが「送る」「受け取る」あるいは「書く」「読む」のような対になる操作を協調して行う必要があることに注意してください。一方で、DMA転送に比べて、複数プログラムの間で協調的な操作を記述できるため、制御コマンドやイベント通知の送受を自然に記述することができます。
  • 以下にそれぞれの方法を簡単に説明します。

メールボックス通信

  • 「メールボックス」では32ビットのデータをPPEとSPEの間で双方向に送受することができます。
実際には、Cell/B.E.にはPPEからSPEにメッセージを送ることが出来る「Inboundメールボックス」、SPEからPPEにメッセージを送ることが出来る「Outboundメールボックス」「Outbound割り込みメールボックス」の3種類の片方向のメールボックスがあります。
  • CTKではメールボックス通信は ctk_mbox_write(メールボックスへのメッセージ書き込み)、ctk_mbox_read(メールボックスからのメッセージ読み込み)のようなAPIを使って記述できます。これらのAPIはPPE/SPE共通です。
SPEからPPEへのメールボックス通信には、ctk_intr_mbox_write (SPE側) と ctk_intr_mbox_read (PPE側) というAPIを使うこともできます。これらのAPIは割り込みメールボックスを使って通信を行うもので、後述するブロッキング通信の場合には通常のメールボックスよりも無駄の少ない手法です。
  • PPE側では、ctk_mbox_write, ctk_mbox_read, ctk_intr_mbox_readなどのメールボックス関数はすべてノンブロッキングな関数です。成功した場合には CTK_SUCCESS が、失敗したり読み書きすることができなかったりした場合には CTK_ERROR_NO_DATA などのエラーメッセージが返されます。
  • PPE側でメッセージの読み書きが成功するまで処理をブロックしたい場合は ctk_mbox_write_blockctk_mbox_read_blockctk_intr_mbox_read_block のようなAPIを使ってください(注: SPE側で呼ぶ場合は常にブロッキング処理となります)。
  • PPE側プログラムでは、ctk_mbox_writectk_mbox_readは第一引数としてSPEスレッドあるいはSPEコンテキストを取ります(どちらの場合も同じAPIが使えます)。

PPEソースコード

  • PPEソースコードは例えば次のようになります(異常時の処理は省いています)。
#include <ctk.h>
#include "spe-hello-embed.h"

int main() {
#include <stdio.h>
#include <ctk.h>
#include "spe-hello-embed.h"  /* generated header file for CEOSF */

int main() {
    unsigned int cmd;
    ctk_spe_thread_t spe;
    ctk_spe_thread_create(&spe, &spe_hello, NULL, NULL, 0);

    while (1) {
        cmd = get_user_command();
        ctk_mbox_write_block(spe, cmd);
        if (cmd == 'q' || cmd == 'Q')
            break;
    }

    ctk_spe_thread_wait(spe, NULL);
    return 0;
}

SPEソースコード

  • 上のPPEソースコードに対応するSPEソースコードは次のようになります。
#include <stdio.h>
#include <ctk_spu.h>

const char *msgs[] = {
    "Hello, World!",                /* Message 1 */
    "My first cell programming",    /* Message 2 */
    "It's not such difficult" };    /* Message 3 */

int main(unsigned long long speid, unsigned long long argp) {
    unsigned int cmd;
    while (1) {
        ctk_mbox_read(&cmd);

        /* cmdが'q'だったらループを抜けて終了 */
        if (cmd == 'q' || cmd == 'Q')
            break;

        /* 指定された番号の文字列を出力 */
        printf("%s\n", msgs[cmd]);
    }
    return 0;
}
SPE プログラムからメールボックス通信するための主なAPI
ctk_mbox_read メールボックスを読む (ブロッキング)
ctk_mbox_write メールボックスに書き込む (ブロッキング)
ctk_intr_mbox_write 割り込みメールボックスに書き込む (ブロッキング)


PPE プログラムからメールボックス通信するための主なAPI
ctk_mbox_write メールボックスに書き込む (ノンブロッキング)
ctk_mbox_read メールボックスを読む (ノンブロッキング)
ctk_intr_mbox_read 割り込みメールボックスを読む (ノンブロッキング)
ctk_mbox_write_block メールボックスに書き込む (書けるまでブロッキング)
ctk_mbox_read_block メールボックスを読む (読めるまでブロック)
ctk_intr_mbox_read_block 割り込みメールボックスを読む (読めるまでブロック; ctk_mbox_read_blockに比べブロック中にビジーウェイトせず割り込みを待つので一般的に効率が良い)

シグナル通知

  • 「シグナル通知」では32ビットのデータをSPEに通知することができます。後述するORモードによる方法を使うことで、複数の状態をビットフィールドを使ってSPEに通知するような使い方もできます。
  • シグナル通知には「レジスタ1」と「レジスタ2」という2つのレジスタを使うことができます。これらのレジスタはそれぞれ独立した値を保持し、複数の用途に、あるいは複数の相手とのやり取りなどに活用することができます。
  • CTKでシグナル通知を行うには、PPEからはctk_signal_write_reg1あるいはctk_signal_write_reg2というAPIを、SPEプログラムではctk_signal_read_reg1あるいはctk_signal_read_reg2というAPIを使います。
PPE側プログラムのAPIの第一引数は、SPEスレッドでもSPEコンテキストでも構いません。
  • これらのAPIはブロッキングです。例えば、SPEでctk_signal_read_regnを呼び出すと、シグナル通知レジスタnに誰かが書き込むまで呼び出しはブロックします。

PPEソースコード

  • シグナル通知を使うと、PPEソースコードは例えば次のように書けます(異常時の処理は省いています)。
#include <ctk.h>
#include "spe-hello-embed.h"

int main() {
#include <stdio.h>
#include <ctk.h>
#include "spe-hello-embed.h"  /* generated header file for CEOSF */

int main() {
    unsigned int cmd;
    ctk_spe_thread_t spe;
    ctk_spe_thread_create(&spe, &spe_hello, NULL, NULL, 0);

    while (1) {
        cmd = get_user_command();
        ctk_signal_write_reg1(spe, cmd);
        if (cmd == 'q' || cmd == 'Q')
            break;
    }

    ctk_spe_thread_wait(spe, NULL);
    return 0;
}

SPEソースコード

  • 上のPPEソースコードに対応するSPEソースコードは次のようになります。
#include <stdio.h>
#include <ctk_spu.h>

const char *msgs[] = {
    "Hello, World!",                /* Message 1 */
    "My first cell programming",    /* Message 2 */
    "It's not such difficult" };    /* Message 3 */

int main(unsigned long long speid, unsigned long long argp) {
    unsigned int cmd;
    while (1) {
        ctk_signal_read_reg1(&cmd);

        /* cmdが'q'だったらループを抜けて終了 */
        if (cmd == 'q' || cmd == 'Q')
            break;

        /* 指定された番号の文字列を出力 */
        printf("%s\n", msgs[cmd]);
    }
    return 0;
}
SPE プログラムでシグナル通知を読むための主なAPI
ctk_signal_read_reg1 シグナル通知レジスタ1を読む(ブロッキング)
ctk_signal_read_reg2 シグナル通知レジスタ2を読む(ブロッキング)
ctk_signal_send 他のSPEのシグナル通知レジスタに書き込む (ctk_spe_map_signal1あるいはctk_spe_map_signal2で得られるアドレスを指定する)


PPE プログラムからシグナル通知を書くための主なAPI
ctk_signal_write_reg1 シグナル通知レジスタ1に書き込む(ブロッキング)
ctk_signal_write_reg2 シグナル通知レジスタ2に書き込む(ブロッキング)
  • シグナル通知では、レジスタを「上書き」あるいは「OR」モードに設定することで、シグナルレジスタの値を次のうちどちらかの方法で書き換えることができます。
    • 上書きモード:シグナル通知レジスタの値を新しい値で上書きする。例えば、nという値を書いてからmという値を書いた場合、レジスタの値はmになる
    • ORモード:シグナル通知レジスタの値を元の値とORして書き込む。例えば、nという値を書いてからmという値を書いた場合、レジスタの値はnmをORした値になる
  • 書き込みモードを設定するには、SPEスレッドを使う場合はctk_spe_thread_createの最後の引数(第5引数)に、SPEコンテキストを使う場合はctk_spe_context_createの第2引数に次のようなフラグを指定します。何も指定しなかった場合、デフォルトは上書きモードになります。複数のフラグをorして指定することももちろん可能です。
    • CTK_SPE_CFG_SIGNOTIFY1_OR: シグナル通知レジスタ1をORモードにする
    • CTK_SPE_CFG_SIGNOTIFY2_OR: シグナル通知レジスタ2をORモードにする

CTKキュー

  • 「CTKキュー」では64ビットのデータをPPEとSPEの間、あるいはSPE間で双方向に送受することができます。
  • CTKキューはDMA転送を使う方法のため、最初にPPEプログラムでキューの領域を用意する必要があります。CTKキューを作るには ctk_queue_create というAPIを使います。このAPIは、引数としてキューサイズの最大長(いくつのキューアイテムをキュー内に保持できるか)を指定します。CTKキューでは最大127個までのアイテムを保持させることが可能です。
  • 一度キューを作ると、ctk_queue_enqというAPIによってPPEあるいはSPEからキューにアイテムを入れる(エンキューする)ことができます。逆に、キューのアイテムを取り出す(デキューする)にはctk_queue_deqというAPIを使います。
  • キューでは、最初にキューに入れたアイテムが必ず最初にキューから取り出されます (FIFO: First In First Out)。キューはCell/B.E.全体で保持されますので、キューへの出し入れはPPE, SPE任意のプログラムから行うことが可能です。

PPEソースコード

  • CTKキューを使ったPPEソースコードの例を以下に示します(異常時の処理は省いています)。
#include <ctk.h>
#include "spe-hello-embed.h"

int main() {
#include <stdio.h>
#include <ctk.h>
#include "spe-hello-embed.h"  /* generated header file for CEOSF */
#define QSIZE  8              /* とりあえずキューサイズを8とする */

int main() {
    unsigned long long cmd;   /* CTKキューで送受するため64ビット値を宣言 */
    ctk_queue_ea_t queue;     /* CTKキュー参照型の変数を宣言 */
    ctk_spe_thread_t spe;
    ctk_queue_create(&queue, QSIZE);  /* キューの作成 */

    /* キューの参照型を引数として渡す */
    ctk_spe_thread_create(&spe, &spe_hello, (void*)(unsigned long)queue, 
        NULL, 0);

    while (1) {
        cmd = get_user_command();
        ctk_queue_enq(queue, cmd);
        if (cmd == 'q' || cmd == 'Q')
            break;
    }

    ctk_spe_thread_wait(spe, NULL);
    ctk_queue_destroy(queue);
    return 0;
}

SPEソースコード

  • 上のPPEソースコードに対応するSPEソースコードは次のようになります。
#include <stdio.h>
#include <ctk_spu.h>

const char *msgs[] = {
    "Hello, World!",                /* Message 1 */
    "My first cell programming",    /* Message 2 */
    "It's not such difficult" };    /* Message 3 */

int main(unsigned long long speid, unsigned long long argp) {
    unsigned long long cmd;   /* CTKキューで送受するため64ビット値を宣言 */
    ctk_queue_ea_t queue = argp;  /* CTKキュー参照型を引数からセット */
    while (1) {
        ctk_queue_deq(queue, &cmd);

        /* cmdが'q'だったらループを抜けて終了 */
        if (cmd == 'q' || cmd == 'Q')
            break;

        /* 指定された番号の文字列を出力 */
        printf("%s\n", msgs[cmd]);
    }
    return 0;
}
  • ctk_queue_enqctk_queue_deqはどちらもブロッキングなAPIです。キューがいっぱいなとき(ctk_queue_createで指定した最大長分のアイテムがキュー内に溜まっているとき)にctk_queue_enqを呼び出すと、誰かがキューからアイテムを取り出して空きができるまでブロックします。逆に、キューが空なときにctk_queue_deqを呼び出すと、誰かがキューにアイテムを入れるまでブロックします。
  • キューに対してノンブロッキングな操作を行いたい場合、ctk_queue_tryenqctk_queue_trydeqというAPIを使うことができます。これらのAPIは、操作ができない場合には何もせず、返り値としてエラーコード (CTK_ERROR_RETRYやCTK_ERROR_BUSY) を返します。
CTKキューを操作するための主なAPI
ctk_queue_create キューの作成 (PPEのみ)
ctk_queue_destroy キューの破棄 (PPEのみ)
ctk_queue_enq キューにアイテムを入れる (キューが一杯ならブロック)
ctk_queue_deq キューからアイテムを出す (キューが空ならブロック)
ctk_queue_tryenq キューにアイテムを入れる (キューが一杯ならエラーを返す)
ctk_queue_trydeq キューからアイテムを出す (キューが空ならエラーを返す)

SPEスレッドグループを使う

  • 複数のSPEスレッドに同じプログラムを実行させて、引数などで渡すデータだけ変えて並列実行するようなプログラムのことを、SPMD (Single Program Multiple Data) タイプのプログラムと呼びます。SPMDプログラムは最も典型的な並列化のタイプの1つで、非常に多くの並列プログラムがSPMDあるいは類似の構成を取ります。
  • CTKでは、SPMDやその他のタイプの「1まとまりの」複数のSPEスレッドを簡単に扱えるように、SPEスレッドグループという概念が用意されています。
  • CTKのSPEスレッドグループではSPMDタイプ以外のスレッドも扱えますが、ここではSPMDタイプのプログラムについてだけ説明します。

PPEソースコード

  • "Hello, World"プログラムのPPEソースコードをSPEスレッドグループを使って書き直すと、以下のようになります(エラー処理は省いています):
#include <stdio.h>
#include <ctk.h>
#include "spe-hello-embed.h"
#define NSPE    6

int main() {
    int i, status[NSPE];
    ctk_spe_thread_group_t group;
    ctk_spe_thread_group_create_spmd(&group, NSPE, &spe_hello, NULL, NULL, 0);

    printf("[PPE] Hello, World!\n");

    ctk_spe_thread_group_wait(group, status);
    return 0;
}
このプログラムでは、ctk_spe_thread_group_create_spmdctk_spe_thread_group_waitというAPIを使って、複数SPEスレッドの生成・開始と終了待ちを一度に行っています。
  • あるSPEスレッドグループの中に含まれるSPEスレッドの数は、ctk_spe_thread_group_get_threads_countという関数で取得できます。
    printf("The group contains %d SPE threads.\n",
        ctk_spe_thread_group_get_threads_count(group));

SPEソースコード

  • SPEスレッドグループを使った場合、SPEプログラムにスレッドグループ内でのSPEの番号(ランク, rank)を示す値が自動的に渡されます。この値はSPEのmain関数の第3引数を通して、SPEの環境情報として渡されます。
  • SPEスレッドグループのランクはSPEプログラム内でctk_env_get_rank()というAPIを呼ぶことで取得できます。ランクを使ってSPEの番号と"Hello, World"を一緒に出力するSPEプログラムを書き直すと次のようになります:
#include <stdio.h>
#include <ctk_spu.h>

int main(unsigned long long speid, unsigned long long argp,
         unsigned long long envp)
{
    ctk_env_init(&envp);
    printf("[SPE %d] Hello, World!\n", ctk_env_get_rank());
    return 0;
}
  • CTKライブラリが渡すSPEの環境情報を使うには、main関数の第3引数で渡された値を一度ctk_env_init関数に渡して初期化する必要があることに注意してください。
  • ctk_env_init関数は、SPEの環境情報としてTimebaseレジスタのクロック値の初期化なども行います。Timebaseレジスタのクロック値はSPE上でのスリープ(ctk_sleep)やプロファイル結果の実時間値への変換に影響しますので、SPEプログラムの(CTKフレームワーク内での)移植性を高めるためにはSPEプログラムの先頭でなるべく呼ぶようにしてください。


PPEプログラムでSPEスレッドグループを使うための主なAPI
ctk_spe_thread_group_create_spmd SPEスレッドグループを生成して実行する
ctk_spe_thread_group_wait SPEスレッドグループの終了を待ち、返り値を取得する
ctk_spe_thread_group_get_threads_count SPEスレッドグループの中に含まれるSPEスレッドの数を取得する
ctk_signal_group_write_reg1 スレッドグループ中の全スレッドにシグナル通知する
ctk_signal_group_write_reg2 スレッドグループ中の全スレッドにシグナル通知する
ctk_mbox_group_write スレッドグループ中の全スレッドのメールボックスにメッセージを1つ書く
ctk_mbox_group_write_block スレッドグループ中の全スレッドのメールボックスにメッセージを1つ書く(ブロッキング)
ctk_mbox_group_read_block スレッドグループ中の全スレッドのメールボックスからメッセージを1つ読む


SPEプログラムで環境情報を取得するためのAPI
ctk_env_init SPEの環境情報を初期化する
ctk_env_get_rank スレッドグループ内でのSPEの番号を取得する


DMA転送用の領域を動的に確保する

  • DMA転送を行うメモリ領域は、基本的には16バイト単位でアラインされている必要があります。また、最適な性能を出すには、128バイトアラインされている必要があります。
  • グローバル変数やスタティック変数のメモリ領域については、変数や型宣言のところで上の例で示したような__attribute__((aligned(N)))というgccの拡張記法を使うことでアラインを調整できます。
  • スタック上やヒープ領域にアラインされたメモリ領域を確保したい場合、CTKの次のようなAPIを使うことができます。このAPIはPPEソースコードでもSPEソースコードでも共通で使えます。

ヒープ領域にアラインされたメモリ領域を確保する

  • ヒープ領域(mallocなどで確保される領域)にアラインされたメモリ領域を動的に確保したい場合、次のように記述します:
    void *ptr = ctk_malloc_align(size, align);
  • ctk_malloc_align関数の第1引数には確保したいバイト数(sizeof(data)など)を、第2引数にはアラインしたいバイト数(12816など)を指定します。

スタック領域にアラインされたメモリ領域を確保する

  • スタック領域(関数内で宣言する自動変数などが使う領域)にアラインされたメモリ領域を確保したい場合、次のように記述します:
    void *ptr;
    ctk_alloca_align(ptr, size, align);
  • APIctk_alloca_alignの実体は簡単なマクロです。PPE, SPEどちらのソースコードでも利用可能です。
  • なお、ctk_alloca_alignではマクロ内部でポインタに値を設定するため、C++では型変換エラーが出ます。C++の場合、ctk_alloca_align_typedマクロを使ってポインタの型を指定してください。
    Data *data;
    ctk_alloca_align_typed(data, Data *, sizeof(Data), 128);


メモリ領域を動的に確保する主なAPI
ctk_malloc_align ヒープ領域にアラインされたメモリ領域を動的に確保する
ctk_alloca_align スタック領域にアラインされたメモリ領域を動的に確保する

PPEからDMA転送してデータをSPEに渡す

  • DMA転送を使った別の例として、今度はPPEからDMA転送を行う例を見てみましょう。CTKでは、PPEからのDMA転送でも、SPEからの場合と同じようにctk_dma_putctk_dma_getのようなAPIを使います。なお、どちらから転送しても必ず
    • SPEからメインメモリへの転送PUT (ctk_dma_put)
    • メインメモリからSPEへの転送GET (ctk_dma_get)
のように呼ぶことに注意してください。
PPE プログラムからDMA転送するための主なAPI
ctk_dma_put DMA転送要求をする (SPE からメインメモリ)
ctk_dma_get DMA転送要求をする (メインメモリから SPE)
ctk_dma_put_block DMA転送要求を出し転送完了を待つ (SPE からメインメモリ)
ctk_dma_get_block DMA転送要求を出し転送完了を待つ (メインメモリから SPE)
  • なお、一般的に、PPEからDMA通信を行うよりは、SPEからDMA通信を行う方が、効率の良い高速なプログラムを書くことが出来ます。Cell/B.E.にはPPEは1個しかないのに対しSPEは8個搭載されていますから、DMA転送を含めて様々な仕事を出来るだけSPEに自立的に行わせるようにする方が性能的なメリットがあります。
  • PPEからDMA転送を行う場合、SPEのmain関数まで実際に到達したかどうかを確かめてから転送を行う必要があります。また、SPEが用意しているバッファのアドレスをPPEに通知してあげる必要があります。このために、メールボックス通信と組み合わせてプログラムを書いてみましょう。

PPEソースコード

  • PPEソースコードは例えば次のようになります。
#include <ctk.h>
#include "spe-hello-embed.h"

char buf[128] __attribute__((aligned(128)));
int main() {
    unsigned int ls_buf_addr;
    ctk_spe_thread_t spe;
    sprintf(buf, "[SPE][Data transfered by PPE] Hello, World!\n");
    ctk_spe_thread_create(&spe, &spe_hello, buf, NULL, 0);

    ctk_mbox_read_block(spe, &ls_buf_addr);     /* SPEからアドレスをもらう */
    ctk_dma_get_block(spe, ls_buf_addr, buf, sizeof(buf));
    ctk_mbox_write_block(spe, 0);               /* SPEに転送終了を通知 */

    ctk_spe_thread_wait(spe, NULL);
}

SPEソースコード

  • 上のPPEソースコードに対応するSPEソースコードは次のようになります。
#include <ctk_spu.h>

char buf[128] __attribute__((aligned(128)));
int main(unsigned long long speid, unsigned long long argp) {
    unsigned int dummy;

    /* PPEに転送先アドレスを通知 */
    ctk_mbox_write((unsigned int)buf);

    /* 転送が終わるのを待つ */
    ctk_mbox_read(&dummy);

    printf(buf);
}

同期を伴うプログラムを書く

  • "Hello, World"プログラムの趣旨をむりやり拡張して、今度は複数SPEで同期を行いながら処理するようなプログラムにしてみましょう。
  • 複数のSPEスレッドとPPEスレッドで、共有データを1つずつインクリメント(加算)しながら何回かループするようなプログラムを書いてみます。共有データをインクリメントするときには、排他制御を行うためにMutexロックをかけるCTKのAPIctk_mutex_lockctk_mutex_unlockを呼びます。
  • 加算が終わったら、バリア同期を取って全スレッドが終了するのを待ち、そのあとで全スレッドで共有データの値を表示します。バリア同期にはctk_barrier_waitを使います。

共通ヘッダ

  • まず、PPEコードとSPEコードの両方で使うデータ構造体や変数を定義しているヘッダファイルを用意します。このファイル名はcommon.hとします。
#ifndef _COMMON_H
#define _COMMON_H

struct arg {
    ea_addr_t buf_addr;
    ctk_mutex_ea_t lock;
    ctk_barrier_ea_t barrier;
} __attribute__((aligned(128))) arg;

int buf[32] __attribute__((aligned(128)));

#define NSPE    6
#define NPTHREADS   10
#define NLOOPS  1000

#endif /* _COMMON_H */
  • 構造体argは、SPEスレッドに渡す引数データを表すためのデータ型です。構造体メンバにea_addr_tというデータ型の値を宣言していますが、これはCTKが定義しているEAアドレス(メインメモリ上のアドレス)を表すデータ型であり、実際には単なる64ビットの符号なし整数型(unsigned long long)です。
ea_addr_t EAアドレス (64ビット符号なし整数型)
ls_addr_t LSアドレス (32ビット符号なし整数型)

PPEソースコード

#include <ctk.h>
#include <pthread.h>
#include "spe-hello-embed.h"
#include "common.h"

/* スレッド関数 - 共有データの更新 */
void *func(void *targ) {
    int i, id = (unsigned long)targ;
    for (i = 0; i < NLOOPS; i++) {
        ctk_mutex_lock(arg.lock);
        buf[0]++;
        ctk_mutex_unlock(arg.lock);
    }
    /* バリア同期 */
    ctk_barrier_wait(arg.barrier);
    /* 結果の表示 */
    printf("[PPE %d] Hello World, We got: %d\n", id, buf[0]);
    return NULL;
}

int main() {
    int i;
    pthread_t threads[NPTHREADS];
    ctk_spe_thread_group_t group;

    /* 共有データ, Mutex排他ロック, バリア同期オブジェクトの初期化 */
    buf[0] = 0;
    ctk_mutex_create(&arg.lock);
    ctk_barrier_create(&arg.barrier, NPTHREADS + NSPE);
    arg.buf_addr = (unsigned long long)(unsigned long)buf;

    /* SPEスレッドグループを生成 */
    ctk_spe_thread_group_create_spmd(&group, NSPE, &spe_hello, &arg, NULL, 0);
    /* PPEスレッドを生成 */
    for (i = 0; i < NPTHREADS; i++)
        pthread_create(&threads[i], NULL, func, (void*)(unsigned long)i);

    /* SPEスレッドグループに処理開始をシグナル */
    ctk_signal_group_write_reg1(group, 0);

    /* PPE, SPEスレッドの終了待ち */
    for (i = 0; i < NPTHREADS; i++)
        pthread_join(threads[i], NULL);
    ctk_spe_thread_group_wait(group, NULL);

    ctk_mutex_destroy(arg.lock);
    ctk_barrier_destroy(arg.barrier);
}
  • ここで実行されるSPEプログラムはすぐに動作が終わってしまうため、なるべくすべてのSPEスレッドが同時に処理を開始するようにシグナル通知を使っています。
  • SPEスレッドグループに対してまとめてシグナル通知を行うのに、グループ通知を行うAPI ctk_signal_group_write を使っています。このAPIは、指定されたデータをスレッドグループ中のすべてのスレッドに通知します。
  • 同様に、SPEスレッドグループに対してまとめてメールボックス通信を行うためのAPIとしてctk_mbox_group_readctk_mbox_group_writeというAPIが用意されています。
  • スレッド関数funcが実際に並列(並行)に実行される箇所になります。ここではctk_mutex_lockでロックをかけてから共有データを更新し、またctk_mutex_unlockでロックをはずしています。
  • 最後に、ctk_barrier_waitでSPEスレッドを含む全スレッドが共有データへのアクセス処理を終えるのを待っています。
  • バリア同期が終わったら、最終的な共有データの値とともに"Hello, World"を(ようやく)表示します。

SPEソースコード

#include <ctk_spu.h>
#include "common.h"

int main(unsigned long long speid, unsigned long long argp,
         unsigned long long envp) {
    int i, dummy;
    ctk_env_init(&envp);
    /* 開始の合図を待つ */
    ctk_signal_read_reg1(&dummy);

    /* 共有データの更新 */
    ctk_dma_get_block(&arg, argp, sizeof(arg));
    for (i = 0; i < NLOOPS; i++) {
        ctk_mutex_lock(arg.lock);
        ctk_dma_get_block(buf, arg.buf_addr, 128);
        buf[0]++;
        ctk_dma_put_block(buf, arg.buf_addr, 128);
        ctk_mutex_unlock(arg.lock);
    }
    /* バリア同期 */
    ctk_barrier_wait(arg.barrier);
    ctk_dma_get_block(buf, arg.buf_addr, 128);
    /* 結果の表示 */
    printf("[SPE %d] Hello World, We got: %d\n", ctk_env_get_rank(), buf[0]);
    return 0;
}
  • SPEプログラムでも、PPEプログラムと同様のAPIを呼び出してMutexロックをかけたりバリア同期したりしています。
  • 最終的に、これらのプログラムをビルドして実行すると、PPEでもSPEでも同じ値 ((NPTHREADS + NSPE) * NLOOPS = 16000)が表示されるはずです。
  • なお、同期のためのその他のAPIとして、CTKでは条件待ち変数、キュー, セマフォなどを提供しています。
同期のための主なAPI
排他ロック (Mutex)
ctk_mutex_create Mutexロックの作成と初期化 (PPEのみ)
ctk_mutex_init Mutexロックの初期化
ctk_mutex_lock Mutexロックをかける
ctk_mutex_trylock Mutexロックをかける (誰かが既にロックをかけていたらエラーを返す)
ctk_mutex_unlock Mutexロックを解除する
ctk_mutex_destroy Mutexロックの破棄 (PPEのみ)
セマフォ (Semaphore)
ctk_sem_create セマフォの作成 (PPEのみ)
ctk_sem_init セマフォの初期化
ctk_sem_post セマフォの値を1増やす
ctk_sem_wait セマフォの値を1減らす(値が0の場合ブロックする)
ctk_sem_trywait セマフォの値を1減らす(値が0の場合エラーを返す)
ctk_sem_destroy セマフォの破棄 (PPEのみ)
条件変数 (Conditional Variable)
ctk_cond_create 条件変数の作成 (PPEのみ)
ctk_cond_init 条件変数の初期化
ctk_cond_wait 条件変数の上で待つ
ctk_cond_signal 条件変数で待っているスレッドを1つ起こす
ctk_cond_broadcast 条件変数で待っている全スレッドを起こす
ctk_cond_destroy 条件変数の破棄 (PPEのみ)
バリア同期 (Barrier)
ctk_barrier_create バリアの作成 (PPEのみ)
ctk_barrier_init バリアの初期化
ctk_barrier_wait バリア同期する
ctk_barrier_destroy バリアの破棄 (PPEのみ)

CTKユーザマニュアル」に戻る

表示