CTKによる基本的なCプログラミング

出典: CTK: Cell ToolKit Library

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

  • ここでは、「CTKでHello, World」で説明したもっとも基本的なHello, Worldプログラムをベースにして、CTKの基本的なAPIや開発方法の概要を説明します。
なお、このページでは説明のために「PPEからSPEに依頼して文字列を出力させる」という例を主に使っていますが、文字列の出力はSPEにまったく向いていない仕事の1つです。実際には printf の部分にSPEが得意な演算などが来ると思って読んでください。
  • APIについての詳細は <installdir>/share/doc以下にインストールされるAPIリファレンスを参照してください。

目次

SPEプログラムをCESOF形式にして埋め込む

  • CTKでHello, Worldで示した"Hello, World"アプリでは、SPEプログラムの実行ファイルをPPEプログラム中でオープンしていました。しかし、これではSPE実行ファイルが想定されている場所に置かれていないと実行時エラーになってしまいます。
  • 実は、SPEプログラムは、CESOF (Cell Embedded SPE Object Format)というフォーマットのオブジェクトファイルにすることで、PPEプログラム中にリンクすることができます。
  • SPEプログラムをCESOF形式にしてPPEプログラムに直接リンクするには、例えば次のようにします:
$ <installdir>/bin/ctk-tool cesof-build spe-hello.c
...
Generated: spe-hello-embed.o spe_hello spe-hello-embed.h
Symbol: spe_hello
  • CESOFオブジェクトを使う場合、PPEプログラムでctk_spe_image_openctk_spe_image_closeを呼ぶ必要はなくなります。この場合の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);

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

    ctk_spe_thread_wait(spe, NULL);
    return 0;
}
最初に書いた"Hello, World"プログラムと比較してみましょう。
  • このプログラムをビルドするには、例えば次のようにします。
$ <installdir>/bin/ctk-tool ppu-build --output=ppe-hello \
    ppe-hello.c spe-hello-embed.o
  • 実行方法は、CESOFを使っていてもいなくても同じです。ただし、CESOFを使った場合はSPE実行ファイルの置かれている場所などを気にする必要はありません。
$ ./ppe-hello

複数のSPEスレッドを並列に実行する

  • 複数のSPEを使ったアプリケーションを開発するには、SPEスレッドを複数生成して実行します。例えば、"Hello, World"アプリを複数SPEに拡張するには、PPEソースコードを次のように変更します。
#include <stdio.h>
#include <ctk.h>
#include "spe-hello-embed.h"
#define NSPE    6

int main() {
    int i;
    ctk_spe_thread_t spe[NSPE];
    for (i = 0; i < NSPE; i++) 
        ctk_spe_thread_create(&spe[i], &spe_hello, NULL, NULL, 0);

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

    for (i = 0; i < NSPE; i++) 
        ctk_spe_thread_wait(spe[i], NULL);
    return 0;
}

SPEプログラムに引数を渡す

  • せっかく複数のSPEスレッドを実行するように変更したので、今度はSPEが出力する"Hello, World"メッセージにSPEの番号を含めるようにしてみましょう。
  • SPEプログラムのmain関数は、次のような3つの引数を取ります(これは、CTKの開発環境に限らず共通の仕様です)。
第1引数 unsigned long long speid SPEの論理ID(SPE管理構造体のアドレス値など)
第2引数 unsigned long long argp PPEから渡された引数。CTKではctk_spe_thread_createの第3引数で指定したポインタ値が渡される
第3引数 unsigned long long envp PPEから渡された引数(通常、環境情報などを渡す)。CTKではctk_spe_thread_createの第4引数で指定したポインタ値が渡される
  • 一見、第1引数のSPE IDを使えば良さそうに見えますが、この値は下位ライブラリのSPE管理構造体のアドレス値などになっていることが多いため、0, 1, ...のようなわかりやすい値になっていません。
  • ここでは、第2引数に整数値を渡すことでSPE番号を渡すようにしてみましょう。PPEソースコードでSPEスレッドを生成するところで、例えば次のように記述します:
    for (i = 0; i < NSPE; i++)
        ctk_spe_thread_create(&spe[i], &spe_hello, (void*)(unsigned long)i, 
            NULL, 0);
  • SPEソースコードは次のようにしてみましょう:
#include <stdio.h>
#include <ctk_spu.h>

int main(unsigned long long speid, unsigned long long argp) {
    printf("[SPE %d] Hello, World!\n", (int)argp);
    return 0;
}
  • ビルドし直して実行してみると、次のようになるはずです:
$ <installdir>/bin/ctk-tool cesof-build spe-hello.c
...
Generated: spe-hello-embed.o spe_hello spe-hello-embed.h
Symbol: spe_hello

$ <installdir>/bin/ctk-tool ppu-build --output=ppe-hello \
    ppe-hello.c spe-hello-embed.o
...
Generated: ppe-hello

$ ./ppe-hello
[PPE] Hello, World!
[SPE 0] Hello, World!
[SPE 1] Hello, World!
[SPE 2] Hello, World!
[SPE 3] Hello, World!
[SPE 4] Hello, World!
[SPE 5] Hello, World!
SPEスレッドのための主なAPI
ctk_spe_thread_create SPEスレッドを生成して実行する
ctk_spe_thread_wait SPEスレッドの終了を待ち、SPE資源を開放する

SPEプログラムの返り値を取得する

  • SPEスレッドとして実行されたSPEプログラムのmain関数の返り値を調べるには、ctk_spe_thread_wait関数の第2引数の返り値とCTK_SPE_EXIT_STATUSマクロを使います。
  • PPE側でSPEスレッドの返り値をチェックするコード断片は次のようになります:
    int status;
    ctk_spe_thread_wait(spe, &status);
    printf("SPE's exit code is: %d\n", CTK_SPE_EXIT_STATUS(status));
SPEプログラムのmain関数の返り値を調べるためのマクロ
CTK_SPE_EXIT_STATUS ctk_spe_thread_wait関数の第2引数の返り値からSPEプログラムのmain関数の返り値を取得する
CTK_SPE_EXIT_SIGNAL ctk_spe_thread_wait関数の第2引数の返り値からSPEプログラムの終了状態を返す (0なら正常終了)
CTK_SPE_IF_EXITED ctk_spe_thread_wait関数の第2引数の返り値からSPEプログラムが正常に終了したかどうかを返す

CTK APIの返り値をチェックする

  • これまでのプログラムでは、すべてのAPIについて返り値やエラーをまったくチェックしていませんでした。しかし、実際のプログラムでは、失敗する可能性のあるAPI呼び出しはエラーチェックをする必要があります。
  • CTKでは、ほぼすべての(PPE側の)APIはint型の返り値を返します。この返り値は、成功の場合はCTK_SUCCESS (=0)で、それ以外の場合はCTKの定義するエラーコードを返します。
  • CTKのエラーコードは、ctk_perrorctk_strerror関数を使うことでエラーメッセージに変換することができます。
  • エラー処理を含めたPPEプログラムのソースコードは例えば次のようになります:
#include <stdio.h>
#include <ctk.h>
#include "spe-hello-embed.h"

int main() {
    int ret, status;
    ctk_spe_thread_t spe;
    ret = ctk_spe_thread_create(&spe, &spe_hello, NULL, NULL, 0);
    if (ret != CTK_SUCCESS) {
        ctk_perror(ret, "ctk_spe_thread_create");    
        return -1;
    }

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

    ret = ctk_spe_thread_wait(spe, &status);
    if (ret != CTK_SUCCESS) {
        ctk_perror(ret, "ctk_spe_thread_wait");    
        return -1;
    }
    printf("SPE returned %d\n", CTK_SPE_EXIT_STATUS(status));
    return 0;
}
CTK APIの返り値をチェックするための主なAPI
ctk_perror エラーメッセージを出力する
ctk_strerror エラーコードをエラーメッセージに変換する

SPEコンテキストによるSPEプログラムの実行

  • CTKでは、SPEスレッドよりさらに低レベルな抽象化であるSPEコンテキストを使ったプログラミングも可能です。SPEコンテキストを使うと、SPEプログラムの実行停止状態の取得や再開など、より細かいSPEの実行制御が可能になります。
CTKにおけるSPEスレッドがLibspe1のSPEスレッドに概ね相当するのに対し、SPEコンテキストはLibspe2のSPEコンテキストにほぼ相当します。
Libspe1しかインストールされていない環境では、SPEコンテキストの一部の機能は利用できません。
  • SPEコンテキストを使ってSPEプログラムを実行するPPEプログラムのソースコードは次のようになります:
#include <stdio.h>
#include <ctk.h>
#include "spe-hello-embed.h"

int main() {
    int ret, status, stop;
    unsigned int entry = CTK_SPE_DEFAULT_ENTRY;
    ctk_spe_context_t ctx;

    ctk_spe_context_create(&ctx, 0, NULL);
    ctk_spe_context_load(ctx, &spe_hello);

    /* SPEの実行 */
    ctk_spe_context_run(ctx, &entry, 0, NULL, NULL, &status, &stop);

    ctk_spe_context_destroy(ctx);
    printf("SPE returned %d\n", CTK_SPE_EXIT_STATUS(status));
    return 0;
}
  • SPEコンテキストによるSPEの実行の流れは次のようになります:
    • ctk_spe_context_createでSPEコンテキストを生成する (SPE資源を確保)
    • ctk_spe_context_loadでSPEコンテキストにSPEプログラムイメージをロードする
    • ctk_spe_context_runでSPEコンテキストを実行する
    • ctk_spe_context_destroyで実行し終えたコンテキストを解放する
  • なお、この例ではほとんどCTK APIの返り値をチェックしていませんが、実際には「CTK APIの返り値をチェックする」であげたような方法でエラーチェックをしてください。
  • SPEコンテキストを実行するctk_spe_context_runは、SPEコンテキストにロードされたプログラムを実行し、そのSPEプログラムの実行が終了するかstop命令などで停止するまでブロックします。つまり、ctk_spe_context_runを一度呼び出すと、SPEプログラムの実行が終わるまで呼び出し元に制御が返りません。
  • SPEコンテキストを使ってSPEプログラムを複数並行して実行するには、ctk_spe_context_runを呼び出すPPEプログラムの部分をpthreadなどでマルチスレッド化する必要があります。

stop命令によるSPEプログラムの停止と再開

  • ctk_spe_context_runによって実行されているSPEプログラムがstop命令などによって実行を停止すると、ctk_spe_context_runの実行が終了して処理がPPEプログラム側に返ってきます。この場合、ctk_spe_context_runの第6引数で返されるstatusに対してCTK_SPE_EXIT_SIGNALマクロを呼び出すとSIGSTOPがセットされています。
  • stop命令によって停止したSPEプログラムは、再びctk_spe_context_runを呼び出すことでその続きから実行を再開することができます。
  • SPEプログラム中でstop命令を発行するには、spu_stop組み込み関数を使います。stop命令を発行すると、SPEの実行が停止されてPPEに処理が戻ります。

stop命令を実行するSPEソースコード

    ...
    /* stopタイプ"code"で停止する */
    spu_stop(code);
    ...
  • stop命令を発行するspu_stop組み込み関数は、stopタイプとして14bitの符号なし整数(unsigned int)の値を引数に取ります。このstopタイプはSPEの特別なレジスタに書かれ、ctk_spe_context_runの第7引数に渡されます。(この機能は、Libspe1しかインストールされていない環境では使うことができません)
  • PPEプログラム側でstopタイプを適切に処理するようなコードを書くには、例えば次のようにします:

stop命令を解釈して処理するPPEソースコード

    while (1) {
        /* SPEプログラムの実行開始あるいは再開 */
        ctk_spe_context_run(ctx, &entry, 0, NULL, NULL, &status, &stop);

        /* 終了した場合はループを抜ける */
        if (CTK_SPE_IF_EXITED)
            break;

        /* stopタイプに応じて適切なPPE側の処理をする */
        switch (stop) {
        case ...:
        }
    }
  • LibspeやCTKの環境ではいくつかのstopタイプは標準入出力関数などのために予約されています。ユーザ側で任意のstopタイプを定義して使う場合、0x2000より小さい値か0x2200より大きい値を使うようにしてください。


SPEコンテキストに関する主なAPI
ctk_spe_context_create SPEコンテキストを生成する
ctk_spe_context_load SPEコンテキストにSPEプログラムイメージをロードする
ctk_spe_context_run SPEコンテキストを実行する
ctk_spe_context_destroy SPEコンテキストを破棄する
  • CTKでは、SPEに対する操作を行うPPE側のAPIとして、引数にSPEスレッドとSPEコンテキストのどちらでも指定できるAPIが数多く用意されています。例えば、後述するメールボックス通信のための ctk_mbox_readctk_mbox_write、シグナル通知のための ctk_signal_writeなどのAPIは、その第一引数としてSPEスレッドあるいはSPEコンテキストのどちらを指定しても動作します。
  • 以下に、メールボックスやシグナル通知以外でSPEスレッドとSPEコンテキスト共通で呼び出すことのできる主なAPIの一覧を挙げます。
SPEスレッドとコンテキストに共通するAPIの例
ctk_spe_map_ls LSのマップされたEAアドレスを返す
ctk_spe_map_signal1 シグナル通知レジスタ1のマップされたEAアドレスを返す
ctk_spe_map_signal2 シグナル通知レジスタ2のマップされたEAアドレスを返す

CTK環境の初期化とログメッセージの出力

  • CTKのAPIは、ロギングレベルを変更することである程度冗長な(verbose)なデバッグ情報やトレース情報を出力することができます。
  • ロギングレベルなどのCTK環境の情報を初期化するには、アプリケーションの先頭でctk_initというAPIを呼び出します。ctk_init関数にmain関数の引数を渡すことで、コマンドラインから与えられたオプションをもとに環境を初期化することができます。
  • ctk_initで初期化したCTK環境は、ctk_destroyというAPIを呼ぶことで解放できます。
#include <ctk.h>

int main(int argc, char **argv)
{
   // CTK環境の初期化
   ctk_init(&argc, &argv);

   ...


   // CTK環境の解放
   ctk_destroy();
}
ロギングレベル調整のためのコマンドラインオプション
-s 致命的なエラー時以外はログ出力をしない (silent)
-v ロギングレベルを冗長(verbose)レベルにする
-d ロギングレベルをデバッグ(debug, より冗長)レベルにする
-ln ロギングレベルをnに設定する (1-6, 6がもっとも冗長)


CTK環境初期化と解放のためのAPI
ctk_init CTK環境の初期化 (ロギングレベルの設定, プロセッサ情報の初期化など)
ctk_destroy CTK環境の解放

SPEからDMA転送してデータを取得する

  • 今まではSPEプログラムに "Hello, World" のような決まった文字列を出力させていましたが、今度はPPEプログラムで用意した文字列をSPEプログラムに渡して表示させるようにしてみます。
  • CTKでは、DMA転送のために ctk_dma_put あるいは ctk_dma_get というラッパAPIを用意しています。ブロッキング転送を行う(転送要求を出したあとさらに転送完了を待つ)には、ctk_dma_put_blockあるいはctk_dma_get_blockのようなAPIを使います。

PPEソースコード

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

char buf[128] __attribute__((aligned(128)));
int main() {
    ctk_spe_thread_t spe;
    sprintf(buf, "[SPE][Data transfered from EA] Hello, World!\n");
    ctk_spe_thread_create(&spe, &spe_hello, buf, NULL, 0);
    ctk_spe_thread_wait(spe, NULL);
}

SPEソースコード

#include <ctk_spu.h>

char buf[128] __attribute__((aligned(128)));
int main(unsigned long long speid, unsigned long long argp) {
    ctk_dma_get_block(buf, argp, sizeof(buf));
    printf(buf);
}
  • なお、CTKがSPEからのDMA転送のために提供しているAPI ctk_dma_getctk_dma_putは、基本的にはSPEプログラムにおいて標準で使うことの出来るマクロ mfc_getやmfc_put と大きな差異はありません。ただし、標準のマクロでは16Kより大きな転送は分割して呼び出す必要がありますが、CTKのAPIではこれらのサイズに対する制限は隠蔽されます。また、CTKのAPIでは16バイトでアラインされていないサイズやアドレスのDMA転送もサポートしています。
  • 明らかに16Kより小さいサイズの領域を転送する場合、API内部の条件分岐のコストを減らすためにctk_dma_get_smallctk_dma_put_smallなどのAPIを使うことができます。これらのAPIはサイズのチェックを行わず、単に1個のDMA転送要求命令を発行します。
SPE プログラムからDMA転送するための主なAPI
ctk_dma_put DMA転送要求をする (SPE からメインメモリ)
ctk_dma_get DMA転送要求をする (メインメモリから SPE)
ctk_dma_put_small 16KB以下の領域についてDMA転送要求をする (SPE からメインメモリ)
ctk_dma_get_small 16KB以下の領域についてDMA転送要求をする (メインメモリから SPE)
ctk_dma_put_block DMA転送要求を出し転送完了を待つ (SPE からメインメモリ)
ctk_dma_get_block DMA転送要求を出し転送完了を待つ (メインメモリから SPE)
ctk_dma_put_small_block 16KB以下の領域についてDMA転送要求を出し転送完了を待つ (SPE からメインメモリ)
ctk_dma_get_small_block 16KB以下の領域についてDMA転送要求を出し転送完了を待つ (メインメモリから SPE)

アクセスヒントを使ってページングコストを減らす

  • SPEからDMA転送を行っているときに、DMA転送先のアドレスがページアウトされている領域にあると、DMA転送の裏でPPEのページフォルト処理が走ることになります。この処理はかなり重い処理であるため、DMA転送するメインメモリ側のアドレスは極力ページアウトされないようにしないと処理速度が極端に落ちることがあり得ます。
  • 東芝SDK環境でCTKを使っている場合、「アクセスヒント」と呼ばれる機構を使うことで特定のメモリ領域がページアウトされるのを防ぐことができます。アクセスヒントのAPIはCTKが使えるどの環境でも呼び出すことが可能ですが、APIの名前どおりの効力を持つのは東芝環境だけです。
  • アクセスヒントはPPE側のプログラムで(SPEがDMA転送を行うよりも前に)あらかじめ設定しておきます。アクセスヒントを設定するAPIは ctk_spe_add_access_hint です。このAPIはSPE(スレッドあるいはコンテキスト)とアクセスヒントを設定したいメモリ領域の開始アドレス・サイズを引数に取ります。

PPEソースコード

  • アクセスヒントを使うPPEコードの例を以下に示します。
#include <stdio.h>
#include <ctk.h>
#include "spe-hello-embed.h"

char buf[128] __attribute__((aligned(128)));
int main() {
    ctk_spe_thread_t spe;
    sprintf(buf, "[SPE][Data transfered from EA] Hello, World!\n");

    /* SPEスレッドを生成する (まだ開始しない) */
    ctk_spe_thread_create(&spe, &spe_hello, buf, NULL, 
        CTK_SPE_DONT_START);

    /* SPEからDMA転送される領域にアクセスヒントを設定し、
       ページアウトされるのを避ける */
    ctk_spe_add_access_hint(spe, buf, sizeof(buf));

    /* SPEスレッドの開始 */
    ctk_spe_thread_start(spe);

    ctk_spe_thread_wait(spe, NULL);
    return 0;
}
  • ctk_spe_add_access_hintは引数にSPE(スレッドあるいはコンテキスト)を取るため、この例ではまずSPEスレッドを開始せずに生成し、アクセスヒントを設定してからスレッドを開始しています。
  • SPEスレッドを生成するときに同時にスレッドを開始しないようにするには、ctk_spe_thread_createを呼び出すときに第5引数にフラグ CTK_SPE_DONT_START を指定します。そのあとスレッドの実行を実際に開始するには、ctk_spe_thread_start APIを使ってください。
  • アクセスヒントは、ヒントを設定したSPEスレッドやコンテキストを破棄すると自動的に破棄されます。実行中のSPEから明示的にアクセスヒントを破棄したい場合、ctk_spe_remove_access_hint というAPIを使います。


処理にかかる時間を計る

  • Cell/B.E.プログラミングでは、高速化や最適化に主眼が置かれることが多いため、細かい粒度で処理時間を計測したいというケースが良くあります。
int func() {
    ...
    /* (A) ここから */

    ...
    /* 重い計算X */
    ...
 
    /* (B) ここまでの時間をはかりたい */
   
  • CTKでは、ctk_profile_xxxというAPI群を使うことで、PPEおよびSPEプログラム中のある特定の範囲の処理時間をCell/B.E.内部のカウンタレジスタを使って精細に測ることができます。
int func() {
    ...
    ctk_profile_t prof;
    ctk_profile_start(&prof);
   
    ...
    /* 重い計算X */
    ...

    printf("計算Xにかかった時間は %d マイクロ秒です\n", 
        ctk_profile_read_in_usec(&prof));
    ...
}
  • 計測の開始はctk_profile_startというAPIで、開始からの経過時間はctk_profile_read_in_xxxというAPIで取得できます。
プロファイリングのための主なAPI
ctk_profile_start 計時の開始
ctk_profile_read_in_msec 計時開始から現在までの経過時間 (ミリ秒)
ctk_profile_read_in_usec 計時開始から現在までの経過時間 (マイクロ秒)
ctk_profile_read_in_clock 計時開始から現在までの経過時間 (内部カウンタレジスタのクロック数)
  • なお、SPEプログラムの計時では、コンパイル時のターゲットCell/B.E.マシンと実行するCell/B.E.マシンが違う可能性がある場合、main関数の先頭で以下のように環境情報を初期化するコードを入れてください。ctk_profile_xxx関数は、Cell/B.E.のカウンタ周期から実時間に変換するために環境情報を使います。
int main(unsigned long long speid,
         unsigned long long argp,
         unsigned long long envp) {

    ctk_env_init(&envp);        /* 環境を初期化 */
    ...
    ctk_profile_t prof;
    ctk_profile_start(&prof);
   
    ...
    /* 重い計算X */
    ...

    printf("計算Xにかかった時間は %d マイクロ秒です\n", 
        ctk_profile_read_in_usec(&prof));
    ...
}

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

表示