本文へジャンプ

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


お客様が developerWorks に初めてサインインすると、プロフィールが作成されます。プロフィールで選択した情報は公開されますが、いつでもその情報を編集できます。お客様の姓名(非表示設定にしていない限り)とディスプレイ・ネームは、投稿するコンテンツと一緒に表示されます。

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

  • 閉じる [x]

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

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

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


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

  • 閉じる [x]

Cell BE プロセッサーでのハイパフォーマンス・アプリケーションのプログラミング、第 3 回: 相乗演算処理装置の紹介

Sony PLAYSTATION 3 の相乗演算処理要素をプログラミングする

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です。

概要: この連載では、Cell BE (Cell Broadband Engine™) プロセッサーの相乗演算処理要素 (SPE) の詳細と最低限の基本レベルでのその動作方法を説明しています。今回の記事で取り上げるのは、ストレージ・アラインメントの問題と SPE の通信機能です。

日付:  2007年 2月 22日
レベル:  中級 この記事の原文:  英語
アクティビティー: 3037 ビュー
お気軽にご意見・ご感想をお寄せください: 


アラインされていないデータのロードと格納

相乗演算処理装置 (SPU) はスカラー演算ではなくベクトル演算を重点にしているため、SPU が一度にロードおよび格納できるのは、16 バイト境界にアラインされたローカル・ストアの位置にある 16 バイトだけです。つまり、あるワードを例えばメモリー位置 12 から単純にロードすることはできないので、まずはメモリー位置 0 からクワッドワードをロードし、それから必要な値がプリファード・スロットに含まれるようにビットをシフトしなければなりません。このように、元のクワッドワードをロードし、該当する値をクワッドワードの正しい位置に挿入してから結果を再び格納する必要があるため、通常はすべてのデータを 16 バイトでアラインすることをお勧めします。16 バイト境界をまたがる値のロードとなるとさらに厄介で、この場合、値を実際に 2 つのレジスタにロードし、シフトしてからマスクをかけて組み合わせることになります。このような値を格納するのはますます厄介になるので、16 バイト境界をまたがる値は使用しないことが最善策です。

16 バイト境界にアラインされていないデータを使用することはできますが、この記事で取り上げるロードおよび格納の手法では、データを必ずアラインして 16 バイト境界をまたがらないようにする必要があります。つまり、ワードは 4 バイトにアラインし、ハーフワードは 2 バイトにアラインします。バイトはアラインする必要はありません。

アラインされていないデータをロードするには、データのサイズに応じて 2 つまたは 3 つの命令が必要です。その理由は、単一の値をロードする場合は大抵、レジスタのプリファード・スロットにロードする必要があるためです。最初の命令でロードし、2 番目の命令で値をローテートさせて要求されたアドレスがレジスタの先頭に来るようにします。データがワードよりも小さい場合は、先頭からシフトしてプリファード・スロットに移動させる必要があります (ワードまたはダブルワードの場合は、レジスタの先頭がプリファード・スロットです)。以下に、バイトをロードするコードを示します。このコードは、レジスタ 3 のプリファード・スロットからアドレスを取得し、そのアドレスを使ってレジスタ 4 のプリファード・スロットにバイトをロードします。


リスト 3. アラインされていないメモリーからのロード
                
###Load byte unaligned address $3 into preferred slot of register $4###

#Loads from nearest quadword boundary
lqd $4, 0($3)
#Rotate value to the beginning of the register
rotqby $4, $4, $3
#Rotate value to the preferred slot (-3 for bytes, -2 for halfwords, and nothing for
words or doublewords)
rotqbyi $4, $4, -3

注意する点として、lqd 命令は 16 バイト境界からしかロードしません。そのためロードする際には下位 4 桁のビットを無視し、16 バイト境界にアラインされたクワッドワードのみをメモリーからロードします。つまり任意のアドレスについては、ロードされたクワッドワードのどこに対象の値があるのかまったくわからないということです。「バイト単位でクワッドワードを (左) ローテートする」という rotqby 命令は、ロード元のアドレスを使ってレジスタをどれくらいローテートさせるかを示します。この命令はレジスタ内にあるアドレスの下位 4 桁のビット (ロードでは無視されたビット) のみを使用して、ローテート値を判断します。この値は常に、指定されたアドレスをレジスタの先頭まで移動させるために左シフトするバイト数です。バイトの場合、プリファード・スロットはレジスタの先頭ではなく、右に 3 バイト分ずれた位置にあります。そこで、rotqbyi 命令が即値モードの値を基準にシフトします。ワードおよびダブルワード・サイズの転送にはこの最後の命令は必要ありません。プリファード・スロットは元々レジスタの先頭にあるためです。このようにして、レジスタ 4 はバイトがプリファード・スロットにシフトされた最終値を持つことになります。

格納するのは、さらに複雑になります。以下は、レジスタ $4 のプリファード・スロットにあるバイトをレジスタ $3 で指定されたアドレスに格納するコードです。


リスト 4. アラインされていないアドレスへの格納
                
###Store preferred byte slot $4 into unaligned address $3

#Load the data into a temporary register
lqd $5, 0($3)
#Generate the controls for a byte insertion
cbd $6, 0($3)
#Shuffle the data in
shufb $7, $4, $5, $6
#Store it back
stqd $7, 0($3)

上記の暗号のようなシーケンスを理解するのにも、念頭に置いておかなければならないのは、SPU は一度にクワッドワードだけを、クワッドワードにアラインされたアドレスにロードおよび格納するという点です。アラインされていないアドレスに 1 バイトだけを直接格納しようとすると、バイトが誤った位置に格納されてしまうだけでなく、クワッドワードの残りのバイトが上書きされてしまいます。このような事態を避けるには、まずクワッドワードをメモリーからロードし、クワッドワードの該当するバイトに値を挿入してから格納し直さなければなりません。ここで難しいのは、アドレスのみに基づいて正しい位置に値を挿入するという部分ですが、幸いにも、cbd (バイト挿入の制御を生成) および shufb (バイトを入れ替える) という 2 つの命令が救いの手になります。cbd 命令でアドレスを取得して制御ワードを生成し、shufb でその制御ワードを使用してそのアドレスのクワッドワードの適切な位置にバイトを挿入できるからです。上記の cbd $6, 0($3) はレジスタ 3 のアドレスを使用して 4 倍長の制御ワードを生成し、これをレジスタ 6 に格納します。shufb $7, $4, $5, $6 命令はレジスタ 6 の 4 倍長の制御ワードを使用して、レジスタ 7 に新しい値 (元はメモリー内にあり、現在はレジスタ 5 内にあるクワッドワードと、プリファード・スロットのレジスタ 4 からのバイトで構成) を生成し、レジスタ 7 に結果を格納します。バイトが入れ替えられると、値がメモリーに再び格納されます。

この手法を説明するため、ASCII 文字のアドレスを取ってロードし、これを大文字に変換してから格納する関数を作成します。main 関数に含まれる convert_to_upper 関数は、別のプログラムで再利用できるように別のファイルに書き込みます。main 関数のコードは以下のとおりです (convert_main.s として保存)。


リスト 5. 大文字変換プログラムの開始部分
                
.data

string_start:
.ascii "We will convert the following letter, "
letter_to_convert:
.ascii "q"
remaining:
.ascii ", to uppercase\n\0"

.text
.global main
.type main, @function

main:
	.equ MAIN_FRAME_SIZE, 32
	.equ LR_OFFSET, 16
	#PROLOGUE
	stqd $lr, LR_OFFSET($sp)
	stqd $sp, -MAIN_FRAME_SIZE($sp)
	ai $sp, $sp, -MAIN_FRAME_SIZE

	#MAIN FUNCTION
	ila $3, letter_to_convert
	brsl $lr, convert_to_upper
	ila $3, string_start
	brsl $lr, printf

	#EPILOGUE
	ai $sp, $sp, MAIN_FRAME_SIZE
	lqd $lr, LR_OFFSET($sp)
	bi $lr
	

次に、実際に大文字変換を行う関数を入力します (convert_to_upper.s として入力)。


リスト 6. 大文字に変換するための関数
                
.text
.global convert_to_upper
.type convert_to_upper, @function
convert_to_upper:
	#Register usage
	# $3 - parameter 1 -- address of byte to be converted
	# $4 - byte value to be converted
	# $5 - $4 greater than 'a' - 1?
	# $6 - $4 greater than 'z'?
	# $7 - $4 less than or equal to 'z'?
	# $8 - $4 between 'a' and 'z' (inclusive)?
	# $9 through $12 - temporary storage for final store
	# $13 - conversion factor

	#address of letter stored in unaligned address in $3
	#UNALIGNED LOAD
	lqd $4, 0($3)
	rotqby $4, $4, $3
	rotqbyi $4, $4, -3

	#IS IN RANGE 'a'-'z'?
	cgtbi $5, $4, 'a' - 1
	cgtbi $6, $4, 'z'
	nand $7, $6, $6
	and $8, $5, $7
	#Mask out irrelevant bits
	andi $8, $8, 255
	#Skip uppercase conversion and store if $4 is not lowercase (based on $8)
	brz $8, end_convert

is_lowercase:
	#Perform Conversion
	il $13, 'a' - 'A'
	absdb $4, $4, $13

	#Unaligned Store
	lqd $9, 0($3)
	cbd $10, 0($3)
	shufb $11, $4, $9, $10
	stqd $11, 0($3)

end_convert:
	#no stack frame, no return value, just return
	bi $lr
	

コードをコンパイルして実行するには、以下のコマンドを実行します。

spu-gcc convert_main.s convert_to_upper.s -o convert
./convert

main 関数の機能は以前とそれほど変わらないので、ここでは説明を省きます。ただし、この関数が convert_to_upper に渡しているのは文字そのものではなく、文字のアドレスであることに注意してください。

convert_to_upper 関数は、任意の文字のアドレスを取り、大文字に変換してから再び格納し、そして何も返しません。別の関数を呼び出すことがない convert_to_upper 関数には、スタック・フレームは必要ありません。

この関数はまず、前に説明したようにアラインされていないデータをレジスタ 4 にロードします。次にバイトが a から z の範囲内にあるかどうかをチェックします。チェックは、まずバイトが 'a' - 1 より大きいかどうかを比較し、それから 'z' より大きいかどうかを調べるという方法で行われます。「より小さい」かどうかの比較は行っていません。SPU では「より小さい」の比較は使用できないからです。SPUでは、「より大きい」および「等しい」の比較しかできないため、「より小さいか等しい (以下)」の比較が必要な場合は、「より大きい」の比較をしてから次に「否定」を実行します。この比較は、nand 命令の両方のソース引数を同じレジスタに設定して行います。それから、and 命令を使って比較を組み合わせます (xor を使ってすべての論理演算命令を 1 つにまとめることもできますが、そうするとコードの簡潔さがかなり損なわれることに注意してください)。分岐命令はハーフワードまたはワードの値でしか動作しないので、最後にレジスタの非該当部分にマスクをかけます (上記の例では、フルワードを扱っているためマスクは必要ありません)。

レジスタ 8 のプリファード・スロット内にあるビットがすべて false に設定されている場合は関数の終わりまでスキップし、true に設定されている場合は変換が行われます。SPU での唯一のバイト指向型の算術関数は、2 つのオペランドの差を絶対値で算出する absdb 「バイトの絶対差」 です。変換には、この絶対値を小文字と大文字の値の差と組み合わせて使用します。最後に、アラインされていないデータの格納を実行します。関数の呼び出しやローカル・ストレージは使用していないため、スタック・フレームは必要ありません。つまり、この時点でリンク・レジスタを使用しなくなります。



PPE との通信

今まで SPE 専用のプログラムに焦点を絞ってきたので、ここからは PPE 制御プログラムに目を向けます。それにはまず、PPE と SPE を通信させる方法を知る必要があります。

チャネルと MFC

SPE には、プロセッサーのメイン・メモリーとは別の、ローカル・ストアと呼ばれるメモリーがあることを思い出してください。SPE はメイン・メモリーを直接読み取ることができないので、メモリー・フロー・コントローラー (MFC) と呼ばれるユニットに対して (Memory Flow Controller) DMA コマンドを実行することで、ローカル・ストアとメイン・メモリー間でのデータのインポートおよびエクスポートを行わなければなりません。ローカル・ストアのアドレス空間は 32 ビットに制限されていますが、通常はこれより遥かに小さいサイズです (例えば、Sony® PLAYSTATION® 3ではわずか 18 ビットです)。SPE がプロセッサーのメイン・メモリーとは別のメモリーを持っている理由は、SPE コードによるメモリー・アクセスに決定性を持たせるためです。メイン・メモリーでは、スワップ、移動、キャッシュ、アンキャッシュが行われたり、メモリーがマップされる場合があります。そのため、特定のメモリー・アクセスに必要な時間はまるで見当がつきません (メモリーがスワップされる場合、スワップにかかる時間は誰にもわかりません)。一方、SPE のメモリーをローカル・ストアに分離すれば、SPE がアクセスするいずれのメモリーに対してもアクセス時間が決定的になり、SPE が、必要に応じてメイン・メモリーとの間でデータを非同期で移動するように MFC をスケジューリングできるようになります。SPE のローカル・ストア内にあるアドレスは、ローカル・ストア・アドレス (LSA) と呼ばれ、メイン・メモリー内にあるアドレスは実効アドレス (EA) と呼ばれます。この違いは、メモリー・フロー・コントローラーの DMA 機能を使用する方法を学ぶ際に重要になってきます。

SPE が外部と通信するにはチャネルを使用します。各チャネルは 32 ビットの領域で、チャネルに対する書き込みまたは読み取りには特殊な命令を使用します (書き込みと読み取りは単一方向のため、同時には行えません)。チャネルには深さ (チャネル・カウント) もあります。チャネル・カウントとは、読み取り操作を待機中のデータの量 (読み取りチャネルの場合)、あるいは書き込み可能なデータの量 (書き込みチャネルの場合) です。すべての SPE 入出力は、チャネルを通じて行われます。つまり、メモリー・フロー・コントローラーに対する DMA コマンドの発行、SPE イベントの処理、そして PPE に対するメッセージの読み取りと書き込みにはチャネルが使用されます。次に紹介するプログラムでは、MFC とチャネル・インターフェースを利用して、PPE の指定するデータで文字変換を行います。

SPE タスクの作成と実行

これまで main 関数ではパラメーターを使用していませんでしたが、PPE プログラムから実行する場合は、3 つの 64 ビット・パラメーターを受け取ることになります。この 3 つのパラメーターは、レジスタ 3 の SPE タスク識別子、レジスタ 4 のアプリケーション・パラメーターへのポインター、そしてレジスタ 5 のランタイム環境情報へのポインターです。アプリケーション・ポインターと環境ポインターが指す領域の内容は、実際はユーザーが定義します。ただし、これらのポインターが指定するのは、SPE のローカル・ストアではなく、アプリケーションのメイン・ストレージ (実効アドレス) 内のメモリーです。メイン・ストレージ内のメモリーには直接アクセスできないので、DMA によって取り込む必要があります。

SPE タスクを作成するには、関数 speid_t spe_create_thread(spe_gid_t spe_gid, spe_program_handle_t *spe_program_handle, void *argp, void *envp, unsigned long mask, int flags) を使用します。これらのパラメーターの役目は以下のとおりです。

  • spe_gid
    このタスクを割り当てる SPE スレッド・グループ。単にゼロに設定することができます。
  • spe_program_handle
    SPE プログラム自体に関するデータを保持する構造体へのポインター。通常このデータは、PPU 実行可能プログラム (後で記載します) 内に SPU アプリケーションを組み込んで自動的に定義するか、あるいは SPU アプリケーションが含まれるライブラリーで dlopen()/dlsym() を使用して定義するか、spe_open_image() を使って直接 SPU アプリケーションをロードして定義するか、のいずれかです。
  • argp
    プログラムの初期化に使用するアプリケーション固有データへのポインター。使用しない場合は null を設定してください。
  • envp
    プログラムの環境データへのポインター。使用しない場合は null を設定してください。
  • mask
    プロセッサー・アフィニティー・マスク。使用可能な SPE にプロセスを割り当てるには -1 を設定します。設定しない場合は、使用可能なプロセッサーごとのビット・マスクが含まれることになります。1 はプロセッサーを使用することを意味し、0 は使用しないことを意味します。大抵のアプリケーションでは、-1 に設定します。
  • flags
    SPE のセットアップ方法を変更するビット・フラグのセット。この記事の対象範囲ではありません。

DMA を使用した PPE/SPE プログラム

DMA 通信の一例として、PPE がストリングを取得し、SPE プログラムを呼び出してそのストリングのコピー、大文字への変換、そしてメイン・ストレージへの再コピーを行うプログラムを作成します。すべてのデータ転送には、SPE チャネルで制御される MFC のDMA 機能を使用します。

メイン SPE プログラムが受け取るのは、メイン・メモリー内に含まれるストリングのサイズとポインターが含まれる構造体への実効アドレス・ポインターです。プログラムはこのポインターをバッファーにコピーし、変換を行ってから再びコピーします。この SPE コードは以下のとおりです (convert_dma_main.s として入力)。


リスト 7. PPU プログラムの大文字変換を行うための SPU コード
                
.data

.align 4
conversion_info:
conversion_length:
	.octa 0
conversion_data:
	.octa 0
.equ CONVERSION_STRUCT_SIZE, 32

.section .bss #Uninitialized Data Section
.align 4
.lcomm conversion_buffer, 16384

.text
.global main
.type main, @function

#MFC Constants
.equ MFC_GET_CMD, 0x40
.equ MFC_PUT_CMD, 0x20

#Stack Frame Constants
.equ MAIN_FRAME_SIZE, 80
.equ MAIN_REG_SAVE_OFFSET, 32
.equ LR_OFFSET, 16

main:
	#Prologue
	stqd $lr, LR_OFFSET($sp)
	stqd $sp, -MAIN_FRAME_SIZE($sp)
	ai $sp, $sp, -MAIN_FRAME_SIZE

	#Save Registers
	#Save register $127 (will be used for current index)
	stqd $127, MAIN_REG_SAVE_OFFSET($sp)
	#Save register $126 (will be used for base pointer)
	stqd $126, MAIN_REG_SAVE_OFFSET+16($sp)
	#Save register $125 (will be used for final size)
	stqd $125, MAIN_REG_SAVE_OFFSET+24($sp)

	##COPY IN CONVERSION INFORMATION##
	ila $3, conversion_info         #Local Store Address
	#register 4 already has address #64-bit Effective Address
	il $5, CONVERSION_STRUCT_SIZE   #Transfer size
	il $6, 0                        #DMA Tag
	il $7, MFC_GET_CMD              #DMA Command
	brsl $lr, perform_dma

	#Wait for DMA to complete
	il $3, 0
	brsl $lr, wait_for_dma_completion

	##COPY STRING IN TO BUFFER##
	#Load buffer data pointer
	ila $3, conversion_buffer #Local Store
	lqr $4, conversion_data   #64-bit Effective Address
	lqr $5, conversion_length #SIZE
	il $6, 0                  #DMA Tag
	il $7, MFC_GET_CMD        #DMA Command
	brsl $lr, perform_dma

	#Wait for DMA to complete
	il $3, 0
	brsl $lr, wait_for_dma_completion

	#LOOP THROUGH BUFFER
	#Load buffer size
	lqr $125, conversion_length
	#Load buffer pointer
	ila $126, conversion_buffer
	#Load buffer index
	il $127, 0
loop:
	ceq $7, $125, $127
	brnz $7, loop_end

	#Compute address for function parameter
	a $3, $127, $126
	#Next index
	ai $127, $127, 1

	#Run function
	brsl $lr, convert_to_upper

	#Repeat loop
	br loop

loop_end:
        #Copy data back
        ila $3, conversion_buffer   #Local Store Address
        lqr $4, conversion_data     #64-bit effective address
        lqr $5, conversion_length   #Size
        il $6, 0                    #DMA Tag
        il $7, MFC_PUT_CMD          #DMA Command
	brsl $lr, perform_dma

        #Wait for DMA to complete
	il $3, 0
	brsl $lr, wait_for_dma_completion

	#Return Value
	il $3, 0

        #Epilogue
        ai $sp, $sp, MAIN_FRAME_SIZE
        lqd $lr, LR_OFFSET($sp)
        bi $lr
        

上記のコードは、いくつかのユーティリティー関数を使用して DMA コマンドを処理しています。これらの関数は dma_utils.s として入力してください。


リスト 8. DMA 転送ユーティリティー
                
##UTILITY FUNCTION TO PERFORM DMA OPS##
#Parameters - Local Store Address, 64-bit Effective Address, Transfer Size, 
DMA Tag, DMA Command
.global perform_dma
.type perform_dma, @function
perform_dma:
	shlqbyi $9, $4, 4  #Get the low-order 32-bits of the address
	wrch $MFC_LSA, $3
	wrch $MFC_EAH, $4
	wrch $MFC_EAL, $9
	wrch $MFC_Size, $5
	wrch $MFC_TagID, $6
	wrch $MFC_Cmd, $7
	bi $lr

.global wait_for_dma_completion
.type wait_for_dma_completion, @function
wait_for_dma_completion:
	#We receive a tag in register 3 - convert to a tag mask
	il $4, 1
	shl $4, $4, $3
	wrch $MFC_WrTagMask, $4
	#Tell the DMA that we only want it to inform us on DMA completion
	il $5, 2
	wrch $MFC_WrTagUpdate, $5
	#Wait for DMA Completion, and store the result in the return value
	rdch $3, $MFC_RdTagStat
	#Return
	bi $lr

ここで必要となるのは、プログラムをコンパイルすると同時に、PPE アプリケーションに組み込めるように準備することです。コードをコンパイルして組み込めるようにするためのコマンドは、前のプログラムの convert_to_upper.s がまだ現行ディレクトリーにある場合は以下のようになります。

spu-gcc convert_dma_main.s dma_utils.s convert_to_upper.s -o spe_convert
embedspu -m64 convert_to_upper_handle spe_convert spe_convert_csf.o

上記のコマンドが作成するリンク可能 CESOF と呼ばれるものによって、SPE のオブジェクト・ファイルを PPE アプリケーションに組み込み、必要に応じてロードできるようになります。

この SPU コードを利用するための PPU コードは以下のとおりです (ppu_dma_main.c として入力)。


リスト 9. SPU アプリケーションを利用するための PPU コード
                
#include <stdio.h>
#include <libspe.h>
#include <errno.h>
#include <string.h>

/* embedspu actually defines this in the generated object file,
 we only need an extern reference here */
extern spe_program_handle_t convert_to_upper_handle;

/* This is the parameter structure that our SPE code expects */
/* Note the alignment on all of the data that will be passed to the SPE is 16-bytes */
typedef struct {
	int length __attribute__((aligned(16)));
	unsigned long long data __attribute__((aligned(16)));
} conversion_structure;

int main() {
	int status = 0;
	/* Pad string to a quadword -- there are 12 spaces at the end. */
	char *tmp_str = "This is the string we want to convert to uppercase.            ";
	/* Copy it to an aligned boundary */
	char *str = memalign(16, strlen(tmp_str) + 1);
	strcpy(str, tmp_str);
	/* Create conversion structure on an aligned boundary */
	conversion_structure conversion_info __attribute__((aligned(16)));

	/* Set the data elements in the parameter structure */
	conversion_info.length = strlen(str) + 1; /* add one for null byte */
	conversion_info.data = (unsigned long long)str;

	/* Create the thread and check for errors */
	speid_t spe_id = spe_create_thread(0, &convert_to_upper_handle, 
	&conversion_info, NULL, -1, 0);
	if(spe_id == 0) {
		fprintf(stderr, "Unable to create SPE thread: errno=%d\n", errno);
		return 1;
	}

	/* Wait for SPE thread completion */
	spe_wait(spe_id, &status, 0);

	/* Print out result */
	printf("The converted string is: %s\n", str);

	return 0;
}

プログラムをビルドして実行するには、以下のコマンドを入力します。

gcc -m64 spe_convert_csf.o ppu_dma_main.c -lspe -o dma_convert
./dma_convert

この記事の目的は、次回の記事で最適化の秘訣を説明するときに泥沼にはまらないよう、必要なすべての基礎材料を紹介することなので、上記のコードではさまざまなことを行っています (最後まで読めば、たちまち SPU プログラミングのエキスパートになれるはずです)。それでは、その内容をこれから説明します。まずは多少簡単な PPU コードから始めましょう。

PPU コードでまず興味深い部分は、libspe.h ヘッダー・ファイルのインクルードです。このファイルには、プログラムを SPE で実行するためのすべての関数宣言が含まれています。コードは次に、convert_to_upper_handle というハンドルを参照しています。これは宣言そのものではなく、extern 参照でしかありません。convert_to_upper_handle は spe_convert_csf.o に定義されているためです。変数の名前は embedspu コマンドのコマンド・ラインで設定されています。この変数はプログラム・コードのハンドルで、SPE タスクを作成する際に使用されます。

次に定義しているのは、SPE プログラムのパラメーターとして使用する構造体です。定義には、ストリングの長さとストリング自体へのポインターが必要となります。いずれもクワッドワードにアラインし、メイン・プログラムにコピーして値を DMA 転送で使用できるようにしなければなりません。ここで使用したポインターは、単なるポインターとしてではなく、unsigned long long として宣言されていることに注意してください。これは、32 ビット・モードでコンパイルした場合でも、64 ビット・モードでコンパイルした場合でも、アドレス転送の格納方法を同じにするためです。ポインターを使用した場合は、32 ビット・モードでコンパイルが行われていたとすると、構造体の中では異なる方法でポインターがアラインされることになります。また、データを適切なアラインメント領域にコピーするため、memalign 関数と strcpy を使用しなければなりません。これが、夜通しこの問題について試行錯誤した結果得られたポインター対策です。もし続けて「バス・エラー」が発生する場合は、DMA 転送を実行する際に、おそらく 16 バイトにアラインされていないか、あるいは 16 バイトの倍数でない DMA 転送をしていることが考えられます。

メイン・プログラムでは、変数を宣言します。ここで宣言して DMA によってコピーされる変数はすべて、クワッドワード境界にアラインされること、そしてクワッドワードの倍数のサイズになることに注意してください。サイズが小さい転送では多少の例外もありますが、DMA 転送はソース・アドレスと宛先アドレスの両方でクワッドワードにアラインしなければならないためです (ソースと宛先が 128 バイトにアラインされていれば、プログラムのパフォーマンスはさらに向上します)。次に spe_create_thread により SPE タスクを作成し、パラメーターの構造体に渡します。ここでは spe_wait を使って SPE タスクが完了するのを待ってから、最終値をプリントアウトできます。ご想像のとおり、すべての DMA 転送を含め、このプログラムで興味深い部分のほとんどは SPE で行われています。DMA 転送が必ずと言っていいほど PPE ではなく SPE によって行われる理由は、SPE は PPE より遥かに大量のデータと遥かに多くのアクティブ DMA 演算を処理できるためです。

メイン・プログラムの詳細を説明する前に、DMA ユーティリティー関数について説明しておきましょう。まず、最初の関数は perform_dma で、当然これは DMA コマンドを実行します。DMA 転送を実行するために必要なチャネル操作のシーケンスは、Cell BE Handbook の 450 ~ 456 ページに定義されています (「参考文献」を参照)。関数は始めに、レジスタ 4 にある 64 ビットの実効アドレスを、32 ビットの上位コンポーネントと 32 ビットの下位コンポーネントの 2 つに変換しています (チャネルの幅は 32 ビットしかないからです)。チャネルはレジスタのワード・サイズのプリファード・スロットに書き込まれるので、64 ビット・アドレスの上位ビットはすでにプリファード・スロットに存在します。そのため、その内容を新しいレジスタに 4 バイト分シフトするだけで、プリファード・スロットに下位ビットが入るというわけです。次に wrch 命令を使用して、ローカル・ストア・アドレス、実効アドレスの上位ビット、実効アドレスの下位ビット、転送のサイズ、DMA コマンドの「タグ」、そしてコマンド自体を該当するチャネルに書き込みます。コマンドが書き込まれると、DMA 要求が MFC のキューに入れられます。この場合条件となるのは、MFC に使用可能なスロットがあるということですが、他に並行 DMA 要求は行っていないので当然、使用可能なスロットはあります。「タグ」は、1 つまたは複数の DMA コマンドに割り当てることができる番号です。同じタグで実行されたすべての DMA コマンドは単一のグループとしてみなされ、ステータスの更新およびシーケンス操作はグループ全体に適用されます。このアプリケーションでは、一度に 1 つの DMA コマンドだけがアクティブになるため、すべての操作では 0 を DMA タグとして使用します。DMA コマンドは、MFC_GET_CMD または MFC_PUT_CMD のいずれかです。DMA コマンドは他にもありますが、ここでは取り上げません。MFC コマンドは、コマンドを実際に発行しているのが SPE であるかどうかには関わらず、SPE の観点から行われます。つまり、MFC_GET_CMD はデータをメイン・メモリーからローカル・ストアに移し、MFC_PUT_CMD はその逆を行います。

DMA コマンドは非同期であるため、DMA コマンドが完了するのを待機できると実用的です。wait_for_dma_completion はまさにそれを実現する関数です。この関数はその唯一のパラメーターとして使用するタグをタグ・マスクに変換しDMA ステータスを要求してそのステータスを読み取ります。DMA 操作の完了を待機する方法は、値が 2 の $MFC_WrTagUpdate チャネルを書き込む際に、操作が完了するまで $MFC_RdTagStat に値を持たせないようにするというものです。したがって、rdch を使ってチャネルを読み取ろうとすると、DMA ステータスが使用可能になって転送が完了するまで、チャネルはブロックされます。

ここからは、いよいよ実際のプログラム自体に話題を移します。SPE プログラムが最初に実行することは、アプリケーションのパラメーター・データ用の空間を確保することです。また、データはクワッドワード境界にアラインされます (アセンブリー言語での .align 4 は、2^4 = 16 であるため C 言語での __attribute__((aligned(16))) と同じように機能します)。.octa はクワッドワードの値を確保します (ニーモニックは、16 ビットの時代からの継承です)。次に、構造全体のサイズとして定数 CONVERSION_STRUCT_SIZE を定義します。

次に続くのは、.bss セクションです。これは .data セクションと同様ですが、実行可能プログラム自体に値が含まれていないという点が異なります。このセクションは、値のために確保しなければならない空間の量を示しているだけで、未初期化データ用です。.lcomm conversion_buffer, 16384 は、conversion_buffer 記号に定義されたアドレスを先頭として 16K の空間を確保します。16K を保持するように定義される理由は、これが MFC DMA 転送の最大サイズだからです。ストリングが 16K より長い場合は、PPE がプログラムを複数回呼び出さなければならなくなります (より優れたプログラムでは、SPE 側で単に要求を複数のチャンクに分割します)。

main 関数にあるのは、プログラムの主要部分です。この関数では、まず初めにスタック・フレームを設定してから、プログラムのメイン制御に使用する 3 つの不揮発性レジスタを保存します。次に、DMA 転送を実行して PPE からパラメーターの構造体をコピーします。関数の最初のパラメーターは、PPE から渡された 64 ビット・アドレスです。続いて DMA コマンドを使って完全な構造体をフェッチし、DMA が完了するのを待ちます。この転送が完了すると、今度はフェッチした構造に含まれるデータを使用して、別の DMA 転送によってストリング自体をローカル・ストアのバッファーにコピーし、転送が完了するのを待ちます。バッファーのアドレスをロードするのに、ila (アドレスを即値ロード) 命令を使っていることに注意してください。ila 命令は 18 ビットを最大限とします。これは PLAYSTATION 3 には有効ですが、Cell BE のローカル・ストアのサイズがそれよりも大きい場合は以下の 2 つの命令でロードすることになります。

ilhu $3, conversion_buffer@h #load high-order 16 bits of conversion_buffer
iohu $3, conversion_buffer@l #"or" it with the low-order 16 bits of conversion_buffer

続いて、ターゲットの実効アドレス、ストリング長、DMA タグ、そして MFC_GET_CMD DMA コマンドのすべてが perform_dma に渡され、操作が完了するまでプログラムが待機します。

この時点ですべてのデータがロードされるので、ロードされたデータを変換します。ループ・カウンターとしてレジスタ 127 を使用し、ベース・ポインターとしてレジスタ 126 を使用して、バッファーの終わりに達するまでそれぞれの値で convert_to_upper を実行します。

loop_end に達した時点ですべてのデータの変換が完了するので、あとは変換されたデータをコピーするだけです。ここで使用するのは最後の転送に使用した DMA パラメーターですが、今回は MFC_PUT_CMD コマンドを実行します。DMA が完了すると、関数が実行されます。戻り値をレジスタ 3 にロードし、関数のエピローグを実行すると、スタック・フレームがリストアされて返されます。



メール・ボックスを使用した SPEとPPE 間の通信

DMA 転送は SPE と PPE との間で大量のデータを移動させるには優れた方法ですが、小規模な転送にはもっと単純な方法があります。その方法とは、これから概説するメール・ボックスです。SPE にとって、メール・ボックスは 32 ビットの値を PPE に書き込むための一連のチャネル (読み取りチャネルおよび書き込みチャネル) でしかありません。

このコンセプトを説明するため、非常に単純な SPE サーバーを作成してみます。このサーバーはメール・ボックスで符号なし整数値を待機してから、その値の二乗を再び書き込みます。コードは以下のとおりです (square_server.s として入力)。



リスト 10. SPU 二乗サーバー
                
.text
.global main
.type main, @function
main:
	#Read the value from the inbox (stalls if no value until one is available)
	rdch $3, $SPU_RdInMbox
	#Square the value
	mpyu $3, $3, $3
	#Write the value back
	wrch $SPU_WrOutMbox, $3
	#Go back and do it again
	br main


コードはたったこれだけです。このコードが要求をじっと待ち、要求を処理します。上記のサーバーは親プログラムが終了した時点で同じく終了します。受信トレイに有効な値がなければ、有効な値が入るまで rdch 命令が停止するまでです。

PPE 側のコードもそれほど複雑ではありません (square_client.c として入力)。



リスト 11. PPE 二乗クライアント
                
#include <libspe.h>
#include <stdio.h>

extern spe_program_handle_t square_server_handle;

int main() {
	int status = 0;

	/* Create SPE thread */
	speid_t spe_id = spe_create_thread(0, &square_server_handle, NULL, NULL, -1, 0);
	if(spe_id == 0) {
		fprintf(stderr, "Unable to create SPE thread!\n");
		return 1;
	}

	/* Request a square */
	spe_write_in_mbox(spe_id, 4);
	/* Wait for result to be available */
	while(!spe_stat_out_mbox(spe_id)) {}
	/* Read and display result */
	printf("The square of 4 is %d\n", spe_read_out_mbox(spe_id));

	/* Do it again */
	spe_write_in_mbox(spe_id, 10);
	while(!spe_stat_out_mbox(spe_id)) {}
	printf("The square of 10 is %d\n", spe_read_out_mbox(spe_id));

	return 0;
}


このプログラムをコンパイルして実行するには、以下のコマンドを実行します。

spu-gcc square_server.s -o square_server
embedspu -m64 square_server_handle square_server square_server_csf.o
gcc -m64 square_client.c square_server_csf.o -lspe -o square
./square

PPE についても、メール・ボックスは SPE の観点で名前が付けられます。つまり、PPE では受信トレイに書き込み、送信ボックスから読み取ります。SPE とは異なり、PPE は読み取りまたは書き込みの際に停止して値を待機することはありません。そのためプログラムでは、値を待機するのに spe_stat_out_mbox を使用し、メール・ボックスに書き込むためのスロットが残っているかどうかを調べるのに spe_stat_in_mbox を使用する必要があります。ただし、一度に動作する値は 1 つだけなので、後者は使用しません。

メール・ボックスの実力は、プログラムがメール・ボックスと DMA の手法を組み合わせたときに発揮されます。例えば、メール・ボックスでバッファー・アドレスをリッスンし、そのアドレスを使用して DMA で処理するすべてのデータを取り入れるといった SPE タスクを作成することもできます。



まとめ

この連載ではこれまで、Linux® を使用した PLAYSTATION 3 の Cell BE プロセッサーにおけるアセンブリー言語プログラミングの主要なコンセプトを説明してきました。取り上げた話題には、基本アーキテクチャー、SPU アセンブリー言語の構文、そして SPE と PPE 間通信の基本モードがあります。次回の記事では、Cell BE プロセッサーの SPE パフォーマンスを最大限引き出す方法に目を向けます。それ以降の記事では人生を少しでも楽にするために、この知識を C 言語での SPE プログラミングに適用する予定です。


参考文献

著者について

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

不正使用の報告のヘルプ

不正使用の報告

ありがとうございます。 このエントリーは、モデレーターの注目フラグが設定されました。


不正使用の報告のヘルプ

不正使用の報告

不正使用の報告の送信に失敗しました。


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, Multicore acceleration
ArticleID=245793
ArticleTitle=Cell BE プロセッサーでのハイパフォーマンス・アプリケーションのプログラミング、第 3 回: 相乗演算処理装置の紹介
publish-date=02222007
author1-email=johnnyb@eskimo.com
author1-email-cc=dwpower@us.ibm.com

タグ

Help
このタグで、My developerWorks のすべてのタイプのコンテンツを見つけるために検索フィールドを使用します。

スライダーバーを使用することで、より多く(少なく)タグを表示します。

人気のタグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するトップのタグを表示します。

マイ・タグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するお客様ご自身のタグを表示します。

このタグで、My developerWorks のすべてのタイプのコンテンツを見つけるために検索フィールドを使用します。人気のタグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するトップのタグを表示します。マイ・タグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するお客様ご自身のタグを表示します。