Cell BE プロセッサーでのハイパフォーマンス・アプリケーションのプログラミング、第 5 回: C/C++ での SPU のプログラミング

言語拡張を使用してアプリケーションを強化する

連載「Cell BE プロセッサーでのハイパフォーマンス・アプリケーションのプログラミング」の第 5 回では、これまでに学んだ相乗演算処理装置 (SPU) の知識を、Cell BE (Cell Broadband Engine™) プロセッサーの C/C++ プログラミングに応用します。ベクトル拡張機能を使用する方法、コンパイラーに分岐を予測するよう指示する方法、そして DMA 転送を C/C++ で実行する方法を学んでください。

Jonathan Bartlett (johnnyb@eskimo.com), Director of Technology, New Medio

Jonathan BartlettはLinuxアセンブリー言語を使ったプログラミングの入門書Programming from the Ground Upの著者です。New Media Worxでの主席開発者であり、顧客向けにWebアプリケーションや、ビデオ、キヨスク、デスクトップなどのアプリケーションを開発しています。連絡先はjohnnyb@eskimo.comです。



2007年 3月 20日

SPU についての今までの説明では、このプロセッサーを十分理解してもらえるように SPU のアセンブリー言語に焦点を絞ってきました。今回は焦点を C/C++ に移し、コンパイラーに大量の作業を実行させる方法を説明します。SPU の C/C++ 言語拡張を使用するには、コードの先頭に spu_intrinsics.h というヘッダー・ファイルを組み込む必要があります。

SPU でのベクトルの基本

ベクトル・プロセッサーとそれ以外のプロセッサーとの大きな違いは、ベクトル・プロセッサーには大規模なレジスタがあり、同じデータ型の複数の値 (要素) を格納して 1 回の操作でこれらの値を同時に処理できるという点です。ベクトル・プロセッサーでは、レジスタは 1 つのユニットとして扱われることも、複数のユニットとして扱われることもあります。C/C++ には、この概念を表すために vector というキーワードが追加されています。このキーワードはプリミティブ・データ型を取り、レジスタ全体で使用します。一例として、vector unsigned int myvec; は要素のロード、処理、そして格納をまとめて行う 4 つの整数のベクトルを作成します。myvec は 4 つの整数すべてを同時に参照する変数で、signed/unsigned は非浮動小数点宣言に必要なキーワードです。ベクトルの定数を作成するには、ベクトルの型を括弧で囲み、その後に中括弧で囲んだベクトルのコンテンツを続けます。例えば myvec というベクトルに値を割り当てるには、以下のようにします。

 vector unsigned int myvec = (vector unsigned int){1, 2, 3, 4};

このように直接割り当てる方法とは別に、スカラー・データとベクトル・データ間でのやり取りに使用する主要なプリミティブとして、spu_insert、spu_extract、spu_promote、および spu_splats の 4 つがあります。spu_insert はスカラー値をベクトルの特定の要素に組み込むためのプリミティブです。例えば spu_insert(5, myvec, 0) とある場合、myvec をコピーして新しいベクトルを作成し、そのベクトルの最初の要素 (要素 0) を 5 に設定したものを返します。spu_extract はベクトルから特定の要素を抽出してスカラーとして返します。spu_extract(myvec, 0) とある場合、myvec の最初の要素がスカラーとして返されることになります。spu_promote は値をベクトルに変換しますが、定義する要素は 1 つだけです。ベクトルの型はプロモートされた値の型に依存します。つまり、spu_promote((unsigned int)5, 1) とある場合は、2 番目の要素 (要素 1) に 5 が含まれる unsigned int のべクトルが作成され、残りの要素は未定義のままになります。spu_splats は spu_promote のように機能しますが、ベクトルのすべての要素に値をコピーするという点で異なります。そのため、spu_splats((unsigned int)5) ではそれぞれの要素が 5 の値を持つ unsigned int のベクトルが作成されます。

ベクトルは短い配列として考えられがちですが、実際にはいくつかの点で配列とは異なる作用をもたらします。まず、配列は参照として操作されますが、ベクトルは基本的にスカラー値として扱われます。例えば、spu_insert はベクトルの要素を変更する代わりに、要素が挿入されたまったく新しいベクトルのコピーを返します。つまり、ベクトルはその結果が値となる表現であり、値自体を変更するものではありません。myvar + 1 が myvar を変更せずに新しい値を返すのと同様に、spu_insert(1, myvec, 0) は myvec を変更せずに、myvec と同じで、ただし最初の要素が 1 に設定された新しいベクトル値を返します。

以下は、この概念を使った簡単なプログラムです (vec_test.c)。

リスト 1. SPU C/C++ 言語拡張を導入したプログラム
#include <spu_intrinsics.h>

void print_vector(char *var, vector unsigned int val) {
	printf("Vector %s is: {%d, %d, %d, %d}\n", var, spu_extract(val, 0),
	 spu_extract(val, 1), spu_extract(val, 2), spu_extract(val, 3));
}

int main() {
	/* Create four vectors */
	vector unsigned int a = (vector unsigned int){1, 2, 3, 4};
	vector unsigned int b;
	vector unsigned int c;
	vector unsigned int d;

	/* b is identical to a, but the last element is changed to 9 */
	b = spu_insert(9, a, 3);

	/* c has all four values set to 20 */
	c = spu_splats((unsigned int) 20);

	/* d has the second value set to to 5, and the others are garbage */
	/* (in this case they will all be set to 5, but that should not be relied upon) */
	d = spu_promote((unsigned int)5, 1);

	/* Show Results */
	print_vector("a", a);
	print_vector("b", b);
	print_vector("c", c);
	print_vector("d", d);

	return 0;
}

このプログラムを elfspe でコンパイルして実行するには、以下のように入力します。

spu-gcc vec_test.c -o vec_test
./vec_test

ベクトル組み込み関数

C/C++ 言語拡張に含まれているデータ型と組み込み関数 (intrinsic) によって、プログラマーはほとんどすべての SPU のアセンブリー言語命令にアクセスできますが、その多くの組み込み関数は、同じような複数の命令を 1 つの組み込み関数に合体させて SPU のアセンブリー言語を大幅に簡易化するものです。オペランドのタイプが異なるだけの命令 (加算用の a、ai、ah、ahi、fa、dfa など) は、オペランドのタイプに応じて適切な命令を選択する単一の C/C++組み込み関数で表現されます。加算の場合、spu_add に 2 つの vector unsigned int をパラメーターとして指定すると、a (32 ビット加算) 命令が生成されます。一方、2 つの vector float をパラメーターとして指定すると、fa (浮動小数点加算) 命令が生成されます。ここで注意しなればならないのは、組み込み関数には通常、その対応するアセンブリー言語命令と同じ制約が課せられるということです。ただし、即値が該当する即値モード命令には大きすぎるような場合は、コンパイラーが即値をベクトルにプロモートして対応するベクトル/ベクトル演算を行います。例えば、spu_add(myvec, 2) は ai (即値加算) 命令を生成しますが、spu_add(myvec, 2000) の場合は il を使って独自のベクトルに 2000 をロードしてから、a (加算) 命令を実行します。

組み込み関数に含まれるオペランドの順序は基本的にアセンブリー言語命令での順序と同じですが、組み込み関数の最初のオペランド (アセンブリー言語では宛先レジスタを保有) は C/C++ 言語で指定されません。代わりに、このオペランドは該当する関数の戻り値として使用されます。アセンブリー言語のコードには、そのコードを生成するコンパイラーによって適切なオペランドが指定されます

よく使用される SPU の組み込み関数には以下のものがあります (これらのほとんどは多相型なので、型は記載しません)。

  • spu_add(val1, val2)
    val1 の各要素を val2 の対応する要素に加算します。val2 がベクトル値でない場合は、val2 の値が val1 の各要素に加算されます。
  • spu_sub(val1, val2)
    val2 の各要素を val1 の対応する要素から減算します。val1 がベクトル値でない場合は、val1 がベクトルとして複製され、そこから val2 が減算されます。
  • spu_mul(val1, val2)
    乗算命令は演算方法がかなり異なるため、SPU の組み込み関数は他の演算と同じほどには乗算命令を合体させません。spu_mul は浮動小数点の乗算 (単精度および倍精度) を処理する命令です。作成されるベクトルでは、各要素が val1 と val2 の対応する要素を掛け合わせた結果となります。
  • spu_and(val1, val2)、spu_or(val1, val2)、spu_not(val)、spu_xor(val1, val2)、spu_nor(val1, val2)、spu_nand(val1, val2)、spu_eqv(val1, val2)
    ブール演算はビットごとに行われるため、ブール演算が受け取るオペランドの型が関係してくるのは、戻り値の型を決定するときだけです。spu_eqv はビット単位の等価演算で、要素単位の等価演算ではありません。
  • spu_rl(val, count)、spu_sl(val, count)
    spu_rl は、count の対応する要素に指定されたビット数の分だけ val の各要素を左ローテートします。要素の左端までローテートすると次は右端からローテートインします。count がスカラー値の場合、この値は val のすべての要素のカウントとして使用されます。spu_sl も同じように機能しますが、ローテートではなくシフトを実行します。
  • spu_rlmask(val, count)、spu_rlmaska、spu_rlmaskqw(val, count)、spu_rlmaskqwbyte(val, count)
    これらの演算には非常に混乱しやすい名前が付いています。名前は「rotate left and mask (左ローテートしてマスクする)」となってはいるものの、実際に行われるのは右シフトです (これらの演算は左シフトとマスクの組み合わせで実装されていますが、プログラミング・インターフェースは右シフトを対象としているためです)。spu_rlmask と spu_rlmaska は、val の各要素を count の対応する要素に指定されたビット数 (count がスカラーの場合は count の値) の分だけ右へシフトします。spu_rlmaska はビットをシフトする際に符号ビットを複製します。spu_rlmaskqw は一度にクワッドワード全体を処理しますが、処理するのは最大 7 ビットです (count に対してモジュロ演算 (mod) を実行し、適切な範囲内になるようにします)。spu_rlmaskqwbyte も同じように機能しますが、count に入るのはビット数ではなくバイト数で mod 8 ではなく mod 16 を実行した値が入ります。
  • spu_cmpgt(val1, val2)、spu_cmpeq(val1, val2)
    この 2 つの命令は、それぞれに含まれる 2 つのオペランドを要素ごとに比較します。作成されるベクトルの対応する要素には、すべてのビットが 1 (比較結果が真の場合) および、すべてのビットが 0 (比較結果が偽の場合) として比較結果が格納されます。spu_cmpgt はより大きいかどうかを比較し、spu_cmpeq は等しいかどうかを比較します。
  • spu_sel(val1, val2, conditional)
    selb アセンブリー言語命令に対応します。この命令自体はビット単位なので、すべての型で同じ基本命令が使用されますが、組み込み関数の演算ではオペランドと同じ型の値を返します。アセンブリー言語での場合と同じく、spu_sel は conditional 内のビットごとに調べます。結果に含まれるそれぞれのビットは、ビットがゼロであれば val1 内の対応するビットから選択され、ゼロでない場合は val2 内の対応するビットから選択されます。
  • spu_shuffle(val1, val2, pattern)
    これは興味深い命令で、val1 と val2 を構成するバイトを pattern に指定されたパターンに従って再配置するためのものです。この命令は pattern 内の各バイトを順に調べ、そのバイトがビット 0b10 で始まっている場合は、結果を格納する先の対応バイトを 0x00 に設定し、ビット 0b110 で始まっている場合は 0xff に、ビット 0b111 で始まっている場合は 0x80 に設定します。そして最も重要なのは、これらの条件がいずれも真でない場合で、その場合は pattern のバイトの下位 5 ビットを使用して、現在対象となっているバイトに格納する値に、val1 または val2 内のどのバイトを使うかが決定されます。つまり 2 つの値 (val1 と val2) を連結し、連結してできた値の各バイトを指し示すインデックスとして、pattern のバイトの下位 5 ビットの値を使用するのです。この命令は、要素をベクトルに挿入するとともにテーブル・ルックアップを高速に行うために使用されます。

接頭部に spu_ を持つ命令は、いずれもオペランドの型に応じて最適な命令を見つけようとしますが、すべての命令がすべてのベクトル型をサポートするわけではありません (ベクトルを処理ためのアセンブリー言語命令がどれだけ用意されているかに基づきます)。さらに、コンパイラーに命令を選択させるのではなく、特定の命令が必要な場合には、特定の組み込み関数を使用すれば、分岐以外のほとんどすべての命令を実行できます。特定の組み込み関数はいずれも、si_assemblyinstructionname という形をとります (ここで、assemblyinstructionname は SPUアセンブリー言語仕様に定義されているアセンブリー言語命令の名前です)。つまり、si_a(a, b) とあれば、命令 a が加算オペランドとして使用されることになります。特定の組み込み関数に対するすべてのオペランドは qword という特殊な型にキャストされます。これは、基本的には不明レジスタ値の型です。特定の組み込み関数からの戻り値も同じく qword なので、必要に応じた任意のベクトル型にこの値をキャストすることができるというわけです。


組み込み関数の使用方法

ここで、アセンブリー言語ではなく、C/C++ を使用して大文字変換関数を実行する方法を見てみましょう。単一のベクトルを変換する際の基本的なステップは以下のとおりです。

  1. 大文字変換によってすべての値を変換します。
  2. すべてのバイトでベクトル比較を行って、バイトが「a」から「z」の範囲内であるかどうかを調べます。
  3. 比較を使用して、選択命令で変換する値と変換しない値とを選択します。

アセンブリー言語のバージョンでは、上記のステップに加え、命令をスケジューリングしやすいように複数の変換を同時に実行しましたが、C/C++ ではインライン関数を複数回呼び出して、コンパイラーに適切にスケジューリングさせることができます。だからといって、あなたの命令スケジューリングの知識が使いものにならないとい言っているわけではありません。むしろ命令スケジューリングがどのように機能するかを知っているからこそ、コンパイラーに操作しやすい素材を与えられるのです。命令スケジューリングによってコードが改善され、ループのアンロールを命令スケジューリングに役立てられるという知識がなかったとしたら、コンパイラーがコードを最適化できるよう手助けすることはできません。

以下は、convert_buffer_to_upper 関数の C/C++ バージョンです (連載のこれまでの記事に記載したファイルと同じディレクトリーにある convert_buffer_c.c です。これらのファイルは、完全なアプリケーションとしてコンパイルするために必要となります)。

リスト 2. C/C++ での大文字変換
#include <spu_intrinsics.h>

unsigned char conversion_value = 'a' - 'A';

inline vec_uchar16 convert_vec_to_upper(vec_uchar16 values) {
	/* Process all characters */
	vec_uchar16 processed_values = spu_absd(values, spu_splats(conversion_value));
	/* Check to see which ones need processing (those between 'a' and 'z')*/
	vec_uchar16 should_be_processed = spu_xor(spu_cmpgt(values, 'a'-1), 
	spu_cmpgt(values, 'z'));
	/* Use should_be_processed to select between the original and processed values */
	return spu_sel(values, processed_values, should_be_processed);
}

void convert_buffer_to_upper(vec_uchar16 *buffer, int buffer_size) {
	/* Find end of buffer (must be casted first because size is bytes) */
	vec_uchar16 *buffer_end = (vec_uchar16 *)((char *)buffer + buffer_size);

	while(__builtin_expect(buffer < buffer_end, 1)) {
		*buffer = convert_vec_to_upper(*buffer);
		buffer++;
		*buffer = convert_vec_to_upper(*buffer);
		buffer++;
		*buffer = convert_vec_to_upper(*buffer);
		buffer++;
		*buffer = convert_vec_to_upper(*buffer);
		buffer++;
	}
}

上記をコンパイルして実行するには、以下のように入力します。

spu-gcc convert_buffer_c.c convert_driver.s dma_utils.s -o spe_convert
embedspu -m64 convert_to_upper_handle spe_convert spe_convert_csf.o
gcc -m64 spe_convert_csf.o ppu_dma_main.c -lspe -o dma_convert
./dma_convert

お気付きだとは思いますが、このプログラムで使用しているベクトル型名の表記は、以前使った表記とはわずかに異なります。SPU 組み込み関数の資料 (「参考文献」を参照) では、vec_ で始まる簡易ベクトル型名を定義しています。vec_ に続く文字は、整数型の場合は、符号付き、符号なしで、それぞれ s、u です。その後に、使用している基本型の名前が続きます (char、int、float など)。名前の末尾は、ベクトルに含まれる該当する型の要素数です。例えば、vec_uchar16 は unsigned char の 16 要素ベクトル、vec_float4 は float の 4 要素ベクトルとなります。この表記により、必要な入力が大幅に簡易化されます。

このプログラムは buffer_end を計算する際にキャストを実行しましたが、size はバイト単位です。そこで、ポインターを char * に変換して、サイズを加算するとクワッドワードではなくバイト単位でポインターが移動するようにしました。ベクトル・ポインターの指す値は 16 バイト長なので、ベクトル・ポインターは 16 バイトの増分値で前進しますが、char ポインターは 1 バイトずつ前進します。これは、buffer++が機能していることを表しています (ベクトル長 (16 バイト) ごとに増分しています)。

C/C++ バージョンでは、コンパイラーに分岐ヒントを生成させるための __builtin_expect も興味深い仕組みです。C/C++ では分岐ヒントを直接指定することはできません。分岐アドレスもターゲット・アドレスもないためです。そのため、コンパイラーに分岐ヒントを指示し、適切な分岐ヒントを生成できるようにします。__builtin_expect(buffer < buffer_end, 1) は最初の引数 buffer < buffer_end を基に分岐コードを生成しますが、分岐ヒントは 2 番目の引数である 1 を基に作成します。このようにして、buffer < buffer_end の値に 1 を予測するヒントを生成するようコンパイラーに指示します。

SPU プログラミングには現在 2 つのコンパイラーが使用できますが、ご想像のとおり、それぞれのコンパイラーが得意とする分野は異なります。例えば GCC には、convert_vec_to_upper 呼び出しの合間に命令を割り込ませ、命令の待ち時間を最小限にするという素晴らしい技があります。ただし上記の特定のプログラムでは、__builtin_expect はほとんど役に立ちません。一方、IBM XLC コンパイラーはその逆です。このコンパイラーは convert_vec_to_upper の呼び出し間に命令を割り込ませることはしませんが、分岐ヒントが最大限の効果を発揮するようにループを構成します。そのため、分岐ヒントは指定されていませんでしたが、コンパイラーは分岐ヒントを予測することができました。当然ながら、どちらのコンパイラーの機能も以前の記事でハンド・コーディングしたアセンブリー言語バージョンには及びませんが、上記のプログラムでは XLC の性能が GCC を上回っています。最適化フラグを一切使わずにコードをコンパイルすると処理に約 5 倍の時間がかかるので、必ず -O2 または -O3 を使用してコンパイルしてください。


複合組み込み関数と MFC プログラミング

複合組み込み関数とは、複数の命令にコンパイルする組み込み関数のことです。複合組み込み関数は SPE での共通使用パターンをカプセル化してプログラミングを簡易化します。複合組み込み関数のなかで最も重要なのは、spu_mfcdma64 と spu_mfcstat. の 2 つです。spu_mfcdma64 は以前の記事で作成して使用した dma_transfer 関数とほとんど同じで、唯一の違いは実効アドレスが上位 32 ビットと下位 32 ビットの 2 つのパラメーターに分割されるという点です (dma_transfer では実効アドレスに 1 つの 64 ビット・パラメーターを使用しました)。

spu_mfcdma64 は、以下の 6 つのパラメーターを使用します。

  1. 転送用ローカル・ストアのアドレス
  2. 実効アドレスの上位 32 ビット
  3. 実効アドレスの下位 32 ビット
  4. 転送サイズ
  5. 転送に指定する「タグ」
  6. 実行する DMA コマンド

たいていの場合、実効アドレスは単一の 64 ビットの値です。これを分割するには、mfc_ea2h と mfc_ea2l をそれぞれ使用して上位ビットと下位ビットを抽出します。タグはプログラマーが指定する 0 から 31 の数値で、ステータスの問い合わせやシーケンシャルな操作のための転送あるいは転送グループを識別するために使用します。DMA コマンドはある範囲の値をとります (ここに掲載されていない情報については、「参考文献」を参照してください)。DMA 転送が SPU ローカル・ストアからシステム・メモリーに転送する場合は PUT と呼ばれ、システム・メモリーから SPU ローカル・ストアに転送する場合は GET と呼ばれます。これらの DMA コマンド名の接頭部は、それぞれ MFC_PUT または MFC_GET となります。MFC コマンドは個別に、またはリストで実行されます。DMA コマンドがリスト・コマンドであれば、DMA コマンド名に L が追加されます (DMA リスト・コマンドについての詳細は、「参考文献」を参照してください)。DMA コマンドには、コマンドに適用する特定の同期レベルを設定することも可能です。その場合、バリア同期には B を、フェンス同期には F を追加します。同期を行わない場合は、何も追加する必要はありません。そして最後に、すべての DMA コマンド名には接尾辞として _CMD が追加されます。以上のことから、フェンス同期を使用してローカル・ストアからシステム・メモリーへ単一の転送を行う場合、コマンド名は MFC_PUTF_CMD となります。

デフォルトでは、SPE の MFC での DMA コマンドにはまったく順序が付けられていないため、MFC はコマンドを任意の順序で処理できます。ただし、タグ、フェンス、バリアを使用すると、MFC DMA 転送の順序には制約が設けられます。フェンスによる制約では、同じタグを使用した前のコマンドがすべて完了してから DMA 転送が実行されることになります。バリアによる制約では、同じタグを使用した前のコマンドがすべて完了し (フェンスと同様)、なおかつ同じタグを使用した後続のコマンドが実行される前に DMA 転送が実行されることになります。

以下に spu_mfcdma64 の例を挙げます。

リスト 3. spu_mfcdma64 の使用方法
typedef unsigned long long uint64;
typedef unsigned long uint32;
uint64 ea1, ea2, ea3, ea4, ea5; /* assume each of these have sensible values */
void *ls1, *ls2, *ls3, *ls4; /* assume each of these have sensible values */
uint32 sz1, sz2, sz3, sz4; /* assume each of these have sensible values */
int tag = 3; /* Arbitrary value, but needs to be the same for all 
synchronized transfers */

/* Transfer 1: System Storage -> Local Store, no ordering specified */
spu_mfcdma64(ls1, mfc_ea2h(ea1), mfc_ea2l(ea1), sz1, tag, MFC_GET_CMD);

/* Transfer 2: Local Storage -> System Storage, must perform after previous transfers */
spu_mfcdma64(ls2, mfc_ea2h(ea2), mfc_ea2l(ea2), sz2, tag, MFC_PUTF_CMD);

/* Transfer 3: Local Storage -> System Storage, no ordering specified */
spu_mfcdma64(ls3, mfc_ea2h(ea3), mfc_ea2l(ea3), sz3, tag, MFC_PUT_CMD);

/* Transfer 4: Local Storage -> System Storage, must be synchronized */
spu_mfcdma64(ls4, mfc_ea2h(ea4), mfc_ea2l(ea4), sz4, tag, MFC_PUTB_CMD);

/* Transfer 5: System Storage -> Local Storage, no ordering specified */
spu_mfcdma64(ls4, mfc_ea2h(ea5), mfc_ea2l(ea5), sz4, tag, MFC_GET_CMD);

上記の例で考えられる順序は 1 つだけではありません。以下のすべての順序が可能です。

  • 1、2、3、4、5
  • 3、1、2、4、5
  • 1、3、2、4、5

Transfer 2 にはフェンスだけが使用されていること、そして Transfer 3 には順序がまったく指定されていないことから、Transfer 3 はバリア (Transfer 4) 以前のどの時点でも実行できます。最初の 3 つの転送で唯一の要件となっているのは、Transfer 2 を Transfer 1 の後に実行するということだけです。一方、Transfer 4 前後の転送は完全に同期する必要があります。

Transfers 4 と Transfers 5 をよく見てください。これは、「保存してリロード」の注目すべき便利なイディオムです。システム・メモリーのデータを一度に少しずつ処理してローカル・ストアに転送し、再びシステム・メモリーに保存している場合、保存とロードを一度にキューに入れ、フェンスまたはバリアを使って順序を付けることができます。これにより、転送ロジックのすべてが MFC に入れられるので、バッファーが新しいデータを待っている間、プログラムは他の計算タスクを自由に実行できるというわけです。二重バッファーの手法を話題にした次回の記事では、この方法を利用します。

spu_mfcdma64 は非常に便利なツールですが、アドレスを変換するためにいちいち mfc_ea2h と mfc_ea2l を使わなければならい場合などは多少面倒です。そのため仕様では、必要な繰り返し入力の作業を少なくするためのユーティリティー関数もいくつか提供しています。mfc_ クラスのすべての関数は、実効アドレスが単一の 64 ビット・パラメーターである以外は、使用するパラメーターは spu_mfcdma64 関数と同じで、関数名には DMA コマンドがエンコードされます。転送クラス ID と置換クラス ID という 2 つの追加パラメーターも使用されますが、リアルタイムのアプリケーションでなければ、この 2 つのパラメーターの両方をゼロに設定しても問題ありません (この 2 つのフィールドについての詳細は、「参考文献」を参照してください)。そこで、上記の Transfer 2 は以下のように書き直すことができます。

 mfc_putf(ls2, ea2, sz2, tag, 0, 0);

タグはデータ転送を同期するのに便利なだけでなく、転送のステータスを確認するのにも役立ちます。SPE には、ステータスの確認に現在使用しているタグを指定するためのタグ・マスク・チャネル、ステータス・リクエストを実行するためのチャネル、そしてチャネル・ステータスを読み込むためのもう 1 つのチャネルがあります。これらの演算はかなり単純なものですが、仕様では演算を実行するための特殊な方法も指定しています。そのうちの mfc_write_tag_mask は 32 ビットの整数を取り、この整数をチャネル・マスクとして使用してその後のステータスの更新を行います。マスクでは、ステータスを確認する各タグのビット位置を 1 に設定してください。つまり、channel 2 と channel 4 のステータスを確認するには mfc_write_tag_mask(20) を使うことになります。あるいはもっと読みやすいように、mfc_write_tag_mask(1<<2 | 1<<4); とすることもできます。実際にステータスの更新を実行するには、ステータス・コマンドを選んで spu_mfcstat(unsigned int command) を使って送信します。これらのコマンドには以下があります。

  • MFC_TAG_UPDATE_IMMEDIATE
    このコマンドを実行すると、SPE は DMA チャネルのステータスを直ちに返します。チャネル・マスクに指定された各チャネルは、該当タグを持つキューにコマンドが残っていない場合 (つまり、前にアクティブになっていた可能性のあるすべての操作が完了している状態) は 1 に設定され、キューにコマンドが残っている場合は 0 に設定されます。
  • MFC_TAG_UPDATE_ANY
    このコマンドを実行すると、SPE はタグ・マスクに指定されたタグのうち少なくとも 1 つのタグの実行待ちコマンドがなくなるまで待機し、それからタグ・マスクに指定された DMA チャネルのステータスを返します。
  • MFC_TAG_UPDATE_ALL
    このコマンドを実行すると、タグ・マスクに指定されたすべてのタグの実行待ちコマンドがなくなるまで SPE は待機し、それからリターンします。戻り値は 0 になります。

上記の制約を使用するには、spu_mfcio.h をインクルードする必要があります。

spu_mfcstat では、DMA 要求のステータスの確認と、DMA 要求の待機の両方を実行できます。MFC_TAG_UPDATE_ANY を使用すれば、複数の DMA 要求を実行し、MFC が最適と判断する順序で MFC に要求を処理させ、その処理順にコードを応答させることができます。


サンプル MFC プログラム

それでは、MFC の複合組み込み関数の知識を大文字変換プログラムに適用してみます。この記事ではすでに主要な変換関数を C 言語で書き直しているので、今度は主要なループを C 言語で書き直します。この新しいコードは、以下のように極めて単純なものになります (convert_driver_c.c)。

リスト 4. 大文字変換の MFC 転送コード
 #include <spu_intrinsics.h>
#include <spu_mfcio.h>
typedef unsigned long long uint64;

#define CONVERSION_BUFFER_SIZE 16384
#define DMA_TAG 0

void convert_buffer_to_upper(char *conversion_buffer, int current_transfer_size);

char conversion_buffer[CONVERSION_BUFFER_SIZE];

typedef struct {
	int length __attribute__((aligned(16)));
	uint64 data __attribute__((aligned(16)));
} conversion_structure;

int main(uint64 spe_id, uint64 conversion_info_ea) {
	conversion_structure conversion_info; /* Information about the data from the PPE */

	/* We are only using one tag in this program */
	mfc_write_tag_mask(1<<DMA_TAG);

	/* Grab the conversion information */
	mfc_get(&conversion_info, conversion_info_ea, sizeof(conversion_info), DMA_TAG, 0, 0);
	spu_mfcstat(MFC_TAG_UPDATE_ALL); /* Wait for Completion */

	/* Get the actual data */
	mfc_get(conversion_buffer, conversion_info.data, conversion_info.length, DMA_TAG, 0, 0);
	spu_mfcstat(MFC_TAG_UPDATE_ALL);

	/* Perform the conversion */
	convert_buffer_to_upper(conversion_buffer, conversion_info.length);

	/* Put the data back into system storage */
	mfc_put(conversion_buffer, conversion_info.data, conversion_info.length, DMA_TAG, 0, 0);
	spu_mfcstat(MFC_TAG_UPDATE_ALL); /* Wait for Completion */
}

上記をコンパイルして実行するには、以下のように入力します。

spu-gcc convert_buffer_c.c convert_driver_c.c -o spe_convert
embedspu -m64 convert_to_upper_handle spe_convert spe_convert_csf.o
gcc -m64 spe_convert_csf.o ppu_dma_main.c -lspe -o dma_convert
./dma_convert

C 言語でのこの実装はオリジナルのコードと同じ基本構造に従っていますが、人間にとっての読みやすさはこちらのほうが上で、結果として改訂も拡張もしやすくなっています。オリジナルのコードには、例えば DMA 転送のサイズに制限されてしまうという問題があります。この制限をなくすとしたら、すべてのものをループにラップし、ストリング全体が処理されるまでデータを一部ずつ移動させていけばいいだけのことです。このように改訂したコードは以下のようになります。

リスト 5. MFC 転送コードでのループ
#include <spu_intrinsics.h>
#include <spu_mfcio.h> /* constant declarations for the MFC */
typedef unsigned long long uint64;
typedef unsigned int uint32;

/* Renamed CONVERSION_BUFFER_SIZE to MAX_TRANSFER_SIZE because it is now 
primarily used to limit the size of DMA transfers */
#define MAX_TRANSFER_SIZE 16384

void convert_buffer_to_upper(char *conversion_buffer, int current_transfer_size);

char conversion_buffer[MAX_TRANSFER_SIZE];

typedef struct {
	uint32 length __attribute__((aligned(16)));
	uint64 data __attribute__((aligned(16)));
} conversion_structure;

int main(uint64 spe_id, uint64 conversion_info_ea) {
	conversion_structure conversion_info; /* Information about the data from the PPE */

	/* New variables to keep track of where we are in the data */
	uint32 remaining_data; /* How much data is left in the whole string */
	uint64 current_ea_pointer; /* Where we are in system memory */
	uint32 current_transfer_size; /* How big the current transfer is (may be smaller 
	than MAX_TRANSFER_SIZE) */

	/* We are only using one tag in this program */
	mfc_write_tag_mask(1<<0);

	/* Grab the conversion information */
	mfc_get(&conversion_info, conversion_info_ea, sizeof(conversion_info), 0, 0, 0);
	spu_mfcstat(MFC_TAG_UPDATE_ALL); /* Wait for Completion */

	/* Setup the loop */
	remaining_data = conversion_info.length;
	current_ea_pointer = conversion_info.data;

	while(remaining_data > 0) {
		/* Determine how much data is left to transfer */
		if(remaining_data < MAX_TRANSFER_SIZE)
			current_transfer_size = remaining_data;
		else
			current_transfer_size = MAX_TRANSFER_SIZE;

		/* Get the actual data */
		mfc_getb(conversion_buffer, current_ea_pointer, current_transfer_size, 0, 0, 0);
		spu_mfcstat(MFC_TAG_UPDATE_ALL);

		/* Perform the conversion */
		convert_buffer_to_upper(conversion_buffer, current_transfer_size);

		/* Put the data back into system storage */
		mfc_putb(conversion_buffer, current_ea_pointer, current_transfer_size, 0, 0, 0);

		/* Advance to the next segment of data */
		remaining_data -= current_transfer_size;
		current_ea_pointer += current_transfer_size;
	}
	spu_mfcstat(MFC_TAG_UPDATE_ALL); /* Wait for Completion */
}

上記をコンパイルして実行するコマンドは、前のサンプルで使ったコマンドと同じです。

spu-gcc convert_buffer_c.c convert_driver_c.c -o spe_convert
embedspu -m64 convert_to_upper_handle spe_convert spe_convert_csf.o
gcc -m64 spe_convert_csf.o ppu_dma_main.c -lspe -o dma_convert
./dma_convert

ここでは、処理可能なデータ・サイズを 4 ギガバイトに拡張しましたが、データ・サイズ変数を 32 ビットではなく 64 ビットにすれば、簡単にそれ以上に拡張することもできます。注目すべき点は、GET を再実行する前に、MFC に PUT が完了するまで待機させるためのコーディングは明示的に行っていないことです。その理由は、転送にはバリアを使用していて、バリアには同じ DMA タグを使っているためです。これによって、転送は MFC 自体によって逐次化されるため、MFC は常に、現行の変換がシステム・ストレージに PUT 処理されてからバッファーへの追加データを GET 処理します。コードの最後で、完了するまで待機するようにすることだけ忘れないでください (ループの外にある spu_mfcstat に注意)。そうでないと、データの最後の部分が完全に転送される前にプログラムで使用されてしまう可能性があります。

C 言語でのプログラミングでもう 1 つ注意しなければならない点として、必ず関数プロトタイプを宣言してください。32 ビットと 64 ビットの値を誤って混在させてしまうことは珍しくありません。PPE では、そのような値は単に切り捨てられるか拡張されるのでそれ程問題になりませんが、SPE となると話は別です。プロトタイプが誤っていると、32 ビットと 64 ビットの値のプリファード・スロットが補正され、この 2 つの値の変換を明示的に処理しなければならなくなります。


C 言語での SPE プログラミングに役立つヒント

SPE アプリケーションを C 言語でビルドする際には、以下のヒントを頭に入れておいてください。

  • ベクトルは異なるベクトル型へのキャスト、そしてベクトル型と特殊な quad 型との間でのキャストが可能ですが、いずれのキャストでもデータ変換は行われません。型間での変換が必要な場合は、該当する SPU の組み込み関数を使用してください。
  • ベクトル・ポインターと非ベクトル・ポインターとの間では、どちらの方向でもキャストが可能ですが、スカラー・ポインターをベクトル・ポインターに変換する場合、ポインターが確実にクワッドワードにアラインされるようにするのはプログラマーの責任です。
  • 宣言済みベクトルを割り当てると、常にクワッドワードにアラインされます。
  • 16 バイト以上の DMA 転送は、16 バイトの倍数であること、そして SPE と PPE の両方で 16 バイト境界にアラインされることが必要です。これより小さいサイズの転送は 2 のべき乗なので、当然アラインされます。最適な転送は、128 バイト境界上にある 128 バイトの倍数です。
  • PPE でデータがアラインされるかどうか不安な場合は、memalign または posix_memalign を使ってヒープからアラインされたポインターを割り当て、memcpy やそれと同等のものを使ってデータをアラインされた領域に移します。
  • 必ず -Wall オプションを使ってコンパイルし、特にプロトタイプ宣言がないことを示すメッセージには注意を払ってください。誤った暗黙のプロトタイプ宣言は (特に 32 ビット型と 64 ビット型の間では)、異常なエラーの状況につながることがあります。
  • 実効アドレスは必ず、PPE と SPE の両方で unsigned long long として保存してください。こうすると、PPE コードのコンパイル対象が 32 ビットでの実行または 64 ビットでの実行のどちらであっても、実効アドレスは SPE と PPE で同じように扱われます。
  • SPE では整数の乗算 (特に 32 ビットの乗算) は使わないようにしてください。整数の乗算には、5 つの命令が必要になるためです。乗算しなければならない場合は、その前に unsigned short にキャストしてください。
  • SPE でのスカラー・コードでは、スカラー値をベクトルとベクトル・ポインターとして宣言すると (ベクトルとして使用していない場合でも) アラインされていないロードとストアを処理する必要がなくなるため、コードの処理時間を短縮できます。
  • SPE では float と double の実装方法が異なり、丸め方も異なることに注意してください。特に float は C99 標準から外れています。これについては次回の記事で詳しく説明します。

まとめ

C 言語で使用可能な組み込み関数により、プログラマーは C 言語とアセンブリー言語の知識を併せて最大限に活用できます。プログラムは SPU の組み込み関数を使用して高級なコードと低級なコードを自由に切り替えられますが、同時にそれはすべて、C 言語が規定する枠組みの中で行われます。

次回の記事では、この知識を現実世界の数値アプリケーションに応用します。

参考文献

学ぶために

製品や技術を入手するために

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=245794
ArticleTitle=Cell BE プロセッサーでのハイパフォーマンス・アプリケーションのプログラミング、第 5 回: C/C++ での SPU のプログラミング
publish-date=03202007