アトミックDMA更新コマンド(LL/SC)を使ってミューテックスを作る
出典: PS3 Linux Information Site / Cell/B.E.のパワーを体験しよう
目次 |
ミューテックスをどうやって実装するか
一般にミューテックスはアトミック命令と呼ばれる不可分なハードウェア命令を用いて実装されます。 昔は Test And Set(TAS)命令や Compare and Swap(CAS)命令などが用意されてきましたが、 現在では Load Linked (またはLoad Locked) / Store Conditional 命令が用意される事が多いようです。 CellのSPEではMFCを使ったDMA更新コマンドとして128バイトまでに対応したLL/SC命令が用意されています。 これは一般的なPCではシングルワードのLL/SCか、あってもダブルワードなLL/SCであることを考えると画期的です。Cellが最初からマルチコアを想定して設計されたことがよくわかります。
ミューテックスやセマフォはこのLL/SC命令を使って実装します。 ここでは例として簡単なミューテックスを作成します。
ミューテックスの実装
ミューテックスはメインメモリ上のデータを排他的に変更することで同時に実行できるプロセスを1つに制御する仕組みです。データが1の時が初期状態で誰もデータを所持していない状態です。プロセスはミューテックスをロードし値を0に変更した後ストアします。このプロセスが”ロックを保持”しています。他のプロセスはミューテックスが0の時は先に進めません。この時確実に唯一人がデータを保持している事を保証するのにアトミック命令であるLL/SC命令を使います。 以下のサンプルではこのメモリ上のデータをmutexと名付けました。 ロックをとる手順はこのようになります。
- mutexをロード(ll)
- mutexが0なら1にもどる
- mutex -= 1
- mutexをストア(sc)
ロックを開放する手順(アンロック)はこのようになります。
- mutexをロード(ll)
- mutex += 1
- mutexをストア(sc)
なおLL/SC命令はDMAキューに入らずに即座に実行されます。したがってミューテックスをとってクリティカル領域に入って普通のDMAコマンドを実行する時はDMAのバリアコマンドを適切に発行する必要があります。ここでは単にLL/SC命令だけを扱います。
次に実際のコードを見ていきます。ロックとアンロックの手順はほぼ同じなので解説はロックだけとします。完全なコードはソースを見てください。
コード解析
LL/SC命令を使った排他制御は複数SPEを使いこなすうえで必須の知識です。ここでは多少くどいですが全てのソースを表示した後、一つずつ順番に解説してきます。
ソース全文
#include <stdio.h>
#include <spu_mfcio.h>
#include <cassert>
volatile int mutex __attribute__((aligned(128))); // (1)
inline void busy_loop (int max)
{
for (int i = 0; i < max; i++)
for (int j = 0; j < max; j++)
asm volatile ("sync");
}
int main (unsigned long long spe_id, unsigned long long xxx_argp, unsigned long long xxx_argv)
{
unsigned int ea_mutex = spu_readch (SPU_RdInMbox); // (2)
// acquire mutex
do
{
retry:
unsigned int s;
spu_mfcdma32 (&mutex,
ea_mutex,
128,
0,
MFC_GETLLAR_CMD); // (3)
s = spu_readch (MFC_RdAtomicStat); // (4)
assert (s != 0);
if (!mutex) // (5)
goto retry;
mutex -= 1; // (6)
spu_mfcdma32 (&mutex,
ea_mutex,
128,
0,
MFC_PUTLLC_CMD); // (7)
} while (spu_readch (MFC_RdAtomicStat)); // (8)
printf ("spe: critical region\n"); // (9)
busy_loop (10000);
// release mutex
do
{
unsigned int s;
spu_mfcdma32 (&mutex,
ea_mutex,
128,
0,
MFC_GETLLAR_CMD); // (10)
s = spu_readch (MFC_RdAtomicStat); // (11)
assert (s != 0);
mutex += 1; // (12)
spu_mfcdma32 (&mutex,
ea_mutex,
128,
0,
MFC_PUTLLC_CMD); // (13)
} while (spu_readch (MFC_RdAtomicStat)); // (14)
return 0;
}
(1) ミューテックスのローカルコピーの確保
volatile int mutex __attribute__((aligned(128))); // (1)
ミューテックスとして使うデータはメインメモリ上に置かれます。SPEではこのデータをDMA転送でLSに転送して使います。ここでは128バイト境界に整列された領域を確保します。これはGETLLAR, PUTLLCでは必ず転送はキャッシュライン単位(128バイト)で行われるです。なお、本例では単純に128バイトアラインされた4バイトの整数をmutex領域としていますが、よりポータブルな実装とするには、128バイトの領域を確保してその中の4バイトだけを更新するようにしてください(例えば、本例ではmutex変数のすぐあとにアラインされていない変数を宣言していた場合、その値はGETLLARのタイミングで上書きされてしまいます)。
(2) ミューテックスのメインメモリのアドレスを得る
unsigned int ea_mutex = spu_readch (SPU_RdInMbox); // (2)
ミューテックスはメインメモリ上の任意の位置に置かれるため、PPE側からアドレスを教えてもらわなければなりません。本例ではPPE側が32bitコードであることを仮定しているため、ここではmailboxを使いました。
(3) ミューテックスをll命令でロードする
spu_mfcdma32 (&mutex, ea_mutex, 128, 0, MFC_GETLLAR_CMD); // (3)
通常のDMA転送ではなくll命令のDMA転送を行いメインメモリからLSにロードします。 ll命令は後で後続のsc命令と対で使います。 ll/sc命令はTAS(Test And Set)命令やCAW(Compare And Swap)命令などと同じアトミック命令の一種で、PowerPCアーキテクチャーではll/sc命令が実装されています。 ll命令はDMAキューに入らず即座に実行されます。
(4) ll命令の実行確認
s = spu_readch (MFC_RdAtomicStat); // (4)
通常のDMA転送命令の終了確認と異なりll命令の実行完了はMFC_RdAtomicStatチャネルを読むことで確認します。 このアトミックステータスの読み込みは必須です。必ず呼び出してください。 ll命令が成功した場合 (1<<2) が返ってきます。
(5) ミューテックスの値を確認し0なら再試行します
if (!mutex) // (5) goto retry;
ミューテックスの値が0ならば、このミューテックスは他のプロセスによってロックされています。 従って再度ミューテックスの値が1になるまでll命令を繰り返さなければいけません。 スピンロックする場合には実戦的にはバックオフ戦略に従って多少の遅延を付けるべきですが、ここでは簡単のためすぐにミューテックスの再ロードにいっています。 詳しく知りたいときは David E. Cullerの著書 "Parallel Computer Architecture: A Hardware/Software Approach"をご覧ください。
なお、Lock Line Reservation Lostイベントの通知機構を使って、 再試行前に誰かがロックラインを失うまでストールして待つ方法もあります。
(6) ミューテックスの値をデクリメントします
mutex -= 1; // (6)
このSPEはクリティカルリージョンに入りミューテックスをロックします。
(7) ミューテックスの値をsc命令でストアします
spu_mfcdma32 (&mutex, ea_mutex, 128, 0, MFC_PUTLLC_CMD); // (7)
ここでsc命令でミューテックスの値をメインメモリに書き戻します。 このDMA転送はll命令の後に他のSPEがミューテックスを書き換えていない限り成功します。
(8) sc命令の完了を確認します
} while (spu_readch (MFC_RdAtomicStat)); // (8)
アトミックステータスチャネルを読みsc命令が成功したかどうかを判定します。 成功したらビット0に1が、失敗したら0が返ってきます。 従って失敗したときは他のSPEと競合したので(3)のll命令からやり直します。 ここでは一連の手順を成功するまで繰り返すために do {} while (spu_readch(...)) で囲んでいます。
(9) クリティカルリージョン
printf ("spe: critical region\n"); // (9)
busy_loop (10000);
(3)〜(8)の手順によってミューテックスを排他的に所持し、ここはただ一つのSPEが入れるクリティカルリージョンです。
(10)〜(14) ミューテックスの開放
(省略)
ここは (3)〜(8)までの手順とほぼ同じなため省略します。 唯一の違いはミューテックスを-1するのではなく、開放するために+1しています。
実行結果
$ ./ppe.out Sarah: setup 5 spe threads main end Sarah: waiting all spe threads end... spe: critical region spe: critical region spe: critical region spe: critical region spe: critical region Sarah: delete all spe threads
約1秒ごとにspeがクリティカル領域に入り"spe: critical region"と表示して終了します。 適切に排他制御が行われた結果、全てのSPEが直列に実行されたことに注目してください。
