レベル: 中級 土居 意弘 (munepi@jp.ibm.com), IBM Systems & Technology Group, AKD48
2009年 03月 06日 Accelerated Library Framework (ALF) の新しいタスクモデルである、軽量タスクサポート (Lightweight task support, LTS) が SDK 3.1 から加わりました。本記事では、Cell Broadband Engine (Cell/B.E.) のヘテロジニアスマルチコア環境に役立つ、 ALF 軽量タスクサポートのタスク切り替え機能について使い方を解説します。
はじめに
前回は、Accelerated Library Framework (ALF) の新しいタスクモデルである、軽量タスクサポート (Lightweight task support, LTS) について、基本的な使い方とオーバーヘッド時間についての簡単な調査を行いました。本記事では、タスクの切り替えについて、使い方の解説とパフォーマンスの考察を行います。
切り替え機能の概要
アプリケーションは単一のタスクで完結するものばかりではありません。複数のタスクを次々と実行していくことも多いでしょう。
異なるタスクを続けて実行する場合には、なんらかのコマンドディスパッチ機構があると便利です。メールボックスによる機構を自作したり、 SPE のプログラムを変更する仕組みなどを自作した経験はありませんか?
ALF LTS では、このタスク切り替えを支援するための機能を、 libspe2 の API だけでは実現できない様々なレベルで提供しています。
ALF LTS で提供しているタスクの切り替え機能には次の 3 つの単位があります(図 1 )。
- タスクメイン関数単位の切り替え
- プログラム単位の切り替え
- 共有ライブラリ単位の切り替え
図 1. ALF が提供しているタスク切り替え機能
それぞれについて詳しく見ていきましょう。
タスクメイン関数単位の切り替え
LTS では、ある SPE 関数をタスクのエントリーポイント(タスクメイン関数)として指定します。ちょうど pthread のスレッド関数を指定することと似ています。タスクメイン関数を異なる名前で複数用意しておけば、タスク実行毎に切り替えることができます。
一旦 SPE にプログラムがロードされてしまえば、次からはこのプログラムが再利用されるため、切り替え時間は最も高速になります。しかし、各タスクメイン関数は同じ SPE プログラムに含まれているので、ローカルストア (Local Store, LS) を多く消費します。
なお、切り替えの際、実行コンテキストはリセットされますが、グローバル宣言したデータ領域はその内容も残ります(図 2 )。この副作用を、連続した関数の実行でデータを受け渡すためのトリックとして使うこともできそうです。
図 2. タスクメイン単位の切り替えのイメージと副作用
プログラム単位の切り替え
同じ共有ライブラリに複数の SPE プログラムを含めておくことで、プログラム単位の切り替えもできます。
プログラムのリロードが必要になるので切り替え時間は多くかかりますが、プログラムごとに LS を専有できるメリットがあります。もちろん、各プログラムが複数のタスクメイン関数を持つこともできます。
また、各プログラムが同じ名前のタスクメイン関数を持つこともできます。各プログラムでタスクメイン関数にルールを設けておくことで、プラグイン型のライブラリ構築に便利に使えそうです。
共有ライブラリ単位の切り替え
共有ライブラリを切り替えることもできます。この場合もプログラムのリロードが必要になるので、切り替え時間は多くかかります。さらに、共有ライブラリをディスクから読み込む時間も必要です。
前節でプラグイン型ライブラリの構築について触れましたが、実際には共有ライブラリをプラグインの単位とするほうがより実用的です。タスクメイン関数とプログラム名の両方にルールを設けておき、共有ライブラリ単位での切り替えを行うのがよいでしょう。
使い方
切り替え機能を使うために必要なコード変更はほんのわずかで、とても直感的なものです。
タスクメイン関数単位の切り替え
まずは、タスクメイン関数単位の切り替えから見ていきましょう。
アクセラレータのプログラムに、複数のタスクメイン関数を作成します(リスト 1 )。
4 ~ 13 行目で task_main_1() を、 15 ~ 24 行目で task_main_2() を作成しています。そして、 26 ~ 29 行目でタスクメイン関数をエクスポートします。 2 つのタスクメイン関数がありますので、 ALF_ACCEL_EXPORT_API() 文が 2 行あります。
リスト 1. 複数のタスクメイン関数持つアクセラレータ用プログラム (spu/spu_prog_1.c)
001: #include <stdio.h>
002: #include <alf_accel.h>
003:
004: int task_main_1(
005: void* p_task_ctx,
006: int instance_id,
007: int number_of_instance)
008: {
009: printf("[%u/%u] Hello ALF LTS World !!\n",
010: instance_id,
011: number_of_instance);
012: return 0;
013: }
014:
015: int task_main_2(
016: void* p_task_ctx,
017: int instance_id,
018: int number_of_instance)
019: {
020: printf("[%u/%u] Jambo ALF LTS World !!\n",
021: instance_id,
022: number_of_instance);
023: return 0;
024: }
025:
026: ALF_ACCEL_EXPORT_API_LIST_BEGIN;
027: ALF_ACCEL_EXPORT_API("", task_main_1);
028: ALF_ACCEL_EXPORT_API("", task_main_2);
029: ALF_ACCEL_EXPORT_API_LIST_END;
|
次はホスト側のプログラムです(リスト 2 )。こちらも特に難しいことはありません。タスクデスクリプタに、タスクメイン関数、プログラム名、共有ライブラリ名のそれぞれを設定するだけです。
14 行目~ 29 行目でタスク 1 を実行しています。タスク 1 の終了を待って (31 行目)、既存のタスクデスクリプタを再利用して別のタスクメイン関数を指定しタスク 2 を実行しています (34 行目~ 41 行目) 。タスクメイン関数の名前は、アクセラレータ側でエクスポートされたものと一致していなければなりません。
プログラム名、共有ライブラリ名を変更する場合もほぼ同様です。複数の設定を一度に書き換えても構いません。
ここではタスクデスクリプタを再利用しましたが、作り直しても構いません。
リスト 2. タスクメイン関数を切り替えて実行する場合のホスト用プログラム (main.c)
001: #include <stdlib.h> // for NULL
002: #include <alf.h> // for alf_xxxx()
003:
004: int main(int argc, char *argv[])
005: {
006: alf_handle_t handle;
007: alf_task_desc_handle_t task_desc_handle;
008: alf_task_handle_t task_handle;
009:
010: alf_init(NULL, &handle);
011: alf_num_instances_set(handle, 8);
012:
013: // task 1
014: alf_task_desc_create(handle, 0, &task_desc_handle);
015: alf_task_desc_set_int32(task_desc_handle,
016: ALF_TASK_DESC_TASK_TYPE,
017: ALF_TASK_TYPE_LIGHTWEIGHT);
018: alf_task_desc_set_int64(task_desc_handle,
019: ALF_TASK_DESC_ACCEL_LIBRARY_REF_L,
020: (unsigned long long) "libspu_library_1.so");
021: alf_task_desc_set_int64(task_desc_handle,
022: ALF_TASK_DESC_ACCEL_IMAGE_REF_L,
023: (unsigned long long) "spu_prog_1");
024: alf_task_desc_set_int64(task_desc_handle,
025: ALF_TASK_DESC_ACCEL_LTS_MAIN_REF_L,
026: (unsigned long long) "task_main_1");
027:
028: alf_task_create(task_desc_handle, NULL, 8, 0, 0, &task_handle);
029: alf_task_finalize(task_handle);
030:
031: alf_task_wait(task_handle, -1);
032:
033: // task 2
034: alf_task_desc_set_int64(task_desc_handle,
035: ALF_TASK_DESC_ACCEL_LTS_MAIN_REF_L,
036: (unsigned long long) "task_main_2");
037:
038: alf_task_create(task_desc_handle, NULL, 8, 0, 0, &task_handle);
039: alf_task_finalize(task_handle);
040:
041: alf_task_wait(task_handle, -1);
042:
043: // clean up
044: alf_exit(handle, ALF_EXIT_POLICY_FORCE, 0);
045:
046: return 0;
047: }
|
プログラム単位の切り替え
次は、プログラム単位の切り替えです。
複数のアクセラレータプログラムを作り、同じ共有ライブラリの中に含めます。ここでは、 spu_prog_1 と spu_prog_2 という 2 つのプログラムを含む、 libspu_library_1.so という共有ライブラリを作ってみましょう。
まず、プログラムを複数作る場合は、SDK の Makefile で使われている PROGRAMS_spu 文を使うと便利です。ここにプログラム名を複数指定すると、同名の C/C++ ソースコードからそれぞれプログラムを作成してくれます。例えば、 spu_prog_1 と spu_prog_2 という 2 つのプログラムを作りたい場合、 PROGRAMS_spu 文にそれらを追加し、 spu_prog_1.c と spu_prog_2.c を同じディレクトリに用意しておけばよいのです。
ここでは、リスト 1 をコピーして spu_prog_2.c として保存しましょう。 spu_prog_1.c と spu_prog_2.c は全く同じ内容ということになりますが、それで構いません。区別したければ挨拶文を変えておくとよいでしょう。
Makefile をリスト 3 に示します。
リスト 3. 複数のアクセラレータプログラムを持つ共有ライブラリを作成する場合の Makefile (spu/Makefile)
001: PROGRAMS_spu = spu_prog_1 spu_prog_2
002: SHARED_LIBRARY_embed = libspu_library_1.so
003: IMPORTS = -lalflts
004: INSTALL_DIR = ..
005: INSTALL_FILES = $(SHARED_LIBRARY_embed)
006: include $(CELL_TOP)/buildutils/make.footer
|
ホスト側は、変更する場所がプログラム名になるほかはタスクメイン単位の場合と同じですので省略します。
共有ライブラリ単位の切り替え
複数の共有ライブラリを使う場合には、アクセラレータ側に特別なコード変更はありません。今まで説明してきた方法を応用して、複数の共有ライブラリを異なる名前で作成し、ホスト側で指定するだけです。
タスク切り替えのからくり
ところで、 ALF はどのように SPU のタスクメイン関数を呼び分けているのでしょうか?
しかも、 図 1 では task_main_1 という名前の関数が spu_prog_1 にも spu_prog_2 にも定義されていて、同じ libspu_library_1.so という共有ライブラリに含まれているようにみえます。同じライブラリに、同じ関数名が存在していて、 ALF はどのようにそれらを区別しているのでしょうか?
ALF の内部構造は公開されていませんが、推測してみたいと思います。
少し難しいので、飛ばして次の処理時間の解説へ進んでも構いません。
PPU から SPU の関数は呼べない
まず、 ALF 使用の有無に限らず、 PPU のプログラムは SPU のプログラムの識別子、たとえば関数などを直接参照することはできません。例え知ることができても、 PPU と SPU は別々のプロセッサですので、関数呼び出しとして実行することはできません。
また、 SPU プログラムは、 PPU プログラムまたは共有ライブラリに一旦埋め込まれてしまうと、 PPU にとってはデータセクションとして扱われます。図 3 に示した nm コマンドの実行結果を見ても、 spu_prog_1 や spu_prog_2 は D のマークが示されていることで確認できます。 PPU にとっては、 SPU プログラムは LS というメモリ空間に送りこむべきデータブロックという扱いなのです。
ですから、 PPU プログラムから直接実行できるのは、 main() のみです ( 注 ) 。
( 注 ) SPU アセンブラを使うとできるのですが、ここでは言及しません。
図 3. libspu_library_1.so でエクスポートされている識別子
SPU 関数を PPU に対してエクスポートするには
しかし、同じ D マークのついた識別子の中に、 spu_prog_1_task_main_1 や spu_prog_1_task_main_2 と言った、いかにも怪しい名前があります。
実はこれらのプログラム名_タスクメイン関数名という名前の付いた識別子こそ、まさに ALF_ACCEL_EXPORT_API() で定義していたものなのです。
spu_prog_1 がどこでくっついたのか、気になりませんか?
厳密には、 ALF_ACCEL_EXPORT_API("", task_main_1)
によって定義されるのは、 _SPUEAR_task_main_1 という識別子です。
そして、 _SPUEAR_ のついた識別子は、
ppu-embedspu
ツールによって CESOF に変換される際、 PPU から参照できる形に自動的にエクスポートされることになっており、このときに _SPUEAR_ という部分がプログラム名、ここでは spu_prog_1 に置き換わったのです。
さて、この spu_prog_1_task_main_1 という識別子を PPU から参照すると何が入っているのでしょうか。
この識別子は 4 バイトのデータを指しており、 task_main_1() の LS 上でのアドレスが入っています。
からくりを解く
いよいよ見えてきましたね。
PPU から SPU のタスクメイン関数を直接呼ぶことはできませんが、タスクメイン関数のアドレスは知ることができました。このアドレスを SPU 起動時に引数として渡せば、タスクメイン関数を指定できそうです。
実際、 SPU 用の ALF LTS ライブラリには main() が予め含まれており、ユーザは指定することができません。この定義済み main() が、 PPU から受け取った関数アドレスを使って、タスクメイン関数呼び分けをしているものと推測されます。
同じ関数名が、同じ共有ライブラリに存在していても問題ないこともわかりましたね。
関数名はプログラムごとに別々の名前空間ですから互いに干渉することはありません。タスクメイン関数のエクスポートは、自動的にプログラム名 _ タスクメイン関数名という識別子に変更されることで区別されています。
図 4. タスクメイン関数を呼び分けるからくり
処理時間
では、今回も処理時間を調べてみましょう。
100 回連続切り替え
タスク 1 、 タスク 2 、 タスク 1 、タスク 2 、 ... と切り替えを行いながら各 50 回、合計 100 回連続して呼び出した場合の各回の実行時間を図 5 に示します。左からタスクメイン関数単位(青の背景)、プログラム単位(黄)、共有ライブラリ単位(緑)のグラフです。 INTERVAL は 100 マイクロ秒で、各グラフの縦軸は INTERVAL に合わせて 100 マイクロ秒から始まっています。
図 5. 100 回連続実行の実行時間(左から、タスクメイン関数単位、プログラム単位、共有ライブラリ単位)
拡大図はこちら
タスクメイン関数単位の切り替えは、前回の同じタスクメイン関数の連続実行と同じ数値とみてよいでしょう。これに比べて、プログラム単位または共有ライブラリ単位の切り替えはかなり大きな値になっています。
もう一つ、プログラム単位または共有ライブラリ単位の切り替えでは、時々非常に大きな値が現れるという特徴があります。これら 2 つの切り替え方法ではシステムメモリまたはディスクへのアクセスを必要とすることが影響していると考えられます。
SPE 個数による実行時間の違い
次に、SPE 個数による実行時間の違いを図 6 に示します。色分けは同様です。赤い四角は初回と 2 回目を除く 98 回の平均値を表しており、青い棒は最大値と最小値の広がりを表しています。 2 種類のタスクがありますので、それぞれの初回を除いています。
図 6. SPE 個数による実行時間の変化(左から、タスクメイン関数単位、プログラム単位、共有ライブラリ単位)
拡大図はこちら
いずれの場合も SPE 個数に応じて増えています。また、プログラム単位または共有ライブラリ単位の切り替えは、切片も傾きもタスクメイン関数単位に比べて非常に大きい値になっています。
プログラム単位または共有ライブラリ単位の 2 つで見てみると、散発的なばらつきの影響で平均値が乱れている部分があるものの、傾きがほぼ同じで切片は共有ライブラリ単位のほうが 40 マイクロ秒ほど大きいと読み取れます。
さらに調べてみましょう。
実行時間のばらつき
図 7 は、 INTERVAL を 1, 10, 100, 1000, 10000 マイクロ秒と変化させてプログラム単位の切り替え時間を調べたものです。
図 7. INTERVAL を変化させた場合の実行時間(プログラム単位の切り替え)
拡大図はこちら
2 つのことが読み取れます。 1 つ目は、 INTERVAL が小さい方がばらつきやすくなること。 2 つ目は、 SPE 個数が多い方がばらつきやすくなることです。そういえば図 6 でも、プログラム単位と共有ライブラリ単位の場合は SPE 個数が増えると最大値が大きく伸びています。
切り替えにはもともと 350 ~ 1000 マイクロ秒程度の時間がかかります。このため、 INTERVAL を小さくしてオーバーヘッド以下の時間間隔で切り替えようとすると、システムメモリアクセスやディスクアクセスが集中して安定しなくなることが原因であると推測できます。 SPE 個数が多くなった場合もこのアクセス集中を引き起こしやすくなりますので、同じ理由で説明できます。
同様のテストを、タスクメイン単位での切り替えについて行ったものを図 8 に、共有ライブラリ単位での切り替えについて行ったものを図 9 に示しておきます。
図 8. INTERVAL を変化させた場合の実行時間(タスクメイン関数単位の切り替え)
拡大図はこちら
図 9. INTERVAL を変化させた場合の実行時間(共有ライブラリ単位の切り替え)
拡大図はこちら
処理時間の考察
タスクメイン関数単位の切り替えは、同じタスクメイン関数の連続実行とパフォーマンスは変わらないことがわかりました。
プログラム単位の切り替えになると、切り替えの度にプログラムのロードが必要であるため、オーバーヘッドは大きく増加します。このプログラムのロードは必ず SPE それぞれに対して必要で、省略することはできません。 SPE 個数に比例していることとも合致します。
一方、共有ライブラリ単位の切り替えは、プログラムの再ロードに先立って共有ライブラリをディスクから読み込みなおさなければなりません。この分だけプログラム単位の切り替えよりも時間がかかるはずですが、結果はプログラム単位とほとんど変わりません。一定の小さなオーバーヘッドがあるのみで、傾きは同じです。
傾きが変わらないのは、読み込んだ共有ライブラリがうまく再利用されているためと考えられます。特に、今回のテストではすべての SPE に対して一斉に切り替えを行って、同じ共有ライブラリの同じプログラムをロードしています。つまり、実際に必要な共有ライブラリの読み込みは SPE 個数によらず 1 回だけです。これらのことから、プログラム単位と共有ライブラリ単位の差の 40 マイクロ秒が、共有ライブラリの読み込み時間であると推測できます。
まとめ
本記事では、 ALF 軽量タスクサポートのタスク切り替え機能について使い方を紹介し、処理時間の考察を行いました。
次回は
次回は、 コンテキストを使って SPE へデータを渡す方法と、タスクパイプライン、そして共有インスタンスについて説明する予定です。
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | 
|  | 土居意弘はIBMで5年間、Cell/B.E. や Digital Signal Processor 上で画像処理システムなどを開発してきました。現在は日本IBMのテクニカルエキスパートとして、 Cell/B.E. 向けソフトウェアの実装やコンサルティング業務などを行っています。 |
記事の評価
|