4.2 DMA転送を活用する
出典: PS3 Linux Information Site / Cell/B.E.のパワーを体験しよう
第3.3節では、DMA転送を利用する際に、いくつかの注意事項を解説しました。本節では、Cellのアーキテクチャに沿って、その注意事項についてもう少し詳しく解説していきます。また、DMA転送を有効活用するためのいくつかのテクニックについて紹介します。
4.2.1 DMA転送サイズと転送アドレスのアラインメント
これまでのケースでは、16バイトの倍数のデータサイズのDMA転送のみを取り扱ってきましたが、実際にはCellでは16バイト未満のデータサイズのDMA転送も可能です。ここでは、16バイト未満のデータサイズの場合を含めて、DMA転送において満たすべき条件についてより厳密に述べます。
(1) DMA転送サイズ
DMA転送のデータサイズは、基本的に16バイトの倍数 (16、32、48バイト…) で指定する必要があります。16バイト未満のデータをDMA転送したい場合、1、2、4、8バイトの各サイズのDMA転送をサポートしています。1度のDMA転送で転送できるデータサイズは、最大16Kバイトまでです。
(2) DMA転送アドレスのアラインメント (16バイト以上のDMA転送)
転送サイズが16バイト以上の場合は、転送元と転送先のアドレスがそれぞれ16バイト境界に揃えられている必要があります。また、DMA転送するデータ領域のアドレスを128バイト境界に揃えた場合、DMA転送の実行性能が最も高くなります。
(3) DMA転送アドレスのアラインメント (16バイト未満のDMA転送)
一方、転送サイズが16バイト未満の場合は、転送元と転送先のデータ領域のアドレスについては、以下の条件を満たしていけなればいけません。
① 転送サイズに応じた自然なアラインメントである (転送サイズのバイト境界に揃えられている) こと
② 実効アドレスおよびLSアドレスの下位4ビット (16バイト境界からの相対位置) が同一であること
例えば、図 4.3に示すように、4バイトのデータをDMA転送する場合は、実効アドレスとLSアドレスがそれぞれ、4バイト境界に揃えられ、かつ、実効アドレスとLSアドレスの16バイト境界からの相対位置が同じである必要があります。
このように16バイト未満のDMA転送では、16バイト単位のDMA転送と比べるとより細かなルールを守らなければいけません。また、16バイト未満のDMA転送は、転送効率も高くないため、シグナル送信のような特殊な用途以外では利用しないようにしてください。
これらの条件が満たされていない場合、MFCの処理は一時停止し、DMAアラインメント・エラーの割り込みが発生します。その結果、PPEプログラム、SPEプログラム双方が異常終了する場合があります。
4.2.2 DMAダブルバッファリング
多くの場合、DMA転送はSPEプログラムの処理時間の大きな割合を占めますので、効率的にDMA転送をおこなうことは、アプリケーションを高速化する上で非常に重要です。ここでは、SPEの性能を最大限に活用して、効率的にDMA転送をおこなうためのテクニックとして「ダブルバッファリング」という手法を紹介します。
まずは、これまでに紹介したDMA転送の手順の効率について考えてみます。第3.3節のDMA転送をともなう基本的なプログラムでは、以下のような手順で処理していました。
(1) 入力バッファに対してDMA転送 (GET) を開始します。
(2) 手順 (1) のDMA転送 (GET) の完了を待ちます。
(3) 計算処理を実行し、出力バッファに計算結果を格納します。
(4) 出力バッファに対してDMA転送 (PUT) を開始します。
(5) 手順 (4) のDMA転送 (PUT) の完了を待ちます。
(6) 手順 (1)~(5) を繰り返します。
この処理の流れを入出力バッファを軸に時系列で表すと、図 4.4 (a) のようになります。また、図 4.4 (a) の図をSPUとMFCがそれぞれ処理している時間に注目して書き換えると、図 4.4 (b) のようになります。
図 4.4 (b) を見て分かるように、これまでのDMA転送の手順ですと、SPUが計算をしている間は、MFCは何も処理しておらず、またMFCがDMA転送をしている間は、SPUはただ単にDMA転送の完了を待っているだけで、SPEの性能を最大限に活用しているとは言えません。
そこで、MFCとSPUを並列して処理させることによりDMA転送を効率よくおこなうための方法が各種考案されており、「ダブルバッファリング」はこのような手法の1つです。
ダブルバッファリングでは、処理の入力や出力に用いるバッファを2組準備し、一方のバッファに対して計算している間にもう一方のバッファに対してDMA転送をおこなうことによって、効率の向上を図ります。なお、これまでのDMA転送の手法は1組の入出力バッファを利用するため、「シングルバッファリング」と呼ばれます。
各組の入出力バッファを軸に、ダブルバッファリングの処理の流れを時系列に表すと、図 4.5 (a) のようになります。また、図 4.5 (a) の図をMFCとSPUがそれぞれ処理している時間に注目して書き換えたものを図 4.5 (b) に示します。
図4.5 (b) を見て分かるように、ダブルバッファリングでは、SPUが計算をしている間にMFCがDMA転送をおこなっているため、SPUがただ単に待っている時間がなくなっています。このように、ダブルバッファリングを利用してDMA転送をおこなうことによって、効率よく計算とDMA転送を並列して実行することができます。
第3.3節で解説した単純なDMA転送の使い方では、DMA転送コマンドを発行した時には必ず直後で完了を確認してから次の処理に進むため、DMA転送と計算を並行して実行することはできませんが、次のようなDMA転送の機能を活用すれば、それも可能になります。
(1) MFCがDMA転送をおこなっている間もSPUは別の処理を同時におこなうことができます。また、その機能を活用するために、DMA転送コマンドの発行と完了待ちは、別々の関数になっています。
(2) DMA転送コマンドは、同時に複数発行することができ、それぞれをタグIDで区別して、個々に完了を待つことができます。
それでは、ダブルバッファリングの基本的な手順を以下に示します。
(1) 入力バッファ0に対して最初のDMA転送 (GET) を開始します。
(2) 入力バッファ1に対して次のDMA転送 (GET) を開始します。
(3) 入力バッファ0に対して発行したDMA転送 (GET) の完了を待ちます。
(4) 出力バッファ0に対して発行したDMA転送 (PUT) の完了を待ちます。
(5) 入出力バッファ0に対して計算処理を実行します。
(6) 出力バッファ0に対してDMA転送 (PUT) を開始します。
(7) 入力バッファ0に対して次のDMA転送 (GET) を開始します。
(8) 入出力バッファ1と0を切り替えます。
(9) 入力データを全て処理するまで、手順 (3) ~(8)を繰り返します。
(10) 出力バッファ0と1に対する未完了のDMA転送 (PUT) を待ちます。
この手順を図に示すと、図 4.6のようになります。
ダブルバッファリングの基本方針としては、入出力バッファがDMA転送可能な状態になったらすぐにDMA転送コマンドを発行し、入出力バッファを計算で使うときになって初めてDMA転送の完了を確認するようにします。
手順の開始時には入力バッファ0と1ともに空いているので、最初の計算で使う入力データのDMA転送を連続して発行します (手順 (1)、(2))。続いて、計算に使う入力データが揃うのと、計算結果を出力するバッファが空くのを待ち (手順 (3)、(4))、それらを用いて計算します (手順 (5))。計算が完了したら、計算結果をDMA転送 (PUT) するのと同時に、空いた入力バッファに対して次の計算で使う入力データをDMA転送 (GET) します (手順 (6)、(7))。これらの手順をバッファを切り替えながら (手順 (8))、すべてのデータを処理するまで繰り返します (手順 (9))。手順 (3)~(8) の繰り返しが終了すると、手順 (6) において出力バッファ0と1に対する最後のDMA転送 (PUT) が発行だけされた状態になっていますので、それらの完了を待ちます (手順 (10))。
次に、この手順でダブルバッファリングをおこなう具体的なプログラムについて解説していきます。リスト (4-1) は、ダブルバッファリングを利用しメインメモリ上にあるデータをメインメモリ上の別の領域にコピーするプログラムのソースコードの抜粋です。
リスト(4-1) ダブルバッファリング・サンプルコード (抜粋)
1 #define TOTALSIZE (1 << 24)
2 #define BUFSIZE (1 << 14)
3
4 char ibuf[2][BUFSIZE] __attribute__((aligned(128)));
5 char obuf[2][BUFSIZE] __attribute__((aligned(128)));
6
7 unsigned long long ea_in, ea_out;
8 unsigned int itag[2] = { 0, 1 };
9 unsigned int otag[2] = { 2, 3 };
10
11 void double_buffer(void)
12 {
13 int i;
14 int cur;
15
16 cur = 0;
17
18 initiate_dma_get_input(ibuf[0], itag[0]); /* Step 1 */
19
20 initiate_dma_get_input(ibuf[1], itag[1]); /* Step 2 */
21
22 for (i = 0; i < TOTALSIZE/BUFSIZE-2; i++) {
23 wait_dma_completion((1 << itag[cur]) | (1 << otag[cur])); /* Step 3 and 4 */
24
25 compute(ibuf[cur], obuf[cur]); /* Step 5 */
26
27 initiate_dma_put_result(obuf[cur], otag[cur]); /* Step 6 */
28
29 initiate_dma_get_input(ibuf[cur], itag[cur]); /* Step 7 */
30
31 cur ^= 1; /* Step 8 */
32 }
33
34 wait_dma_completion((1 << itag[cur]) | (1 << otag[cur])); /* Step 3 and 4 */
35
36 compute(ibuf[cur], obuf[cur]); /* Step 5 */
37
38 initiate_dma_put_result(obuf[cur], otag[cur]); /* Step 6 */
39
40 cur ^= 1; /* Step 8 */
41
42 wait_dma_completion((1 << itag[cur]) | (1 << otag[cur])); /* Step 3 and 4 */
43
44 compute(ibuf[cur], obuf[cur]); /* Step 5 */
45
46 initiate_dma_put_result(obuf[cur], otag[cur]); /* Step 6 */
47
48 wait_dma_completion((1 << otag[0]) | (1 << otag[1])); /* Step 10 */
49 }
50
51 void initiate_dma_get_input(char *buf, unsigned int tagid)
52 {
53 spu_mfcdma64(buf, mfc_ea2h(ea_in), mfc_ea2l(ea_in), BUFSIZE, tagid, MFC_GET_CMD);
54 ea_in += BUFSIZE;
55 }
56
57 void initiate_dma_put_result(char *buf, unsigned int tagid)
58 {
59 spu_mfcdma64(buf, mfc_ea2h(ea_out), mfc_ea2l(ea_out), BUFSIZE, tagid, MFC_PUT_CMD);
60 ea_out += BUFSIZE;
61 }
62
63 void wait_dma_completion(unsigned int mask)
64 {
65 spu_writech(MFC_WrTagMask, mask);
66 spu_mfcstat(MFC_TAG_UPDATE_ALL);
67 }
68
69 void compute(char *src, char *dst)
70 {
71 memcpy(dst, src, BUFSIZE);
72 }
| 1~2行目 | DMA転送用のバッファサイズを定義しています。TOTALSIZEは、メインメモリ側の領域のサイズ(16MB)で、BUFSIZEはLS側のバッファサイズ(16KB)です。 |
| 4~9行目 | ダブルバッファリングで利用するLS側入出力バッファと、実効アドレス変数、DMA転送タグIDを定義しています。 |
| 14行目 | ダブルバッファリングでバッファの切り替えをおこなうための変数curを定義します。 |
| 16行目 | 入出力バッファ0から使い始めるので、変数curを0で初期化します。 |
| 18~20行目 | 入力バッファ0、1に対して最初のDMA転送 (GET) を発行します。 |
| 23行目 | 入出力バッファibuf[cur]、obuf[cur]に対するDMA転送(GET/PUT)の完了を待ちます。 ここでは、入力バッファと出力バッファのタグIDを同時に指定して、2つのDMA転送の完了を一度で待っています。なお、forループの初回においては出力バッファのDMA転送は発行されていませんが、発行されていないDMAのタグIDに対してはDMA転送は完了しているものとして扱われるため、問題ありません。 |
| 25行目 | 入力バッファibuf[cur]に対して、計算をおこない、結果をobuf[cur]に格納します。 |
| 27行目 | 出力バッファobuf[cur]に対してDMA転送 (PUT) を発行します。 |
| 29行目 | 入力バッファibuf[cur]に対して次のDMA転送 (GET) を発行します。 |
| 31行目 | 入出力バッファを切り替えます。排他的論理和を利用することで、入出力バッファのインデクスが0と1を交互に切り替わるようにしています。 |
| 34~46行目 | 繰り返しの最後の2回は、DMA転送 (GET) が不要なので、forループの外で特別に処理します。 |
| 48行目 | 最後に、出力バッファ0、1に対するDMA転送 (PUT) の完了を待ちます。 |
ダブルバッファリングに関する詳細は、「Cell Broadband Engine Programming Handbook」の第19章「DMA Transfers and Inter-Processor Communication」および第24.1節「DMA Transfers」を参照してください。
4.2.3 MFCアトミック更新
複数のSPEプログラムが、メインメモリ上の同じ領域にある変数を共有し、排他的に読み書きしたい場合には、通常のDMA転送ではなくMFCアトミック更新という特殊なDMA転送を利用します。
このような場合の例として、メインメモリ上の変数を複数のSPEで共有して、それぞれのSPEがその共有変数をインクリメントするプログラムを考えてみます。通常のDMA転送を利用して作成すると、リスト (4-2) のようなプログラムになります。このプログラムでは、共有変数の値をDMA転送 (GET) により取得して、インクリメントした結果をDMA転送 (PUT) により書き戻しています。
リスト (4-2) 通常のDMA転送によるインクリメント・プログラム
1 #include <spu_intrinsics.h>
2 #include <spu_mfcio.h>
3
4 int counter __attribute__((aligned(128)));
5
6 int main(unsigned long long spe, unsigned long long argp, unsigned long long envp)
7 {
8 int i;
9
10 for (i = 0; i < 10000; i++) {
11 spu_mfcdma64(&counter, mfc_ea2h(argp), mfc_ea2l(argp), sizeof(int), 0, MFC_GET_CMD);
12 spu_writech(MFC_WrTagMask, 1 << 0);
13 spu_mfcstat(MFC_TAG_UPDATE_ALL);
14
15 counter++;
16
17 spu_mfcdma64(&counter, mfc_ea2h(argp), mfc_ea2l(argp), sizeof(int), 0, MFC_PUT_CMD);
18 spu_writech(MFC_WrTagMask, 1 << 0);
19 spu_mfcstat(MFC_TAG_UPDATE_ALL);
20 }
21
22 return 0;
23 }
このプログラムは、SPE1個では意図したとおりに動作しますが、複数のSPEで同時に実行すると問題が起こります。ここでは、その問題について説明します。
図 4.7は、リスト (4-2) のインクリメント・プログラムを2個のSPEで実行した場合の例を示しています。
図 4.7 (a) のようにSPE1の処理とSPE2の処理が同時におこなわれなければ、共有変数は正しくインクリメントされます。しかし、図 4.7 (b) に示すように、SPE1がインクリメントした結果でメインメモリ上の共有変数を更新する前にSPE2が共有変数を読み込んでしまうと、SPE2が古い値を使って計算した結果でSPE1の更新結果が上書きされることになり、正しい結果が得られません。
このように、通常のDMA転送では、複数のSPEで共有された変数を正しく更新することができません。しかし、SPEが共有変数を読み込んだ後に他のSPEが共有変数を更新したとしても、そのことに気付くことができれば、もう一度共有変数の読み込みからやり直すことによって、この問題を回避できます。このような手順を実現するために、MFCアトミック更新という機能があります。
MFCアトミック更新では、通常のDMA転送におけるGETコマンド、PUTコマンドの代わりに、GETLLAR (Get Lock Line and Reserve) コマンドとPUTLLC (Put Lock Line Conditional) コマンドをそれぞれ利用します。
GETLLARコマンドを使用することによって、メインメモリ上の領域のデータがLSに読み込まれ、SPEはこれ以降のその領域に対する他のSPEによる更新を知ることができるようになります。また、PUTLLCコマンドは、GETLLARコマンドで読み込んできたメインメモリ上の領域にデータを書き込むために用いられますが、その領域がGETLLARコマンド以降に他のSPEによって更新されていると、PUTLLCコマンドは失敗し、データの書き込みはおこなわれません。SPEはPUTLLCコマンドの成否を確認することができますので、PUTLLCコマンドが失敗していた場合は成功するまで、GETLLARコマンドからやり直すことによって、共有変数を正しく更新することができます。
リスト (4-3) は、MFCアトミック更新を利用したインクリメント・プログラムのソースコードの抜粋です。
リスト (4-3) MFCアトミック更新によるインクリメント・プログラム
1 #include <spu_intrinsics.h>
2 #include <spu_mfcio.h>
3
4 int counter[32] __attribute__((aligned(128)));
5
6 int main(unsigned long long spe, unsigned long long argp, unsigned long long envp)
7 {
8 int i;
9
10 for (i = 0; i < 10000; i++) {
11 do {
12 spu_mfcdma64(counter, mfc_ea2h(argp), mfc_ea2l(argp), 128, 0, MFC_GETLLAR_CMD);
13 spu_readch(MFC_RdAtomicStat);
14
15 counter[0]++;
16
17 spu_mfcdma64(counter, mfc_ea2h(argp), mfc_ea2l(argp), 128, 0, MFC_PUTLLC_CMD);
18 } while(spu_readch(MFC_RdAtomicStat) & MFC_PUTLLC_STATUS);
19 }
20
21 return 0;
22 }
| 4行目 | DMA転送用のバッファサイズを定義しています。MFCアトミック更新では、必ず128バイト境界から128バイトの領域がDMA転送されるため、その条件にあったバッファを用意します。 |
| 12行目 | GETLLARコマンドを発行し、メインメモリ上の共有変数のデータを読み込みます。 |
| 13行目 | MFCアトミック更新コマンドを発行したら、必ずspu_readch()組み込み関数を利用して、MFC_RdAtomicStatチャネルの値を読み、DMA転送の完了を待ちます。 |
| 15行目 | 共有変数をインクリメントします。 |
| 17行目 | PUTLLCコマンドを発行し、メインメモリ上の共有変数にインクリメントした結果を書き込みます。もし、他のSPEプログラムが共有変数をすでに更新している場合は、インクリメントの結果はメインメモリへ書き込まれません。 |
| 18行目 | spu_readch()組み込み関数によって、MFC_RdAtomicStatチャネルの値を読み、PUTLLCコマンドが成功したかどうかをチェックします。もし、PUTLLCコマンドが失敗している場合、12行目からやり直します。 |
このように、MFCアトミック更新を利用することで、複数SPEによって共有される変数を排他的に更新することができます。さらに、MFCアトミック更新は、セマフォ、ミューテックスなどさまざまな同期機構を実現するためにも使用できます。また、PPEにも同等の仕組みがあるため、MFCアトミック更新は、PPEとの変数の共有にも使えます。
MFCアトミック更新については、転送サイズやコマンドの完了待ちの手順などが通常のDMA転送コマンドとは異なるため、使用の際には特に注意が必要です。詳しくは、「Cell Broadband Engine Programming Handbook」の第20.3節「SPE Atomic Synchronization」を参照してください。






