l'essentiel est invisible pour les yeux

Showing posts with label Cell. Show all posts
Showing posts with label Cell. Show all posts

Saturday, January 13, 2007

[Cell] SPUプログラムをスタンドアロンで実行する。(spulets)

SPUプログラムもスタンドアロンで実行可能にするには、crt(C RunTime Object)を合わせてリンクする必要がある。GCCでは-mstdmainオプションを付加することで、スタンドアロンで実行可能としてコンパイルできる。SPUプログラムのHello, Wolrdを単独で実行可能としてコンパイルする。

PPUから起動される場合と異なり、main()の引数がint argc, char **argvとなることに注意する。引数のアドレスはローカルストア上のアドレスを参照している。
hello.c


#include <stdio.h>

int main(int argc __attribute__((unused)), char **argv __attribute__((unused))) {
int i;

printf("Hello, world from SPE.\n");
for(i=0;i<argc;i++) {
printf("argv[%d]=%p\n", i, argv[i]);
}
return 0;
}


Makefileを作成する。Cell SDK 2.0に付属しているmakeファイルを使用する。LDFLAGS_gcc定数に-mstdmainを定義する。

PROGRAM_spu = hello
LDFLAGS_gcc = -mstdmain
include /opt/ibm/cell-sdk/prototype/make.footer


実行するとargv[1]の引数の値が0x3fff0に配置されていることが分かる。

# ./hello test
Hello, world from SPE.
argv[0]=0x3ffe8
argv[1]=0x3fff0
#


ローカル・ストレージは0x3fffd0を開始位置としてスタックポインタを初期化する。プログラムに渡されるコマンドライン引数は、0x3fff0のアドレスからスタックに詰まれる。メモリスタックの図をSPU Application Binary Interface仕様書より拝借した。



参考
SPU Application Binary Interface仕様書 (PDF)

Sunday, January 07, 2007

[Cell] libsp2 APIを使用してMailboxを介した通信

3週間ほどCellから離れている間に、CBE SDK2.0やlibspe2がリリースされました。
libspe2はlibspe1と大きくspufs抽象化の仕組みが異なっているので、libspe2 APIの基本的な使い方を見ていきます。

SPEはプログラム中から直接扱えないため、ソフトウェアレベルで抽象化されて扱うための実装としてコンテキストが導入されており、コンテキストは"論理的なSPE"についての情報を保持します。プログラマはSPEコンテキストへのポインタを扱うことでSPEを操作できます。

SPEコンテキストを1つだけ扱う場合は次の順序で実行する。

  1. SPEコンテキストの作成
  2. SPEのローカルストアにSPEに実行させるプログラムをロードする
  3. SPEコンテキストを実行する
  4. SPEコンテキストを廃棄する

SPEコンテキストの作成はspe_context_create関数を使用します。
第一引数にコンテキストに適用するフラグを指定します。
第二引数はギャングコンテキスト(複数のコンテキストを1つに関連付け)を使用する際に使用します。

PPE側のプログラム


spe_context_ptr_t ctx;

// Create SPE context
if((ctx = spe_context_create(SPE_EVENTS_ENABLE, NULL)) == NULL) {
perror("spe_context_create");
return 1;
}


SPEのローカルストアにプログラムをロードするには、spe_program_load()関数を使用する。
ここではCESOFを使用しているため、ELFイメージをロードする必要はありませんが、ELFイメージを手動でロードする場合は、spe_image_open()関数を使用します。


extern spe_program_handle_t spe_mbox;

// Programe load to Local Store
if((rc = spe_program_load(ctx, &spe_mbox)) < 0) {
perror("spe_program_load");
return 1;
}


SPUと他のデバイス間(他のSPUやPPU)の通信には、MMIO・DMAといった手段のほかに、Mailboxという32bitメッセージをやり取りする仕組みが用意されています。今回はMailboxを利用してデータを送信します。メールボックスの詳細は、Cell Broadband Engine Programming Handbook 19.6 Mailboxesを参照してください。

SPUのMailboxへデータを書き込むには、spe_in_mbox_write()関数を使用します。
送信先のコンテキスト, 書き込むデータへのポインタ, 書き込むデータ数, ブロッキングに関する振る舞いを引数に指定します。

unsigned int mbox_data = 123;

// Write to Mailbox
if((rc = spe_in_mbox_write(ctx, &mbox_data, 1, SPE_MBOX_ALL_BLOCKING)) < 0) {
perror("spe_in_mbox_write");
return 1;
}


SPEコンテキストを実行します。
引数に与えたspe_stop_info_t型の変数であるstop_infoへのポインタを渡すことでSPEの終了状態に関する情報が取得できる。spe_stop_info_t構造体についての詳細は、libspe-v2.0.pdfを参照。


// Print exit status code
switch(stop_info.stop_reason) {
case SPE_EXIT:
printf("SPE_EXIT stop_info.result.stop_exit_code=0x%x\n", stop_info.result.spe_exit_code);
break;
case SPE_STOP_AND_SIGNAL:
printf("SPE_STOP_AND_SIGNAL stop_info.result.stop_signal_code=%d\n", stop_info.result.spe_signal_code);
break;
case SPE_RUNTIME_ERROR:
printf("SPE_RUNTIME_ERROR stop_info.result.spe_runtime_error=%d\n", stop_info.result.spe_runtime_error);
break;
case SPE_RUNTIME_EXCEPTION:
printf("SPE_RUNTIME_EXCEPTION stop_info.result.spe_runtime_exception=%d\n", stop_info.result.spe_runtime_exception);
break;
}



最後にSPEコンテキストを破棄する。

// Destroy SPE Context
if((rc = spe_context_destroy(ctx)) < 0) {
perror("spe_context_destroy");
return 1;
}


SPE側のプログラム

#include <stdio.h>
#include <spu_mfcio.h>

typedef union {
unsigned long long ull;
unsigned int ul[2];
} addr64;

int main(unsigned long long speid __attribute__((unused)),
addr64 argp __attribute__((unused))) {

unsigned int mbox_data;

printf("Hello, world from SPE.\n");

// Read data from mailbox
mbox_data = spu_read_in_mbox(); // spu_readch(SPU_RdInMbox)
printf("SPE mbox_data: %d\n", mbox_data);

return 26;
}



spu_read_in_mbox()関数でinbound mailboxにあるデータを読み込む。

ソースコード一式
ディレクトリ構造は次の通り。

% ls -FR
.:
Makefile ppu/ spu/

./ppu:
Makefile mbox_test.c

./spu:
Makefile spe_mbox.c
%


ppu/mbox_test.c


#include <stdio.h>
#include <libspe2.h>

extern spe_program_handle_t spe_mbox;

int main() {
int rc;
int flags;
unsigned int entry = SPE_DEFAULT_ENTRY;
unsigned int mbox_data = 123;
spe_context_ptr_t ctx;
spe_stop_info_t stop_info;

// Create SPE context
if((ctx = spe_context_create(SPE_EVENTS_ENABLE, NULL)) == NULL) {
perror("spe_context_create");
return 1;
}

// Programe load to Local Store
if((rc = spe_program_load(ctx, &spe_mbox)) < 0) {
perror("spe_program_load");
return 1;
}

// Write to Mailbox
if((rc = spe_in_mbox_write(ctx, &mbox_data, 1, SPE_MBOX_ALL_BLOCKING)) < 0) {
perror("spe_in_mbox_write");
return 1;
}

// SPE Context running
if((rc = spe_context_run(ctx, &entry, 0, NULL, NULL, &stop_info)) < 0) {
perror("spe_context_run");
return 1;
}

// Print exit status code
switch(stop_info.stop_reason) {
case SPE_EXIT:
printf("SPE_EXIT stop_info.result.stop_exit_code=0x%x\n", stop_info.result.spe_exit_code);
break;
case SPE_STOP_AND_SIGNAL:
printf("SPE_STOP_AND_SIGNAL stop_info.result.stop_signal_code=%d\n", stop_info.result.spe_signal_code);
break;
case SPE_RUNTIME_ERROR:
printf("SPE_RUNTIME_ERROR stop_info.result.spe_runtime_error=%d\n", stop_info.result.spe_runtime_error);
break;
case SPE_RUNTIME_EXCEPTION:
printf("SPE_RUNTIME_EXCEPTION stop_info.result.spe_runtime_exception=%d\n", stop_info.result.spe_runtime_exception);
break;
}

// Destroy SPE Context
if((rc = spe_context_destroy(ctx)) < 0) {
perror("spe_context_destroy");
return 1;
}

return 0;
}


実行結果
Mailboxを介した通信とSPEプログラムの返り値26(=0x1a)が正常に取得できています。


# ./mbox_test
Hello, world from SPE.
SPE mbox_data: 123
SPE_EXIT stop_info.result.stop_exit_code=0x1a
#


参考
SPE Runtime Management Library (PDF)
Cell Broadband Engine Programming Handbook Version 1.0 (PDF)

Wednesday, December 06, 2006

[Cell] オーバーレイで使用するリンカスクリプトを詳しく

[Cell] SPE上でオーバーレイプログラムを書くでオーバーレイを利用したプログラムを書いた。オーバーレイではリンカスクリプトによる処理が肝となるので、もう少し詳しくみていく。

リンカスクリプトは、-c commandfile(または、-T commandfile)で指定する。これはldのデフォルトコマンドに追加されるわけではなく全て上書きすることになるので、オーバーレイのセクション以外にもターゲットフォーマットに関して必要な全ての記述がされなければならない。

以前使ったld.script中の出力ファイルに関する指定。


OUTPUT_FORMAT("elf32-spu", "elf32-spu", "elf32-spu")
OUTPUT_ARCH(spu)
NOCROSSREFS(.olay1 .olay2)
ENTRY(_start)

SPUプログラムのBFDフォーマットにはelf32-spuを指定する。
spu-objdump -fを使用して確認することが出来る。

% spu-objdump -f spu/spu_main
spu/spu_main: file format elf32-spu
architecture: spu:256K, flags 0x00000012:
EXEC_P, HAS_SYMS
start address 0x000000c0
%

NOCROSSREFSで指定したセクション間で参照があった際に、エラーが発生する。オーバーレイのテクニックを使用する際は、二つのコードが同時にメモリ上にロードされているかどうかはわからないのでこのオプションが有効。

オーバーレイに関するセクションを抜き出したもの。

PROVIDE(__executable_start = 0x0000080);

/**** overlay sections ****/
/* overlay load address */
__overlay_region = 0x000080;
__load_start_overlay1 = 0x4000000;
.overlay1 __overlay_region(NOLOAD) : AT (__load_start_overlay1)
{ mod1/*.o(.text) }
__overlay1_size = SIZEOF (.overlay1);
__load_stop_overlay1 = __load_start_overlay1 + __overlay1_size;

__load_start_overlay2 = (__load_stop_overlay1 + 0x80 - 1) & ~(0x7F); /* alinged by 2**7 */
.overlay2 __overlay_region(NOLOAD) : AT (__load_start_overlay2)
{ mod2/*.o(.text) }
__overlay2_size = SIZEOF(.overlay2);
__load_stop_overlay2 = __load_start_overlay2 + __overlay2_size;
__overlay_region_size = MAX(SIZEOF(.overlay1), SIZEOF(.overlay2));
/**** overlay sections ****/


__overlay_regionはオーバレイモジュールを読み込む領域で0x80に指定する。オーバレイモジュールを読み込む領域を二つ指定する事もできる。

__overlay_region1 = 0x000080;
__overlay_region2 = 0x010080;


オーバレイセクションの開始アドレスと終端アドレスはそれぞれ、__load_start_secname, __load_stop_secnameというシンボルで定義される。

オーバーレイのポイントは、出力セクション型にNOLOADを指定することでメモリ上にロードしないことである。次の設定は、olay1/*.oにマッチする全てのオブジェクトの.textセクションをLMA(ロードアドレス)とVMA(仮想アドレス)を等しく設定するATキーワードを付けて__load_start_overlay1をセクションのロードアドレスに指定する。__overaly_region(NODATA)で定義済みメモリ領域にセクションを割り当てるが、メモリ上にロードしない。

.olay1 __overlay_region (NOLOAD) : AT (__load_start_overlay1)
{ olay1/*.o(.text) }


SIZEOFでセクションのサイズを取得できる。セクション1の開始アドレスとサイズから終端ロードアドレスを算出し、その次の128byte境界のアドレスをセクション.overlay2の開始アドレスとする。

__overlay1_size = SIZEOF (.overlay1);
__load_stop_overlay1 = __load_start_overlay1 + __overlay1_size;
__load_start_overlay2 = (__load_stop_overlay1 + 0x80 - 1) & ~(0x7F); /* alinged by 2**7 */


位置カウンタ(. = で指定される)をオーバーレイをロードする領域のアドレスにセクションの大きい方のサイズを加算して設定する。

__overlay_region_size = MAX(SIZEOF(.overlay1), SIZEOF(.overlay2));
/**** overlay sections ****/
. = __overlay_region + __overlay_region_size;


Cell上でのリンカスクリプトはIBMが提供するCell SDKサンプルから利用することが出来る。

参考
3. リンカスクリプト

Tuesday, December 05, 2006

[Cell] SPE上でオーバーレイプログラムを書く

IT用語辞典バイナリより

オーバーレイとは、主にプログラムが大きすぎてメインメモリに格納できないような場合に、プログラムを機能ごとに分割し、それぞれのセグメントをプログラム自身が管理、制御することである。他にも、層を重ね合わせるといった意味で様々な場面に用いられる表現である。

メモリ容量の小さい昔は良く使われていた"らしい"テクニックだが、これまで低レイヤプログラミングの機会が無かったので使ったことのないテクニックだった。(組み込みぐらいでしか使わない?) SPU上では、LS(ローカルストレージ)内のプログラムを実行することが可能だが、LSには256KBという厳しい制限があるためこれより大きいサイズのプログラムを動かすためにオーバーレイというテクニックが有効となる。

ということでSPE上でオーバーレイを実現するプログラムを書いてみた。「どういう仕組みでオーバーレイが実現されるのか?」なんて全く知らなかったので仕組みを調べながら書いてみた。

ディレクトリ/ファイル構成は次の通り。

% ls -R
.:
Makefile ppu/ spu/

./ppu:
Makefile ppu_overlay.c

./spu:
Makefile ld.script mod1/ mod2/ offset.map spu_main.c toe_section.s

./spu/mod1:
Makefile func.c

./spu/mod2:
Makefile func.c
%

オーバレイを実現するには、オーバレイモジュール(プログラムの断片)とオーバーレイモジュールのロードを管理するプログラムが必要となる。上記のファイル構成との対応付けは次の通り。
  • モジュールのロードを管理するプログラム : spu/spu_main.c
  • オーバーレイモジュール : mod1/func.c, mod2/func.c
オーバーレイモジュールの作成
加算と乗算を行う関数を実装する。mod1/func.cにはint型の加算/乗算を行う関数、mod2/func.cにはfloat型の加算/乗算を行う関数を実装する。

spu/mod1/func.c

#include <stdio.h>

int mod1_add(int a, int b) {
int ret = a + b; // value may be overflowed
return ret;
}

int mod1_mul(int a, int b) {
int ret = a * b; // value may be overflowed
return ret;
}

spu/mod2/func.c
#include <stdio.h>

float mod2_fadd(float a, float b) {
float ret = a + b; // values maybe overflowed
return ret;
}

float mod2_fmul(float a, float b) {
float ret = a * b; // values maybe overflowed
return ret;
}
ここは特に説明なし。

spu/spu_main.c
次に、モジュールのロードを管理するプログラム(spu/spu_main.c)を実装する。ソースコード全体は次の通り。

#include <stdio.h>
#include <stdlib.h>
#include <libmisc.h> /* copy_to_ls and copy_from_ls */
#include <spu_mfcio.h>
#include <string.h>

/* value resolved by CESOF in the system memory */
extern unsigned long long _EAR_spu_main;
extern unsigned long long _EAR__overlay1_offset;
extern unsigned long long _EAR__overlay2_offset;

/* overlay symbols from linking process */
extern char __overlay_region;
extern char __overlay1_size;
extern char __overlay2_size;

/* these function is overlayed with overlay modules */
extern int mod1_add(int, int);
extern int mod1_mul(int, int);
extern float mod2_fadd(float, float);
extern float mod2_fmul(float, float);

/* SPE programe handle */
struct spe_program_handle {
unsigned int handle_size;
void *elf_image;
void *toe_shadow;
} __attribute__((aligned(128)));

int main(unsigned long long spuid __attribute__((unused)),
unsigned long long argp __attribute__((unused))) {

unsigned long long overlay1_ldaddr, overlay2_ldaddr;
struct spe_program_handle handle;

/* Get the spu_main image location and caluclate overlay modules address */
copy_to_ls((uint32_t)(&handle), _EAR_spu_main, sizeof(handle));
overlay1_ldaddr = (uint32_t) handle.elf_image + _EAR__overlay1_offset;
overlay2_ldaddr = (uint32_t) handle.elf_image + _EAR__overlay2_offset;

printf("__overlay_region: %p\n", &__overlay_region);
printf("__overlay1_size: %d\n", (int)&__overlay1_size);
printf("overlay1_ldaddr: 0x%llX\n", overlay1_ldaddr);

/* load overlay modules from system memory to .overlay section in LS */
copy_to_ls((uint32_t)(&__overlay_region), overlay1_ldaddr, (int)&__overlay1_size);
printf("mod1_add(2,3) result: %d\n", mod1_add(2, 3));
printf("mod1_mul(2,3) result: %d\n", mod1_mul(2, 3));

copy_to_ls((uint32_t)(&__overlay_region), overlay2_ldaddr, (int)&__overlay2_size);
printf("mod2_fadd(2.2,3.3) result: %.3f\n", mod2_fadd(2.2, 3.3));
printf("mod2_fmul(2.2,3.3) result: %.3f\n", mod2_fmul(2.2, 3.3));

return 0;
}
プログラムの解説。
次の変数はPPUプログラム上で宣言された変数(_EAR_を取り除いた名前の変数)を参照する。CESOF(Cell Embed SPU Object Format)でシンボル名がどのように解決されるかを[Cell] CESOFの仕組みで調べた。

_EAR_spu_mainは、システムメモリマップにマップされた実行可能SPEプログラムのアドレスである。
_EAR__overlay1_offset, _EAR__overlay2_offsetは、mod1/func.c, mod2/func.cは実行可能SPEプログラム中の.overlay1, .overlay2セクションの先頭バイトからのオフセットである。この値は、spu/offset.mapにて提供される。(後述)
/* value resolved by CESOF in the system memory */
extern unsigned long long _EAR_spu_main;
extern unsigned long long _EAR__overlay1_offset;
extern unsigned long long _EAR__overlay2_offset;
_EAR_spu_mainで提供されるアドレスから実行可能SPEイメージを読み込み、.overlay1, .overlay2セクションへのオフセットを加算してそれぞれのオーバーレイモジュールのアドレスを算出する。このアドレスを利用してメインメモリからLSへオーバーレイモジュールをDMA転送を利用して読み込む。


/* Get the spu_main image location and caluclate overlay modules address */
copy_to_ls((uint32_t)(&handle), _EAR_spu_main, sizeof(handle));
overlay1_ldaddr = (uint32_t) handle.elf_image + _EAR__overlay1_offset;
overlay2_ldaddr = (uint32_t) handle.elf_image + _EAR__overlay2_offset;


次の変数へのシンボルは、リンカスクリプト ld.script(後述)を使用してリンク時に解決される。このプログラムでは、オーバーレイモジュールをロードする領域を一つしか用意していないため、異なるオーバーレイモジュールに実装された関数を呼び出すには、ロードしなおす必要がある。

__overlay_regionはオーバーレイモジュールをロードする領域。
__overlay1_sizeは.overlay1セクションのサイズ。
__overlay2_sizeは.overlay2セクションのサイズ。
/* overlay symbols from linking process */
extern char __overlay_region;
extern char __overlay1_size;
extern char __overlay2_size;
オーバレイモジュールを読み込むには、低レベルのDMA転送をラップした便利な関数copy_to_lsを利用する。この関数は、libmisc.hで提供される。一つ目は、オーバーレイモジュールを読み込む領域に.overlay1セクションの値を読み込む。(サイズやオーバーレイを読み込む領域のアドレスはリンカスクリプトによって提供される。後述)
copy_to_ls((uint32_t)(&__overlay_region), overlay1_ldaddr, (int)&__overlay1_size);
copy_to_ls((uint32_t)(&__overlay_region), overlay2_ldaddr, (int)&__overlay2_size);


EAR変数のシンボルを解決するために、TOEセクション中にEARシンボルを定義したアセンブリspu/toe_section.sを書く。(参考: [Cell] CESOFを使用してPPU/SPU間で変数を相互に参照する)

spu/toe_section.s

.section .toe, "a", @progbits
.align 4

.global _EAR_spu_main
_EAR_spu_main:
.octa 0x0

.global _EAR__overlay1_offset
_EAR__overlay1_offset:
.octa 0x0

.global _EAR__overlay2_offset
_EAR__overlay2_offset:
.octa 0x0


リンカスクリプト
リンカスクリプトの雛形はCell SDKのなかでも提供されている。ここで定義する必要があるのは、**overlay sections**の間。上記の例で出てきた、__overlay_region等のシンボルの値はリンカ中で解決される。

オーバレイモジュールを読み込むアドレスは0x80にする必要がある。
.overlay1, .overlay2の出力セクションをNOLOADで指定することによりプログラムの実行時にロードされなくなる。.overlay1, .overlay2セクションのサイズを__overlay1_size, __overlay2_sizeというシンボルで参照できるようにして、SPUプログラム中からEARを利用して参照する。CellではオーバーレイはDMA転送を利用して行われるため、.overlay1, .overlay2ともに128byteアラインする。使用したリンカスクリプトの詳細な解説は別の記事で。

OUTPUT_FORMAT("elf32-spu", "elf32-spu", "elf32-spu")
OUTPUT_ARCH(spu)
NOCROSSREFS(.overlay1, .overlay2)
ENTRY(_start)
SECTIONS
{
PROVIDE(__executable_start = 0x0000080);

/**** overlay sections ****/
/* overlay load address */
__overlay_region = 0x000080;
__load_start_overlay1 = 0x4000000;
.overlay1 __overlay_region(NOLOAD) : AT (__load_start_overlay1)
{ mod1/*.o(.text) }
__overlay1_size = SIZEOF (.overlay1);
__load_stop_overlay1 = __load_start_overlay1 + __overlay1_size;

__load_start_overlay2 = (__load_stop_overlay1 + 0x80 - 1) & ~(0x7F); /* alinged by 2**7 */
.overlay2 __overlay_region(NOLOAD) : AT (__load_start_overlay2)
{ mod2/*.o(.text) }
__overlay2_size = SIZEOF(.overlay2);
__load_stop_overlay2 = __load_start_overlay2 + __overlay2_size;
__overlay_region_size = MAX(SIZEOF(.overlay1), SIZEOF(.overlay2));
/**** overlay sections ****/

. = __overlay_region + __overlay_region_size;
/* Read-only sections, merged into text segment: */
.init :
{
KEEP (*(.init))
} =0
.plt : { *(.plt) }
.text :
{
*(.text .stub .text.*)
} =0
.fini :
{
KEEP (*(.fini))
} =0
PROVIDE (__etext = .);
PROVIDE (_etext = .);
PROVIDE (etext = .);
.rodata : { *(.rodata .rodata.*) }
.rodata1 : { *(.rodata1) }
.eh_frame_hdr : { *(.eh_frame_hdr) }
. = ALIGN(0x80);
PROVIDE (__preinit_array_start = .);
.preinit_array : { *(.preinit_array) }
PROVIDE (__preinit_array_end = .);
PROVIDE (__init_array_start = .);
.init_array : { *(.init_array) }
PROVIDE (__init_array_end = .);
PROVIDE (__fini_array_start = .);
.fini_array : { *(.fini_array) }
PROVIDE (__fini_array_end = .);
.data :
{
*(.data .data.*)
SORT(CONSTRUCTORS)
}
.data1 : { *(.data1) }
.tdata : { *(.tdata .tdata.* ) }
.tbss : { *(.tbss .tbss.*) *(.tcommon) }
.eh_frame : { KEEP (*(.eh_frame)) }
.gcc_except_table : { *(.gcc_except_table) }
.dynamic : { *(.dynamic) }
.ctors :
{
/* gcc uses crtbegin.o to find the start of
the constructors, so we make sure it is
first. Because this is a wildcard, it
doesn't matter if the user does not
actually link against crtbegin.o; the
linker won't look for a file to match a
wildcard. The wildcard also means that it
doesn't matter which directory crtbegin.o
is in. */
KEEP (*crtbegin*.o(.ctors))
/* We don't want to include the .ctor section from
from the crtend.o file until after the sorted ctors.
The .ctor section from the crtend file contains the
end of ctors marker and it must be last */
KEEP (*(EXCLUDE_FILE (*crtend*.o ) .ctors))
KEEP (*(SORT(.ctors.*)))
KEEP (*(.ctors))
}
.dtors :
{
KEEP (*crtbegin*.o(.dtors))
KEEP (*(EXCLUDE_FILE (*crtend*.o ) .dtors))
KEEP (*(SORT(.dtors.*)))
KEEP (*(.dtors))
}
.jcr : { KEEP (*(.jcr)) }
.got : { *(.got.plt) *(.got) }
_edata = .;
PROVIDE (edata = .);
.toe ALIGN(128) : { *(.toe) } = 0
__bss_start = .;
.bss :
{
*(.dynbss)
*(.bss .bss.*)
*(COMMON)
. = ALIGN(16);
}
. = ALIGN(0x80);
/* stick the overlay section here */
_end = .;
PROVIDE (end = .);
PROVIDE (__stack = 0x3fff0);
}
ppu/ppu_overlay.c
次にSPUプログラムを呼び出すPPUプログラムを実装する。
#include <stdio.h>
#include <stdlib.h>
#include <libspe.h>
#include <errno.h>
#include <sched.h>

#define BUF_SIZE 256

extern spe_program_handle_t spu_main;

/* values proviede by offset.map, to be used by spu_main */
extern char _overlay1_offset;
extern char _overlay2_offset;

int main() {
int status;
speid_t spe_id;
spe_gid_t gid;

printf("%s overlay1_offset: %p\n", __FILE__, &_overlay1_offset);

/* Create SPE thread group */
gid = spe_create_group(SCHED_OTHER, 0, 1);
if(gid == 0) {
fprintf(stderr, "Failed spe_create_group(errno=%d)\n", errno);
return 1;
}

/* Create SPE thread */
spe_id = spe_create_thread(gid, &spu_main, NULL, NULL, -1, 0);
if(spe_id == 0) {
fprintf(stderr, "Failed spe_create_thread(errno=%d)\n", errno);
return 1;
}

/* Waiting SPE thread */
spe_wait(spe_id, &status, 0);

return 0;
}


次の変数は、spu/offset.map中で定義され.overlay1/.overlay2セクションのオフセットを持つ。
SPU中で変数名に_EAR_をつけることでこの変数を参照できる。(参考: [Cell] CESOFの仕組み)
/* values proviede by offset.map, to be used by spu_main */
extern char _overlay1_offset;
extern char _overlay2_offset;

spu/offset.map
オフセットの値は、コンパイルしてspu-readelfコマンドで確認してセクションへのオフセットの値を代入する。一度コンパイルしてからオフセットの値を参照し、再コンパイルした。
_overlay1_offset = 0x001850;
_overlay2_offset = 0x001888;
spu-readelfコマンドで.overlay1, .overlay2セクションのオフセットを確認して、offset.mapに代入する。プログラムを変更するとこの値も変わるのでその際は、offset.mapを書き直す。

% spu-readelf -S spu/spu_main | grep .overlay
[ 1] .overlay1 NOBITS 00000080 001850 000038 00 AX 0 0 8
[ 2] .overlay2 NOBITS 00000080 001888 000010 00 AX 0 0 8
%


コンパイルと実行
./Makefile
DIRS = spu ppu
include /opt/IBM/cell-sdk-1.1/make.footer

ppu/Makefile
IMPORTSでspuembededで作成したCESOFのリンク可能SPUを含む静的ライブラリを指定する。
LDFLAGSでリンカに-R ../spu/offset.mapをオプションを渡しシンボル名とアドレスのマップを与える。
PROGRAM_ppu = ppu_overlay
IMPORTS = ../spu/spu_overlay.a -lspe
LDFLAGS = -Wl,-R,../spu/offset.map
include /opt/IBM/cell-sdk-1.1/make.footer

spu/Makefile
LIBRARY_embedを作成する事で、リンク可能なCESOFを作成しspu-arで静的ライブラリを作成する。LDFLAGS-Wl,-T,ld.scriptオプションを指定することでリンカスクリプトとしてld.scriptを使用する。IMPORTでオーバーレイモジュールのfunc.oを組み込むがリンカスクリプト中でNODATAを指定しているためロードされない。


DIRS = mod1 mod2
PROGRAM_spu = spu_main
LIBRARY_embed = spu_overlay.a
IMPORTS = mod1/func.o \
mod2/func.o \
$(SDKLIB_spu)/libc.a \
$(SDKLIB_spu)/libmisc.a
LDFLAGS = -Wl,-T,ld.script
include /opt/IBM/cell-sdk-1.1/make.footer


spu/mod1/Makefile, spu/mod2/Makefile
TARGET_PROFESSOR  = spu
OBJS_ONLY = func.o
include /opt/IBM/cell-sdk-1.1/make.footer
コンパイルしシュミレータに転送し実行する。
# ./ppu_overlay
ppu_overlay.c overlay1_offset: 0x1850
__overlay_region: 0x80
__overlay1_size: 56
overlay1_laddr: 0x1803550
mod1_add(2,3) result: 5
mod2_mul(2,3) result: 6
mod2_fadd(2.2,2.3) result: 5.500
mod2_fmul(2.2,3.3) result: 7.260
#



実行できた。


参考
3. リンカスクリプト
Manpage of ld
[Cell] CESOFの仕組み
[Cell] CESOFを使用してPPU/SPU間で変数を相互に参照する

[Cell] CESOFの仕組み

PPUプログラムからSPUプログラムへのアクセス
PPUプログラムからSPUプログラムにアクセスする方法には、PPUプログラム内で明示的に行う方法と実行時OSによりロードする方法の二つがある。どちらの方法でも初めにSPUプログラムをグローバルな実効アドレスにメモリマップされなければならない。

マップされたアドレスはPPUから利用することが出来る。メモリマップされた実効可能SPUイメージはSPUローダーを通して、SPEのローカルストレージにDMA転送され実行される。そしてSPE実行イメージはPPU中のシンボルを通して参照可能になる。


SPUプログラムからPPUシンボルへのアクセス
SPEプログラムでは、共用されるシンボル領域を持たないため、SPUプログラム中からPPU中のシンボルを参照することは出来ない。このためSPUではPPUからEA(Effective Address)を受け取りDMAオペレーションを利用してデータを転送する。しかし、この操作は実行時オーバヘッドをうみやすく、エラーが混在しやすい。

そこでCESOF中では、EA(実効アドレス)を扱うEAR(Effective Address Reference)と呼ばれる新しいタイプの64bit変数(unsigned long long)を使用してPPU中のシンボルを参照可能にする。このシンボルはPPU中のグローバルスコープで宣言された変数名に_EAR_のプレフィックスを付けた名前にネームマングリングされる。

[Cell] CESOFを使用してPPU/SPU間で変数を相互に参照する

でEARを利用した簡単なプログラムを書いた。


実行可能SPUイメージがどのようにPPUプログラム内にロードされるか
PPUプログラム内でSPUイメージを明示的にメモリマップするには、libspe.hで提供されるspe_open_image関数を使用する方法である。次の場合、spe_handle_t型の変数spe_handleでSPU実行イメージの参照が可能である。


spe_program_handle_t *spe_handle;
speid_t spe_id;
spe_handle = spe_open_image(SPU_FILE_NAME);
spe_id = spe_create_thread(0, &spe_handle, NULL, NULL, -1, 0);


図 Separatted PPE and SPE code module life cycles.

OSによりメモリマップする方法は、CESOF(Cell Embeded SPU Object Format)を利用する。リンク可能なSPUオブジェクトを作成する方法は次の記事で紹介。

[Cell] PPUプログラムにSPUプログラムを組み込む

embeded-spuは実行可能SPUをリンク可能なCESOFに埋め込んでラップしたオブジェクトを作成する。リンク可能なCESOFは次のような構造を持つ。

図 CEOF linkable layout

先ほどのソース中のspe_program_handle_t型は、次のような構造体で.dataセクション中に配置される。

typedef struct spe_program_handle {
unsigned int handle_size;
void *elf_image;
void *toe_shadow;
} spe_program_handle_t;

  • ハンドラのサイズ
  • 実行イメージへのポインタ
  • Shadow SPU TOE領域(Table of EARs)
といった要素をもつ。(EAR: Effective Address Reference)
(Shadow SPU TOEの良い訳語がみつからなかったのでこのままで)

CESOFがどのように組み込まれて動作するのかを見ていく。

1. 実行可能SPUイメージのロードとシンボル作成
実行時に、SPU実行イメージがSPEローダーによりLS(ローカルストア)中の.rodata.speelfと呼ばれる特別なセクションにロードする。embedespuは、SPU実行イメージをポインタで参照するためのPPEシンボル(ファイル名と同じ)を作成する。構造体のメンバの(void *elf_image)は、.rodata.speelf中にロードされた実行可能SPUイメージへのポインタである。


% spu-readelf -S spu/spu_hello-embed.o | grep speelf
[ 2] .rodata.speelf PROGBITS 00000000 000080 004080 00 A 0 0 128
%

.rodata.speelfセクションは名前の通り読み込み専用である。これは、同一マシン上で実行中の複数のCBEアプリケーションによりSPE実行イメージが共用される可能性があるためである。

2. Shadow SPU TOE領域
Shadow SPU TOE領域は、実行可能SPU上に埋め込まれたTOEセグメントをミラーリングするための領域である。TOE(Table of EARs)セグメント中には、EARが集められていてembededspuはそれぞれのEARに一致するPPEシンボル参照を作成する。(_EAR_プレフィックスを取り除いたシンボル名)
また、spu_program_handle構造体は、図 CEOF linkable layoutへのポインタ(void *toe_shadow)を持つ。

3. SPEプログラムの扱い
CESOFはCESOF情報を持つデータ構造を実行時に提供する。このデータ構造は二つのポインタから構成されており、一つは組み込まれた実行可能SPEイメージを指し、もう一つはShadow領域を指す。この二つのポインタの値はPPEリンカーがリンク可能なCESOFを最終の実行イメージにリンクした場合のみ解決される。

embedspuがしてくれること
  1. .rodata.speelfと.data.spetoeへのポインタを持つspe_program_handle_t型の構造体の変数を定義する。
  2. .rodata.speelfにSPE実行イメージをロードする。(by .incbin)
  3. SPUバイナリ中の_EAR_を持つシンボル名を抜き出し、.data.spetoeに対応するPPEシンボルのアドレスを格納する。

CESOFの基本的な仕組みを学んだ。
低レベルのアーキテクチャから並列処理・SIMDにいたる深くて広い知識を理解したうえでプログラミングテクニックを展開しないと、ハイスピードなプログラムが書けないというエンジニアにとってとても挑戦的なプロセッサがCell. PPEリンカの動作などを理解しているというにはまだまだ程遠いが、次はCell上でのオーバーレイを試す。

参考
Programming the Cell Broadband Engine
IC 2006 新しいマルチコアプロセッサ Cell で遊ぼう

Monday, December 04, 2006

[Cell] CESOFを使用してPPU/SPU間で変数を相互に参照する

CESOF(Cell Embeded Object Format)は、実行可能SPEプログラムをPPEプログラムに埋め込むためのフォーマット。

前回の[Cell] PPUプログラムにSPUプログラムを組み込むでは、ppu-embedespuコマンドでCESOFオブジェクト(hello_spu-embed.o)を作成して静的ライブラリにしPPUプログラムのリンク時に合わせてリンクすることで、PPUプログラムからspu_program_handle_t型の外部変数としてSPUプログラムを参照可能になるようにした。

作成されたCEOSFのシンボル表には、hello_spuシンボルが定義されておりこれによりPPUプログラムでSPUプログラムを参照できるようになっている。


% nm spu/hello_spu-embed.o
00000000 D hello_spu
%

今回作成するプログラムについて
同じくCESOFを使用してPPU/SPU間で変数を相互に参照するコードを書く。PPU側で宣言された変数をSPU側で参照し出力する。その変数をSPU側書き換えてSPUの処理が完了するのを待ってからPPU側で出力する。

まずは、作業ディレクトリを作成し、その中に次のようにディレクトリとファイルを構成する。

% ls -R
.:
Makefile ppu/ spu/

./ppu:
Makefile ppu_hello.c

./spu:
Makefile spu_hello.c spu_hello_toe.s
%

ppu/ppu_hello.c
PPUプログラムではhello_stringというシンボル名を持つ変数を宣言する。この変数がSPU側から参照されることになる。

#include <stdio.h>
#include <stdlib.h>
#include <libspe.h>
#include <errno.h>
#include <sched.h>

#define BUF_SIZE 256

/* this symbol definded in spu_hello-embed.o */
extern spe_program_handle_t spu_hello;

char hello_string[BUF_SIZE] = "Hello, World!";

int main() {
int status;
speid_t spe_id;
spe_gid_t gid;

/* Create SPE thread group */
gid = spe_create_group(SCHED_OTHER, 0, 1);
if(gid == 0) {
fprintf(stderr, "Failed spe_create_group(errno=%d)\n", errno);
return 1;
}

/* Create SPE thread */
printf("hello_string: %p\n", &hello_string);
spe_id = spe_create_thread(gid, &spu_hello, NULL, NULL, -1, 0);
if(spe_id == 0) {
fprintf(stderr, "Failed spe_create_thread(errno=%d)\n", errno);
return 1;
}

/* Waiting SPE thread */
spe_wait(spe_id, &status, 0);

/* print hello_string */
printf("PPU: %s\n", hello_string);

return 0;
}


spu/spu_helllo.c
PPUで宣言したhello_stringをSPU側で参照し値を出力する。SPU側では、_EAR_というプレフィックスを付けることで、spuembedがCESOFオブジェクトを作成する際にシンボル名から_EAR_取り除いた値に変換してくれる。EARはEffective Address Referenceの略。
(シンボルについては、下記で説明)

#include <stdio.h>
#include <stdlib.h>
#include <libmisc.h> /* copy_to_ls and copy_from_ls */
#include <spu_mfcio.h>
#include <string.h>

static const int BUF_SIZE = 256;

/* to be resolved by CESOF in the system memory */
extern unsigned long long _EAR_hello_string;

int main(unsigned long long spuid __attribute__((unused)),
unsigned
long long argp __attribute__((unused))) {
char buf[BUF_SIZE];

/* Copy data from system memory to Local Storage */
printf("_EAR_hello_string: 0x%llX\n", _EAR_hello_string);
copy_to_ls((uint32_t)(&buf), _EAR_hello_string, sizeof(buf));
printf("SPU: %s\n", buf);

/* change buf */
strncpy(buf, "Welcome to SP program.", sizeof(buf));
copy_from_ls(_EAR_hello_string, (uint32_t)(&buf), sizeof(buf));

return 0;
}
LSとメインメモリ間のデータ転送については、copy_to_ls/copy_from_ls関数を使用する。それぞれ、libmisc.hで定義されている。ppu-embededにより作成されるCESOFオブジェクト(下記の例ではspu_hello-embbed.o)では、_EAR_がとりのぞかれhello_stringで参照するように変換され、PPEリンカがシンボル名を解決する。

% nm spu/spu_hello-embed.o
U hello_string
00000000 D spu_hello
% nm spu/spu_hello.o
00000000 r BUF_SIZE
U _EAR_hello_string
U copy_from_ls
U copy_to_ls
00000000 T main
U printf
U strncpy
%

このままでは、リンク時に_EAR_hello_stringシンボルの実効アドレスをリンカが解決できない。CESOFではEARは.toe(table of EARs)と呼ばれるセクションに置かれる。そこで、このセクションに関するアセンブリを書いて合わせてコンパイルする必要がある。(CESOFの詳細参照 <- 書いたらリンクに置き換える)

spu_hello_toe.s

.section .toe, "a", @progbits
.align 4
.global _EAR_hello_string
_EAR_hello_string:
.octa 0x0


作成したアセンブリはspu-asを使用することでオブジェクトファイルにコンパイルできる。コンパイルされたコードをリンク時に合わせてリンクすることで.toeセクションを追加できる。

% spu-as -I. -I /opt/IBM/cell-sdk-1.1/sysroot/usr/spu/include -o spu_hello_toe.o spu_hello_toe.s


CESOFの作成、静的ライブラリの作成、アセンブリコードのコンパイルといったこれらの処理も、下記に書くCell SDKが提供するMakefileを利用すれば全て自動的にやってくれる。

コンパイルに必要なMakefile達

Makefile
DIRS = spu ppu
include /opt/IBM/cell-sdk-1.1/make.footer
ppu/Makefile
PROGRAM_ppu = ppu_hello
IMPORTS = ../spu/spu_hello.a -lspe
include /opt/IBM/cell-sdk-1.1/make.footer
spu/Makefile
PROGRAM_spu   = spu_hello
LIBRARY_embed = spu_hello.a
IMPORTS = $(SDKLIB_spu)/libc.a $(SDKLIB_spu)/libmisc.a
include /opt/IBM/cell-sdk-1.1/make.footer


実行
# ./ppu_hello
hello_strint: 0x181688
_EAR_hello_string: 0x1816188
SPU: Hello, World!
PPU: Welocome to SPU Program.
#


TOEセクション

% spu-readelf -S spu/spu_hello | grep -w .toe
[ 6] .toe PROGBITS 00001680 0016c0 000010 00 A 0 0 16
%
% spu-objdump -s --start-address=0x1680 --stop-address=0x1690 spu/spu_hello

spu/spu_hello: file format elf32-spu

Contents of section .text:
Contents of section .rodata:
Contents of section .ctors:
Contents of section .dtors:
Contents of section .data:
Contents of section .toe:
1680 00000000 00000000 00000000 00000000 ................
Contents of section .comment:
Contents of section .debug_aranges:
Contents of section .debug_pubnames:
Contents of section .debug_info:
Contents of section .debug_abbrev:
Contents of section .debug_line:
Contents of section .debug_frame:
Contents of section .debug_str:
Contents of section .debug_loc:
Contents of section .note.spu_name:
%

TOEセクションの詳細とCESOFの動作原理の詳細については、あとで書く。

Sunday, December 03, 2006

[Cell] PPUプログラムにSPUプログラムを組み込む

SPUプログラムは静的ライブラリとしてPPUプログラムに組み込んでコンパイルすることができる。組み込みでコンパイルした際は、PPUプログラムからextern属性をつけて実行プログラム名でspe_program_handle_t型の変数として参照することが出来る。

組み込みSPEを作るには、オブジェクトファイルを作成してからppu-embedspuコマンドで組み込み用のオブジェクトファイルを作成し、ppu-arコマンドで静的ライブラリを作成する。次の例は、hello_spuというSPUプログラムから組み込み用の静的ライブラリを作成する。


% spu-gcc -o hello_spu hello_spu.o -Wl,-N /opt/IBM/cell-sdk-1.1/sysroot/usr/spu/lib/libc.a
% ppu-embedspu -m64 hello_spu hello_spu hello_spu-embed64.o
% ppu-ar -qcs hello_spu.a hello_spu-embed64.o


ppu-gccでリンクする際に作成した静的ライブラリも合わせてリンクするとPPUプログラム内で次のようにして実行イメージを参照可能になる。


extern spe_program_handle_t spe_program;


組み込みSPUを使用した、簡単なプログラムを作成した。
ファイル&ディレクトリの構成は次の通り。

% ls -R
.:
Makefile ppu/ ppu_time.h spu/

./ppu:
Makefile spu_program_embed.c

./spu:
Makefile hello_spu.c
%


ppu/spu_program_embed.c

#include <stdio.h>
#include <libspe.h>
#include <errno.h>
#include <sched.h>
#include <stdint.h>
#include "../ppu_time.h"

extern spe_program_handle_t hello_spu;

int main () {
int status;
spe_gid_t gid;
speid_t spe_id;
uint32_t ts, te;

StartTimer(ts);
puts("Starting PPE program.");
gid = spe_create_group(SCHED_OTHER, 0, 1);
if(gid == 0) {
fprintf(stderr, "Failed spe_create_group(errno=%d)\n", errno);
return 1;
}
if(spe_group_max(gid) < 1) {
fprintf(stderr, "System doesn't have a working SPE.\n");
return 1;
}

spe_id = spe_create_thread(gid, &hello_spu, NULL, NULL, -1, 0);
if(spe_id == 0) {
fprintf(stderr, "Failed spe_create_thread(errno=%d)\n", errno);
return 1;
}
printf("Waiting SPE thread...\n");
spe_wait(spe_id, &status, 0);
printf("done.\n");
StopTimer(te, ts);

PrintTimer(te);

return 0;
}


spu/hello_spu.c

#include <stdio.h>

int main(unsigned long long speid, unsigned long long argp) {
puts("SPE: Hello, world!");
return 0;
}


コンパイルはCell SDKで提供されているMakefile(make.footer)を使用すると簡単である。
./Makefile

DIRS = spu ppu
include /opt/IBM/cell-sdk-1.1/make.footer


ppu/Makefile
IMPORTSを設定して、静的ライブラリをリンク時に含めるようにする。

PROGRAM_ppu64 = ./spu_program_embed
IMPORTS = ../spu/hello_spu.a -lspe
CC_OPT_LEVEL = -O0
include /opt/IBM/cell-sdk-1.1/make.footer


spu/Makefile
LIBRARY_embed64に静的ライブラリ名を設定すると、自動的に作成してくれる。

PROGRAMS_spu := hello_spu
LIBRARY_embed64 := hello_spu.a
IMPORTS = $(SDKLIB_spu)/libc.a
include /opt/IBM/cell-sdk-1.1/make.footer


実行結果

% ./spu_program_embed
Starting PPE program.
Waiting SPE thread...
SPE: Hello, world!
done.
time: 0.83680(msec)
%

spe_open_imageでSPE実行プログラムを読み込む方法と組み込み方法の実行時間を比較してみた。





















回数組み込みspe_open_image
1回目0.881480(msec)0.840640(msec)
2回目0.835840(msec)0.838560(msec)
3回目0.833240(msec)0.838640(msec)

実行速度はほぼ同じだった。(シュミレータ上で実行した事が原因?)

Saturday, December 02, 2006

[Cell] SPUで分岐ヒント命令を明示的に書くが

うまく動作しない。

SPUは26段の深いパイプラインを持つため、分岐予測が外れた場合には15サイクルのペナルティが発生する。この分岐ペナルティへの対処として、SPUではハードウェア的な分岐予測回路ではなく、ソフトウェア的に分岐予測へのヒントを与える分岐ヒント命令を実装している。

分岐ヒント命令を与えた場合と与えない場合でどれほど実行速度に差が出るか計測してみた。SPE上で実行時間計測には、[Cell] SPE上での実行時間計測を利用した。


#include <stdio.h>
#include <spu_intrinsics.h>
#include "../spe_time.h"

#define likely(cond) __builtin_expect((int)(cond), 1)
#define unlikely(cond) __builtin_expect((int)(cond), 0)

static const int N = 100000;
void nope(void){}

int main() {
int i;
unsigned int t1, t2;

// 条件分岐を含むループƒ—
StartTimer(t1);
for(i=0;i<N;i++) {
if(i>100000) nope();
}
StopTimer(t1);

// 分岐ヒント命令を与えた条件分岐を含むループƒ—
StartTimer(t2);
for(i=0;i<N;i++) {
if(unlikely(i>100000)) nope();
}
StopTimer(t2);

PrintTimer(t1);
PrintTimer(t2);

return 0;
}


実行
#./spu.out
timer: 0.43780(msec)
timer: 0.468720(msec)
#

最適化なし。-O3で最適化すると実行速度はどちらも同じになる。
分岐ヒント命令を与えたほうが遅くなってるし。。
分岐ヒント命令を与えたループと与えないループのアセンブリコード。

42,44c42,45
< lqd $2,64($sp)
< ila $3,100000
< cgt $2,$2,$3
---
> lqd $3,64($sp)
> ila $2,100000
> cgt $2,$3,$2
> sfi $2,$2,0


unlikeyマクロで定義した(int)(cond)と0の比較を行うための、SFI $2, $2, 0という減算処理命令が追加されている。(下側が分岐ヒント命令を与えたアセンブリ)
その分実行速度が遅くなった様子。

分岐ヒント命令は、hbr, hbra, hbrrというニーモニックで与えられるのだが生成されたアセンブリコードには該当する命令はなかった。シュミレーター上では分岐ヒント命令は動作しない??

参考
Cell Broadband Engine Programming Tutorial Version 1.1

Thursday, November 30, 2006

[Cell] Function-Offloadプログラミングモデル 2

[Cell] Function-OffloadモデルでHello, Worldでは、SPE上でhelloプロシージャを実行した。

今回は入力引数として2つの配列を受け取り、出力として配列の各々の値を合計した値を返すリモートプロシージャーを定義する。この加算処理を行うプロシージャを次の3パターン定義する。

  • 同期(SPEでの処理が終了するまで待つ)して実行する。
  • 同期+SIMDで実行する。
  • 非同期で実行する。
前回同様に、次の順番で3つのファイルを作成する。
  1. インタフェース定義ファイル(IDL)を定義 (add.idl)
  2. プロシージャの実装を定義 (spu/add.c)
  3. PPEメインプログラム (ppu/add.c)
インタフェースの定義フォーマット

プロシージャーの定義は次の4つのコンポーネントから構成され、フォーマットは次の通り。
  • 同期タイプ
  • 返り値(常にidl_id_t)
  • プロシージャの名前
  • パラメータの詳細
[<op_attribute>] idl_id_t <identifier> <parameter_declarators>

  • op_attribute - プロシージャーの同期に関して指定する。
  • sync - 同期実行する。PPEアプリケーションはSPEでの実行が終了するまで待たされる。
  • async_b - 非同期で実行する。プログラムは関数が値を返すとすぐに入力用のバッファを再利用できる。プロシージャーの返り値idl_id_tは、以降のPPEプログラム中のjoin_func(idl_id_t id)関数中で、プロシージャーの実行完了を待つ際に使用する。
  • async_i - async_bと同じだが、プロージャーを呼び出した後の入力引数のバッファをjoin_funcが成功する以前に再利用することが出来ない。
  • async - async_iと同じ。
引数の定義(parameter_attributes)は次のようなフォーマットに従う。
[<parameter_attributes>] <type_specifier> <parameter_declarator>

parameter_attributes以下のどれかを指定する。
  • in - 入力パラメータであることを指定する。データはPPEプログラムからSPEプロシージャーへ渡される。
  • out - 出力パラメータであることを指定する。データはPPEプログラムからSPEプログラムへ渡される。
  • size_is(val) - valのサイズを指定する。整数か定数かを指定する。
  • タは整数か定数で指定されるvalのサイズを持つ。この値が配列のサイズに一致しなければダブルバッファリングクエリは無視される。

引数に配列を指定する際は、size_is(val)属性を使用してサイズを指定しなければならない。



add.idl: リモートプロシージャのインタフェースを定義する。

interface add {
import "../stub.h";

const int ARRAY_SIZE = 1024;
[sync] idl_id_t do_add([in] int array_size, [in, size_is(array_size)] int array_a[],
[in, size_is(array_size)] int array_b[],
[out, size_is(array_size)] int array_c[]);

[sync] idl_id_t do_vec_add([in] int array_size, [in, size_is(array_size)] int array_a[],
[in, size_is(array_size)] int array_b[],
[out, size_is(array_size)] int array_c[]);

[async] idl_id_t do_async_add([in] int array_size, [in, size_is(array_size)] int array_a[],
[in, size_is(array_size)] int array_b[],
[out, size_is(array_size)] int array_c[]);
}


spu/do_add.c: プロシージャの実装をする。

#include <stdio.h>
#include <spu_intrinsics.h>
#include <idl_util.h>
#include "../stub.h"

idl_id_t do_add(int array_size, int array_a[], int array_b[], int array_c[]) {
int i;
printf("SPU do_add: start executing with array_size=%d, array_a=0x%x, array_b=0x%x\n", array_size, (int)array_a, (int)array_b);
for(i=0;i<array_size;i++) {
array_c[i] = array_a[i] + array_b[i];
}

return 0;
}

idl_id_t do_vec_add(int array_size, int array_a[], int array_b[], int array_c[]) {
int i;
printf("SPU do_vec_add: start executing with array_size=%d, array_a=0x%x, array_b=0x%x\n", array_size, (int)array_a, (int)array_b);
vec_int4 *vi_a = (vec_int4 *) array_a;
vec_int4 *vi_b = (vec_int4 *) array_b;
vec_int4 *vi_c = (vec_int4 *) array_c;
for(i=0;i<array_size/4;i++) {
vi_c[i] = spu_add(vi_a[i], vi_b[i]);
}

return 0;
}

idl_id_t do_async_add(int array_size, int array_a[], int array_b[], int array_c[]) {
int i;
printf("SPU do_add: start executing with array_size=%d, array_a=0x%x, array_b=0x%x\n", array_size, (int)array_a, (int)array_b);
for(i=0;i<array_size;i++) {
array_c[i] = array_a[i] + array_b[i];
}
return 0;
}

プロシージャの出力は、インタフェース定義ファイルで[out]属性を指定したパラメータに設定することでPPE上から値を参照できる。


ppu/add.c: PPE上で実行するメインプログラムを定義する。

#include <stdio.h>
#include <libspe.h>
#include <libidl.h>
#include "../stub.h"
#include "../ppu_time.h"

int array_a[ARRAY_SIZE] __attribute__((aligned(128)));
int array_b[ARRAY_SIZE] __attribute__((aligned(128)));
int array_add[ARRAY_SIZE] __attribute__((aligned(128)));

#define PrintDebug(array, size) { \
unsigned int i;\
printf("array: {");\
for(i=0;i<size;i++) printf("%d ", array[i]); \
printf("}\n"); \
}

int main() {
int i, async_add_id;
uint64_t ts, time_do_add, time_do_vec_add, time_do_async_add, time_ppu_add;

puts("PPU starting.");
for(i=0;i<ARRAY_SIZE;i++) {
array_a[i] = i << 1;
array_b[i] = i;
array_add[i] = 0;
}

// リモートプロシージャを同期呼び出し—
printf("PPU: do_add calling...\n");
StartTimer(ts);
do_add(ARRAY_SIZE, (int*)(&array_a), (int*)(&array_b), (int*)(&array_add));
StopTimer(time_do_add, ts);
puts("done.");

// SIMDを用いて加算処理するプロシージャーの呼び出し†
printf("PPU: do_vec_add calling...\n");
StartTimer(ts);
do_vec_add(ARRAY_SIZE, (int*)(&array_a), (int*)(&array_b), (int*)(&array_add));
StopTimer(time_do_vec_add, ts);
puts("done.");

// リモートプロシージャを非同期呼び出し—
#ifdef IDL_ASYNC
printf("PPU: do_async_add calling...\n");
StartTimer(ts);
async_add_id = do_async_add(ARRAY_SIZE, (int*)(&array_a), (int*)(&array_b), (int*)(&array_add));
StopTimer(time_do_async_add, ts);

printf("PPU join_do_add \n");
idl_join_do_async_add(async_add_id);
#endif

// PPU 上で同じ処理を実行Œ
StartTimer(ts);
for(i=0;i<ARRAY_SIZE;i++) array_add[i] = array_a[i] + array_b[i];
StopTimer(time_ppu_add, ts);

// 実行時間を出力Š›
printf("PPU do_add: ");
PrintTimer(time_do_add);
printf("PPU do_vec_add: ");
PrintTimer(time_do_vec_add);
printf("PPU do_async_add: ");
PrintTimer(time_do_async_add);
printf("PPU add: ");
PrintTimer(time_ppu_add);

return 0;
}
Tips:
プロシージャで利用する、array_a, array_b, array_cとDMA転送されるデータを128byte境界でアラインする。実際PPEプログラム中でリモートプロシージャに渡す引数の値は128byteでアラインしなくても動作した。これは生成されたスタブ中で128byte境界でアラインするようにメモリの再割り当てを行っているためである。だが、ここでは128byteアラインを明示的に書くことにした。

IDLコンパイラにより生成されるソースコードの一部(ppu/stub_add.c)
/* make sure array_a is properly aligned */

/* allocate 128 bytes aligned chunk of mem for idl_wi->array_a */
idl_alloc_array_a = (int*)alloca(array_size*sizeof(int) + 127);
idl_wi->array_a = (int*)(((unsigned int)idl_alloc_array_a + 127) & 0xFFFFFF80);

memcpy (idl_wi->array_a, array_a, array_size*sizeof(int));
全ての引数に対してこの処理を適用している。

プロシージャの返り値であるidl_id_t型は、IDLコンパイラによってプロシージャーごとにユニークな値(1001から順にインクリメントされた値)が割り当てられる。この値を使用してプロシージャーを識別しidl_join_{プロシージャ名}で実行完了を待つことが出来る。

ppu_time.hの内容は、[Cell] PPE上での実行時間計測のプロファイリング用の関数とマクロをヘッダファイルにしたもの。

コンパイルと実行
ディレクトリ構造は、[Cell] Function-OffloadモデルでHello, Worldの記事と同じspu, ppuというサブディレクトが存在する。Makefileを次のように作成する。

./Makefile
DIRS      := ppu spu
IDL_SRC := add.idl
IDL_FLAGS := -i -p ppu/stub_add.c -s spu/stub_add.c -n 4 -b spu_add
INCLUDE := -I $(SDKINC) -I spu
include /opt/IBM/cell-sdk-1.1/make.footer

ppu/Makefile
PROGRAM_ppu     = ./ppu_add
CFLAGS := -DIDL_ASYNC -DDEBUG
CC_OPT_LEVEL := -O0
IMPORTS = $(SDKLIB)/libidl.a $(SDKLIB)/libmisc.a -lspe

CC_OPT_LEVELで最適化レベルを最適化無しにしないと、ppu_time.h中のmftb関数がインライン展開されて、loopラベルが衝突する。(これ、どうすればいいのだろう?)

spu/Makefile

PROGRAM_spu = $(SDKBIN_ppu)/samples/spu/spu_sync_add
IMPORTS = $(SDKLIB)/libc.a
include ../../../../../../make.footer
コンパイルと実行
% make
% cp spu/spu_add ppu/ppu_add /tmp

Cellシミュレータ上に転送

# callthru source /tmp/ppu_add > ppu_add
# callthru source /tmp/spu_add > spu_add
# chmod +x spu_add ppu_add
# ./ppu_add
省略
PPU do_add: time: 17.489200(msec)
PPU do_vec_add: time: 7.99760(msec)
PPU do_sync_add: time: 2.018320(msec)
PPU add: time: 0.008000(msec)
#

シミュレータ上での実行なので、上記のプロファイリング値はあくまで参考だが、実機でもスタブを介したDMA転送のコストが気になる。DMA転送を減らしながらタスクをSPEに並列化してスケジューリングするあたりのTipsが必要な気がする。PPU上のL1, L2キャッシュ機構やDMA転送のコスト、MFCといったCellのアーキテクチャをあとでやる。

実行結果



実行できた。

Cellプログラミングチュートリアル

オリジナルの文章に加えていくつか書いていきます。
Cell Broadband Engine Programming Tutorial Version 1.1

順次更新予定。

2.1 PPE レジスタ

GPRs(General-Purpose Registers)
32個の64ビット幅のGPR(汎用レジスタ)を持つ。

FPR(Floating-Point Registers)
32個の64ビット幅の浮動小数点レジスタを持つ。浮動小数点の形式はIEEE754の倍精度フォーマットに従う。単精度の結果も内部的には倍精度として扱われる。

LR(Link Register)
64bitのLRは分岐先の実行アドレスを扱う。分岐命令は、LK(link bit)に1が設定されている場合次の命令をLRにコピーする。Move To Special-Purpose Register命令は、GPRの内容をLRにコピーする。

CTR(Counter Register)
64bitのCTRは分岐先の実行アドレスかループカウンタのどちらかを保持することが出来る。いくつかの条件分岐命令フォームはCTRの値を減少させ0かどうか判定する。Move To Special-Purpose Register命令はGPRの値をCTRにコピーする。

XER(Fixed-Point Exception Register)
64bit XERレジスタは、整数オペレーションでのキャリーやオーバフローなどの条件、及びインデクス付きロードストア命令で転送されるバイト数を扱う。

CR(Condition Register)
32bitのコンディション・レジスタはテストと分岐のためのメカニズムを提供する。
比較命令か命令を読みこむ事により32ビットCR中の条件コードが初めに設定されて比較命令によって使われる。

FPSCR(Floating-Point Status and Control Register)
プロセッサは、結果と関連した例外に関する全ての浮動小数点演算の後で、32bitのFPSCRを更新する。IEEE 754によって必要とされているステータス情報が含まれる。加えて例外を扱うためのいくつかの情報がある。

VMRs(Vectoor Multimedia Registers)
全てのベクタ演算のためのソースとあて先を処理するための128bit幅のVMRが32個存在する。

VSCR(Vector Status and Control Register)
32bit VSCRはFPSCRと似た方法で読み書きが行われる。2つの定義されたビットを持ち、non-Javvaモードビットとsaturationビットであり、残りのビットは予約済みである。VSCRからVMRレジスタに移動させるための命令が提供されている。

VRSAVE(Vector Save Register)
32bit VRSAVEレジスタはコンテクストスイッチを横切った状態中のユーザ情報や特権のあるソフトウェアをサポートする。



2.2.1.3 Compatibility with Existing PowerPC Code
PowerPCアーキテクチャのPPE version2.0.2は以下のユーザモード命令が実装されている。

fsqrt(.) - 倍精度浮動小数の平方根
fsqrts(.) - 単精度浮動小数の平方根
fres(.) - 単精度浮動小数の逆数
frsqrte(.) - 倍精度浮動少数の逆数
fsel(.) - 浮動少数
mtofrf - CRのフィールドの一つをXFX-formに移動する
bccbr - CBRへの分岐条件

3.5.1 DMA変換

3.5.2 DMAリスト変換

3.5.3 ダブルバッファ

3.5.4 スカラー演算のベクタ演算化



3.5.5.3 分岐ヒントを用いて分岐予測ミスを減少させる
SPUは26段という深いパイプラインを動作周波数を向上させている。SPUのパイプライン処理は深いため、分岐予測が外れた場合には15サイクルのペナルティが発生する。この分岐ペナルティへの対抗策として、プログラマがソフトウェアレベルで分岐予測アドレスを設定する分岐ヒント命令を用意している。ハードウェアレベルレベルでの分岐予測回路はない。

一般的なプロセッサは通常BHT(分岐ヒストリテーブル), BTAC(分岐先アドレスキャッシュ), BTIC(分岐先命令キャッシュ)を用いたハードウェアレベルの分岐予測を持っている。

ブランチヒント命令は3種類提供される。
・ブランチ先のアドレス
・実際のブランチ命令のアドレス
・プリフェッチスケジュール


3.5.5 分岐処理のコストを減少させる。


3.5.6 関数のインライン化とループアンローリング


4. プログラミングモデル
Cellプログラミングでは7つのプログラミングモデルが提唱されている。

0. Function-Offloadモデル
パフォーマンスを要求するような計算処理をRPCを利用してPPE上からPPE上で定義された関数呼ぶようにして、SPE上で実行する事を可能にする。PPEとSPEの通信処理は、IDLコンパイラが生成するスタブクラスが隠蔽してくれる。DMA転送のコストが気になる。

Tutorials:
  1. [Cell] Function-OffloadモデルでHello, World
  2. [Cell] Function-Offloadプログラミングモデル 2

1. デバイス拡張モデル
デバイス拡張モデルは、SPEがI/Oデバイスのように動作するFunction-Offloadモデルの特別なケース。
全てのI/Oデバイスはメモリマップされているため、SPEはI/Oデバイスと対話することができ、SPEのDMA変換は単一のバイトサイズへの変換をサポートする。I/Oデバイスは、SPEのシグナル通知機構を利用しコマンドの完了をSPEに通知することが出来る。

SPEがデバイス拡張モデルを使用しているとき、それらはOSの一部として特権を持ったプログラムとして実行される。このコードは信頼されて、物理的なデバイスのレジスタへのアクセス権限を与えられるかもしれない。例えば、セキュアなファイルシステムはデバイスとして扱われる。OSのデバイスドライバは、ディスクコントローラが仮想デバイス上の全てのファイルの読み込みと書き込みに応じるためや暗号化と複合化のためにSPEを使用して書き込むことが出来る。

2. 演算加速モデル(Computation-Acceleration Model)
演算加速モデルは、小さい粒度でSPEを統合するSPE中心のプログラミングモデル。
このモデルは、アプリケーションの大きな書き換え無しにアプリケーションをスピードアップさせることが出来る。このモデルはSPEと通信するために共有メモリかメッセージ・パッシングモデルを使用する。

3. ストリーミングモデル
ストリーミングモデルでは、データストリームを介して連続または並列にパイプラインで演算がおこなわれる。PPEはストリーム制御として動作し、SPEはストリームデータの処理をする。SPEでは、チップへの読み込みと書き込みの帯域は、チップ外のDMA転送のバンド幅を一桁上回る。もしそれぞれのSPEは大量の仕事を持っているならば、Cell上ではこのモデルは効果的に動作する。なぜなら、データ長い間Cell Broadband Engine上にとどまらせることができるからである。PPEとSPEは、PPEと処理中のSPE間、その他のSPE間のメッセージパッシングをサポートしている。

だけれども、SDKでは一般的なストリーミング言語はサポートしていない。大抵のCellプログラマは、ストリーミングモデルを拡張して使うことを好む。例えば、P104のSection 3.6.3中のオイラー粒子シミュレーションはストリーミングモデルを実装している。粒子シミュレーションはデータパケットを同時にそれぞれのステップを同時にストリームするカーネルからなる。

参考
http://ja.wikipedia.org/wiki/Stream_processing

4. 共有メモリマルチプロセッサモデル

5. 非対称スレッド実行モデル(Asymmetric-Thread Runtime Model)


6. ユーザモードスレッドモデル
ユーザモデルスレッドは、一つのSPEスレッドが並列に動作するユーザレベル関数の集合を管理する。ユーザレベル関数は、マイクロスレッド(ユーザスレッドとユーザレベルのタスクも)と呼ばれる。SPEスレッドはOSによってサポートされる。マイクロスレッドはユーザソフトウェアによって作成されサポートされる。OSは含まれない。しかしながらマイクロスレッドの集合は複数のSPUをまたがって実行することが出来る。

SPUアプリケーションは共有メモリ中のタスクをスケジューリングし、タスクは利用可能なCPUで処理される。例えばゲームプログラミングでは、タスクは更新する必要があるシーンオブジェクトを参照することが出来る。マイクロスレッドはいつでも完了することが出来、新しいマイクロスレッドはいつでも生成される。

このプログラミングモデルの利点は、SPUの集合をSPEスレッド下で実行することで予測可能なオーバヘッドを持つことである。一SPEは、PPEの補助無しにMFCコマンドキューを削除したり格納したりすることは出来ない。


7. SPE プラグイン
SPEプラグインでは、SPEのLS(256KB)にプログラムが収まらないとき、オーバーレイを使用することで大きなプログラムを実行できる。オーバーレイは、SPUコードを動的にロードしSPUプログラムとして動作させる。

SPE Pluginsでは、モジュラ型内のSPUコードをプログラマが管理することができる。実行時に必要とされる特定のSPUコードは動的にロードされる。このモデルはコードをrequireする他のSPEプログラミングモデルとは異なり、読み込まれるコードはあらかじめ知ることはできない。
SPE PluginはSPUプログラムの実行スタックを使用し、グローバルから参照することは出来ない。SPE Pluginは、入力/出力用のパラメータではなく、実行中のSPUプログラムを用いて通信をする。


参考
次世代プロセッサ Cell Broadband Engine

[Cell] Function-OffloadモデルでHello, World

Cellプログラミングにはいくつかのプログラミングモデルが存在する。
Cell上でのプログラミングモデルを順を追って紹介してみる。

第一回は、PPEとSPEの間でRemote Procedure Callを行うFunction-Offloadモデルと呼ばれるプログラミングモデルで一番単純なHello worldを書いてみた。このプログラミングモデルは、PPEからSPE上のプロシージャをPPE上で定義されたプロシージャのように呼び出すことが出来るRPCを実現する。

前回の記事、SPE上で実行時間を計測するでは、SPEの実行プログラムをオープンしSPEスレッドを作成して処理を実行し、そして処理が終了するのを待ってから、SPEプログラムハンドラを閉じて終了した。


spe_open_image();
spe_create_thread();
spe_wait();
spe_close_image();


Function-Offloadモデルでは、プログラム単位でSPEに処理をさせるのではなく、プロシージャ単位でSPUを使用して処理を並列化することを可能にする。Cell上ではスタブを利用することでMDAやMFCといった低レベルの詳細を知ることなく、PPE上からSPE上のプロシージャを実行することが可能になる。スタブがプロキシの役割を果たしPPEとSPEの通信の詳細であるDMA転送やMFCといった細かくて面倒な処理をラップしてくれる。

IDLのパラメータ定義やPPEからSPE上のプロシージャを呼び出すRPCの仕組みの詳細についてはとりあえず保留。

始めにプロシージャのインタフェースをIDLファイルで定義する。そのファイルをCBE SDKが提供するIDLコンパイラでコンパイルすることで、スタブクラスのソースコード(C言語)を作成してくれる。

初めに、IDLコンパイラの実行ファイルが存在しなかったのでビルドする。

% sudo yum install byacc flex
% cd /opt/IBM/cell-sdk-1.1/src/tools/idl
% make
% ls /opt/IBM/cell-sdk-1.1/host/bin/idl

/opt/IBM/cell-sdk-1.1/host/bin/idlという実行バイナリができあがる。

では、Function-Offload モデルを利用して"Hello World!"を書いていく。
始めに次のように作業用ディレクトリを作成する。ファイルについては順に作っていく。

% ls -FR
.:
Makefile hello.idl ppu/ spu/

./ppu:
Makefile hello.c

./spu:
Makefile hello.c
%

Function Offloadモデルでプログラミングを行うには、3つのソースファイルを作成する必要がある。
  • PPE上で実行するメインファイル
  • SPE上で実行するプロシージャ
  • プロシージャのインタフェースを定義したIDLファイルである。
始めに作業ディレクトリのトップディレクトリにhello.idlというファイルを作成する。
stub.hはIDLコンパイラにより自動生成される。

IDLファイルでhelloという名前のプロシージャーを定義する。[in]と指定されているのは関数への入力のための引数で、[out]と指定される場合は出力のための引数。返り値はつねにidl_id_t型で無ければならない。(その他については別途説明)
helloは、第一引数に文字列のサイズ、第二引数に文字列へのポインタを引数として受け取る。


interface greeting
{
import "../stub.h";
[sync] idl_id_t hello ([in] int nbytes, [in, size_is(nbytes)] char message[]);
}


このIDLファイルからスタブクラスを作成するためのMakeファイルを定義する。

########################################################################
# Subdirectories
########################################################################
DIRS := ppu spu

########################################################################
# Local Defines
########################################################################
IDL_SRC := hello.idl
IDL_FLAGS := -i -p ppu/stub_hello.c -s spu/stub_hello.c -n 4 -b spu_hello

INCLUDE = -I $(SDKINC) -I spu

include /opt/IBM/cell-sdk-1.1/make.footer


-b引数はスタブをプロキシとして実行されるSPUプログラム名、-n引数は割り当てるSPUの最大数を設定する。

makeを実行すると、ppu/stub_hello.cとspu/stub_hello.cというスタブが作成される。合わせてstub.hというヘッダが作成されるのでPPEとSPEプログラム上からこのヘッダを読み込むことでプロシージャを利用することが出来る。

次にPPU上で実行するプログラムppu/hello.cを書く。

#include <stdio.h>
#include <string.h>
#include <libidl.h> // stub.hの前に読み込む‚‹
#include "../stub.h"

int main() {
char *str = "Hello, World!";
hello(strlen(str), str);
return 0;
}


SPEプログラムにhelloプロシージャーの実装の詳細を定義する。
返り値はidl_id_tで固定。引数はIDLファイルで定義したものと合わせる。

#include <stdio.h>
#include <stdlib.h>
#include <idl_util.h>
#include "../stub.h"

idl_id_t hello(int nbytes, char msg[]) {
printf("SPE: %s\n", msg);
return 0;
}
PPE/SPEプログラムをコンパイルするためのMakefileを次のように書く。

ppu/Makefile
########################################################################
# Target
########################################################################
PROGRAM_ppu = ppu_hello

########################################################################
# Objects
########################################################################
IMPORTS = $(SDKLIB)/libidl.a $(SDKLIB)/libmisc.a -lspe

########################################################################
# make.footer
########################################################################
include /opt/IBM/cell-sdk-1.1/make.footer

spu/Makefile

########################################################################
# Target
########################################################################
PROGRAM_spu = ./spu_hello

########################################################################
# make.footer
########################################################################
include /opt/IBM/cell-sdk-1.1/make.footer


作業ディレクトリのトップディレクトリでmakeしシミュレータ上に転送する。

% make
% cp ppu/ppu_hello spu/ppu_hello /tmp/


シミュレータ上でゲストOSから/tmp/ppu_helloと/tmp/spu_outを転送する。

# callthru source /tmp/ppu_hello > ppu_hello
# callthru source /tmp/spu_hello > spu_hello
# chmod +x ppu_hello spu_hello
# ./ppu_out
SPE: Hello, World!
#


見事にHello, WorldをFunction-Offloadモデルを使用し、SPE上で実行させることが出来た。

Tuesday, November 28, 2006

[Cell] SPE上での実行時間計測

SPE上で実行時間を計測するには、SPU Decrementerを利用する。
SPU Decrementerは参考資料のPDFに次のように説明されている。


A register that counts down each time an event occurs. Each SPU
contains dedicated 32-bit decrementers for scheduling or performance
monitoring, by the program or by the SPU itself.


プロファイリングのために利用できる32bitレジスタで一定周期で値が減少していく。この差を取ることで実行時間の計測が出来る。

PPUのTime Base Registerと同じように周期を知るには、/proc/cpuinfoを見る。

% cat /proc/cpuinfo | grep -w timebase
timebase: 25000000
%

SPU Decrementerもこの値が周期の基準になるのか少し疑問だが、fixstartsのWikiでもこの値を使っているようなので。

SPUチャネルにSPU_WrDec, SPU_RdDecというニーモニックを指定し書き込む or 読み込むことでレジスタの値が取得できる。

uint32_t t;
spu_writech(SPU_WrDec, 0xffffffff); // 0xffffffffを書き込み
t = spu_readch(SPU_RdDec); // SPU Decrementerの値を読み込む。€‚


この低レベルAPIをラップした関数として、spu_write_decrementer(uint32_t), spu_read_decrementer(void)という関数がspu_mfcio.hで提供されている。

uint32_t t;
spu_write_decrementer(0xffffffff);
t = spu_read_decrementer();


この関数を利用したプロファイリング用のマクロを定義する。

static const int TIMEBASE = 2.5 * 1.0e7; // cat /proc/cpuinfo | grep -w timebase

// プロファイル用関数ƒ­
#define StartTimer(ts) {spu_write_decrementer(0xffffffff); ts=spu_read_decrementer();}
#define StopTimer(te) {te -= spu_read_decrementer();}
#define PrintTimer(te) {printf("timer: %f(sec)\n", te / (float)TIMEBASE * 1.0e3);}


初期値としてSPU Decrementerに大きな値を設定しておく。StopTimerでStartTimer時に取得した値との差を計算する。PrintTimerでレジスタの減少値から実行時間を算出する。
これを利用してスカラー演算とベクタ演算の実行速度を比較する。

ソースコード

#include <stdio.h>
#include <stdint.h>
#include <spu_intrinsics.h>
#include <unistd.h>
#include <spu_mfcio.h> // spu_read_decrementer and spu_write_decrementer

static const int N = 200000;
static const int TIMEBASE = 2.5 * 1.0e7; // cat /proc/cpuinfo | grep -w timebase

// プロファイリング用マクロ
#define StartTimer(ts) {spu_write_decrementer(0xffffffff); \
ts=spu_read_decrementer();}
#define StopTimer(te) {te -= spu_read_decrementer();}
#define PrintTimer(te) {printf("timer: %f(msec)\n", te / (float)TIMEBASE * 1.0e3);}

int main(unsigned long long spu_id, unsigned long long arg) {
uint32_t profile, profile_simd, i;
uint32_t
in[4] __attribute__((aligned(16))) = {1,2,3,4};
uint32_t
out[4] __attribute__((aligned(16))) = {0};
vec_int4 *v_in = (vec_int4 *) in;
vec_int4 *v_out = (vec_int4 *) out;

// スカラー値で計算
StartTimer(profile);
for(i=0;i<N;i++) {
out[0] += in[0];
out[1] += in[1];
out[2] += in[2];
out[3] += in[3];
}
StopTimer(profile);

// SIMDでベクタ演算
StartTimer(profile_simd)
for(i=0;i<N;i++) {
spu_add(*v_out, *v_in);
}
StopTimer(profile_simd);

// 出力
printf("Scalar: ");
PrintTimer(profile);

printf("Vector: ");
PrintTimer(profile_simd);

return 0;
}

コンパイルとシミュレータへの転送用に/tmpへコピー

% make
spu-gcc -Wall -I/opt/IBM/cell-sdk-1.1/src/include/spu/ -I/opt/IBM/cell-sdk-1.1/src/include/ -I/opt/IBM/cell-sdk-1.1/sysroot/usr/lib/gcc/spu/4.0.2/ 0.c -c
spu-gcc -L/opt/IBM/cell-sdk-1.1/sysroot/usr/lib/ 0.o -o spu.out
% make install
cp spu.out ../ppu-main/ppu.out /tmp/
%
SPUプログラムを動かすためのPPUプログラムを作る。
#include <stdio.h>
#include <libspe.h>

static const char SPE_FILE_NAME[] = "./spu.out";
int main() {
int status;
spe_program_handle_t *spe_handle;
speid_t spe_id;

printf("[PPE] Open SPE program.\n");
spe_handle = spe_open_image(SPE_FILE_NAME);
if(spe_handle == 0) {
printf("ERROR: Cannot open SPE program.\n");
return 1;
}

printf("[PPE] Create SPE thread.\n");
spe_id = spe_create_thread(0, spe_handle, NULL, NULL, -1, 0);
if(spe_id == 0) {
printf("ERROR: Cannot crate SPE thread.\n");
return 1;
}

printf("[PPE] Waiting SPE thread...");
spe_wait(spe_id, &status, 0);
printf("done.\n");

printf("[PPE] Release SPE program.\n");
spe_close_image(spe_handle);

return 0;
}


コンパイルと転送
% make                                                                                                   
ppu-gcc -m32 -Wall -I/opt/IBM/cell-sdk-1.1/sysroot/usr/include 1.c -c
ppu-gcc -L/opt/IBM/cell-sdk-1.1/sysroot/usr/lib 1.o -lspe -o ppu.out -m32
% make install
cp ../spu-main/spu.out ppu.out /tmp/
%


シミュレータ上に転送し実行
# callthrue source /tmp/ppu.out > ppu.out
# callthrue source /tmp/spu.out > spu.out
# chmod +x ppu.out spu.out
# ./ppu.out
[PPE] Open SPE program.
[PPE] Create SPE thread.
Scalar: timer: 2.812480(sec)
Vector: timer: 0.687480(sec)
[PPE] Waitin SPE thread...done.
[PPE] Release SPE program.
#
追記
SPUの実行形式オブジェクトファイル単体でも実行可能。

SIMDを利用したベクタ演算の方が4倍ほど早くなっています。
SIMDでは同時に4データを扱えるので妥当なところかな。
参考
Cell Broadband Engine Programming Tutorial Version 1.1 (PDF)
lesson12 - Pukiwiki