CellプログラミングではSPEプログラミングでの最適化テクニックが重要となる。CellにおけるDMA転送はLSとメインメモリの間をCPUを介さずに通信する仕組みである。今回は、DMA転送を最適化するダブルバッファリングというテクニックを試してみた。
DMA転送を最適化するテクニックとしてダブルバッファリングがある。 ダブルバッファリングでは、二つのバッファとタググループを使用し、DMA転送と計算を同時に行うことで処理時間を短縮することができる。
DMA転送に使用するバッファを2つ宣言する。
2^20という巨大なサイズを持つ配列に0~2^20-1間で初期値を代入する。PPE側で8つのSPEで処理を並列処理するようにデータを準備する。PPEの処理は次のようにする。
- DMA転送に使用するため128byte境界にアラインした領域をヒープにメモリを確保する。
- 配列の値を初期化する。
- 配列を16個のチャンクにわけ、1つのSPUで2つのチャンクの処理する。
- DMA転送するデータにはチャンクのサイズとチャンクの開始アドレスを設定する。
特定の境界にアラインされた領域をヒープ上に確保するには、CBE SDKで提供されているmalloc_align.hに含まれる_malloc_align()関数を利用する。確保したメモリは、free_align.hに含まれる_free_align()で開放する必要があることに注意する。
array_size = 2 << 20;
data = (int*)_malloc_align(array_size * sizeof(int), 7);
for(i=0;i<array_size;i++) data[i] = i;
配列をSPUで処理する際のアドレスやチャンクのサイズといった制御情報を持つ構造体を次のように定義する。padを入れて128byteのサイズで定義することでDMA転送を最適化する。
typedef struct _control_block {
unsigned int chunk_size;
unsigned int addrSB;
unsigned int addrDB;
unsigned char pad[116];
} control_block;
1つのDMA転送に渡すcontrol_block構造体は二つのチャンクへのアドレスを持つ。それぞれシングルバッファリングとダブルバッファリングを使用して処理するためのチャンクへの開始アドレスを設定する。1つのSPUで2つのチャンクを処理するので、元の配列を1/16したサイズをチャンクのサイズとする。
control_block cb[8] __attribute__ ((aligned(128)));
/** 省略 **/
chunk_size = array_size >> 4;
for(i=0;i<8;i++) {
cb[i].chunk_size = chunk_size * sizeof(int);
cb[i].addrSB = (unsigned int) &data[chunk_size * (2*i+0)];
cb[i].addrDB = (unsigned int) &data[chunk_size * (2*i+1)];
}
SPE側では、「データをインクリメントする関数」「シングルバッファリングでデータを読み込みデータをインクリメントする関数」「ダブルバッファリングを使用してデータを読み込みデータをインクリメントする関数」の3つを定義する。
DMA転送では1度の最大転送サイズが16KBとなっているため、バッファの大きさは16KB=4096byteの2倍のバッファを用意する。DMA転送のデータが格納されるため、128byteアラインする。
control_block cb __attribute__((aligned(128)));
/* DMA data buffer */
int dma_data_buffer[8192] __attribute__((aligned(128)));
int *data[2];
int *data[2]の二つのポインタは、dma_data_buffer変数で確保した領域をダブルバッファリングで使用するため二つにわけそれぞれの先頭アドレスへの参照を持つ。
data[0] = &dma_data_buffer[0];
data[1] = &dma_data_buffer[4096];
SIMD演算を使用してデータをインクリメントする関数の定義。
/* SIMD演算でデータをインクリメント */
void inc_data_SIMD(int *dest, unsigned int asize) {
int i;
vector unsigned int *vdest;
vector unsigned int vinc = (vector unsigned int) {1,1,1,1};
vdest = (vector unsigned int *) dest;
for(i=0;i<(int)asize/16;i++) vdest[i] = spu_add(vdest[i], vinc);
}
シングルバッファリングとダブルバッファリングを利用してデータを読み込む関数を比較する。
loopcountにはチャンクサイズを1度のDMA転送の最大サイズ16KBで割った値が格納されているものとする。
次の関数ではシングルバッファを利用し引数で渡された実効アドレスのデータを読み込み、バッファに格納する。タグ値として20を指定する。mfc_write_tag_maskで20bit目にマスクをあてDMA転送が完了するまで、mfc_read_tag_status_all()でプロセッサが停止する。その後データをインクリメントし、mfc_putマクロを使用してデータをLSからメインメモリに転送する。
このようにシングルバッファリングではデータ転送とデータの計算処理が逐次的に行われるため効率が悪い。
void single_buffer_example(unsigned int addr) {
int i;
for(i=0;i<loopcount;i++) {
mfc_get(data[0], addr+16384*i, 16384, 20, 0, 0); // 16KB
mfc_write_tag_mask(1 << 20);
mfc_read_tag_status_all();
inc_data_SIMD(data[0], 16384);
mfc_put(data[0], addr+16384*i, 16384, 20, 0, 0);
mfc_write_tag_mask(1 << 20);
mfc_read_tag_status_all();
}
}
ダブルバッファリングを利用したDMA転送では、転送とデータの計算処理が並行して行われる。
ダブルバッファリングを利用したDMA転送は、初めの読み込み・ループによる繰り返し読み込み・最後に転送されたデータの処理の3部から構成される。
void double_buffer_example(unsigned int addr) {
int i;
mfc_get(data[0], addr, 16384, 20, 0, 0); // 16KB
for(i=1;i<loopcount;i++) {
mfc_write_tag_mask(1<<(20+(i&1)));
mfc_read_tag_status_all();
mfc_get(data[i&1], addr+16384*i, 16384, 20+(i&1), 0, 0);
mfc_write_tag_mask(1<<21-(i&1));
mfc_read_tag_status_all();
inc_data_SIMD(data[(i-1)&1], 16384);
mfc_put(data[(i-1)&1], addr+16384 * (i-1), 16384, 21-(i&1), 0, 0);
}
mfc_write_tag_mask(1<<21);
mfc_read_tag_status_all();
inc_data_SIMD(data[1], 16384);
mfc_put(data[1], addr+16384*(loopcount-1), 16384, 21, 0, 0);
mfc_write_tag_mask(1<<20 || 1<<21);
mfc_read_tag_status_all();
}
次の関数では、ループ処理に入る前にDMA転送でメインメモリからLS上のdata[0]にデータをタグの値に20をつけて転送する。ループ一回目の動作は次のようになる。
mfc_write_tag_mask(21)が実行されるため、mfc_read_tag_status_all()でプロセッサがDMA転送が完了するまで待つことは無い。DMA転送ニ使用しているバッファとは違うもう1つのバッファdata[i&1]=>data[1]に並行してデータをシステムメモリから読み込む。このときタグの値として20とは違う値(21)を指定する。
mfc_get(data[0], addr, 16384, 20, 0, 0); // 16KB
for(i=1;i<loopcount;i++) {
mfc_write_tag_mask(1<<(20+(i&1)));
mfc_read_tag_status_all();
mfc_get(data[i&1], addr+16384*i, 16384, 20+(i&1), 0, 0);
mfc_write_tag_mask(1<<20);で20ビット目にマスクを指定してmfc_read_tag_status_all();を実行することで、ループ処理の前に開始したDMA転送が完了するまで待つ。(その間もタグに21を指定したDMA転送が並行して動作している) 読み込まれたdata[0]から始まる値をインクリメントする。
mfc_get(data[i&1], addr+16384*i, 16384, 20+(i&1), 0, 0);
mfc_write_tag_mask(1<<21-(i&1));
mfc_read_tag_status_all();
inc_data_SIMD(data[(i-1)&1], 16384);
mfc_put(data[(i-1)&1], addr+16384 * (i-1), 16384, 21-(i&1), 0, 0);
}
loopcountの値は常に偶数になることに注意する。
ループ処理が完了した後に、mfc_get(data[i&1], addr+16384*i, 16384, 20+(i&amp;amp;1), 0, 0);の転送処理が完了するまで待つ。ループの最後の繰り返しで実行されるmfc_put(data[(i-1)&1], addr+16384 * (i-1), 16384, 21-(i&amp;amp;1), 0, 0);は常にdata[0]が指すバッファに対しての処理となるため、data[1]のバッファも同様にLSからメインメモリにDMA転送する。
そして、最後にmfc_write_tag_mask(1<<20)で全てのDMA転送が完了するまで待つためのマスクを指定する。
}
mfc_write_tag_mask(1<<21);
mfc_read_tag_status_all();
inc_data_SIMD(data[1], 16384);
mfc_put(data[1], addr+16384*(loopcount-1), 16384, 21, 0, 0);
mfc_write_tag_mask(1<<20 || 1<<21);
mfc_read_tag_status_all();
}
今回は、DMA転送関連のプログラミングTipsとしてダブルバッファリングを調べた。ダブルバッファリングを拡張したマルチバッファリング・共有I/Oバッファというテクニックもあるようだ。これについては次回。
参考
Cell Broadband Engine Programming Handbook Version 1.0 [PDF] (10MB近くあります)