見出し画像

MGL週報 #39 - 改良型タスクシステムの並列実行の仕組み

このエントリはゲーム開発用フレームワーク「MGL」の開発記録です。MGLはzlibライセンスの下に無償で提供されています。


今週の作業内容

タスクシステムの並列実行改善

気が付いたら着手から2週間も経過していましたが、ようやく片付けました。もちろん、2週間ずっと作業していた訳ではなく、実装方法に悩みながら別の作業を進めていたりしていました。

片付けたと言っても、相変わらず細かい動作検証はできていません。リアルタイムな非同期処理となると実験レベルでは心許無く、実際のゲーム開発に用いてみるまでは安心はできません。ですので、当面はこの機能はベータ版の位置付けで提供する予定です。

また、前回のアップデートに含めた実装は今回の更新で削除してしまう事にしました。実装がややこしくなってしまう事に加え、有効活用できる見込みも少ないためです。理屈上は前回までの仕組みのほうが速度を出せそうな気はしますが、使い勝手を犠牲にするなら他にもっと良い手法がありますからね。


改良型タスクシステムの並列実行の仕組み

そんな訳で、ここ最近悩んでいたタスクシステムの並列実行の仕組みがおおよそ固まったため、どのような機能かをざっくりと紹介することにしましょう。実はこの辺の情報は過去にも週報で書いているのですが、今回は並列処理に関わる部分に特化して一から解説します。

タスクシステムのおさらい

まず、タスクシステムが何なのかを改めて説明しておきましょう。タスクシステムは各々の処理を数珠繋ぎにして、フレーム毎にそれを順に実行するための仕組みです。この仕組みはビデオゲーム黎明期である1970年代後半に考案された手法であると伝えられており、今日ではその実装や活用方法は多種多様に枝分かれしています。

MGLに標準で用意されているタスクシステムは、多くの機能を持たず実行に特化した作りになっています。このような原始的な実装は「古典的タスクシステム」とも呼ばれており、シンプル・イズ・ベターをモットーとするMGLらしい実装と言えるでしょう。

さて、以降の解説はMGLのタスクシステムに特化しましょう。タスクシステムは、各々の処理を表すタスクノードと、それを包括したタスクリストによって構成されています。プレイヤーや敵の挙動、UIの制御、その他のゲーム中に存在するオブジェクトなどはタスクノードとして実装します。それをタスクリストに登録することで毎フレーム実行されるという仕組みです。

タスクリスト上のノードのイメージ図

MGLのタスクシステムの特徴のひとつに、実行順序が厳格であるという点が挙げられます。各々のタスクはタスクIDと呼ばれる値と共に登録され、この値が小さい順に実行されます。また、タスクIDはそのタスクの種類の判別も兼ねており、通常はそのタスクを表す別名を用いて登録します。例えば、プレイヤー、敵、ステータス表示をタスクノードとして実行する場合は次のような定義を行うことになるでしょう。

enum
{
    kTaskPlayer,     // プレイヤー
    kTaskEnemy,      // 敵
    kTaskDrawStatus, // ステータス表示
}

プレイヤーや敵は複数同時に存在する事も有り得ます。そういった場合でも、同じ種類のタスクに対してはタスクIDを用いた一括指定が可能です。もちろん、各々のタスクは種類を表すタスクIDとは別に個別に割り振られるユニークIDも持っているため、ピンポイントで特定のタスクを判別することも可能です。

さて、ここで重要な事を2つ挙げておきましょう。

  1. タスクはタスクIDによって種類分けされている

  2. タスクの実行順序は種類によって実行順序が定められており、IDが小さいほど先に実行される

今回導入した並列実行の機能は、この特徴を活かした実装となっています。

タスクの並列実行

先に述べた「各々のタスクの実行順序が厳格である」のルールは、並列実行においても例外ではありません。今回導入した機能は、端的に言うとタスクの実行完了を待たずに次のタスクを実行する機能です。

通常、タスクは1つづつ実行されます。あるタスクが完了するまでは次のタスクは実行されません。これによってタスクの実行順序は一定に保たれています。

一方で、その必要がない場合もあります。あるタスクの処理結果を次のタスクが参照しないのであれば、わざわざ完了を待つ必要はありません。一方で、今日のCPUの多くは複数のコアを搭載しており、同時に複数の処理を実行できます。これを活用し、すぐに待つ必要のないタスクは別のコアに処理をさせ、完了を待たずに進めてしまおうというのが今回追加する機能です。(※スレッドの仕組みの関係上、状況によっては同じコアで処理される事もありますが、その辺はややこしいので割愛します)

もちろん、すぐには結果を必要としなくても、どこかで必要にはなります。そこで登場するのがタスクIDです。タスクに並列実行を要求する際に、同時にタスクIDを指定することで、そのタスクIDが実行される前に完了の待ち合わせを行うようになります。もしタスクIDを指定しなかった場合でも、全てのタスクの実行を走らせた最後には待ち合わせることになります。

具体的な例として、先に示した図のTaskA、TaskB、TaskCの構成のタスクリストに、次のような前提を定めましょう。

  • TaskAの処理結果はTaskCが参照する

  • TaskBはどのタスクからも参照されず、自身の処理で完結している

この場合、TaskAはTaskC実行前までに待ち合わせれば動作に支障はありません。TaskBについてはどこからも参照されないため、全てのタスクを走らせた最後に待ち合わせることにしましょう。すなわち、このタスクは次のような構成にできます。

タスクノードを並列実行した場合のイメージ図

……ええと、かえってややこしくなった気がしますが、並列処理なんて得てしてややこしいものなので諦めてください。重要なのは、直列に実行していた場合の全体の実行時間は単純な足し算になるのに対し、並列化したことでその一部を省けるという点です。仮に各々のタスクの実行時間を5とした場合、直列に実行すれば30になります。もしこのプログラムが完全に理想的な並列実行が行われた場合、その実行時間は10まで短縮できるはずです。(※実際には理想的な時間になることはありませんが、それは後述)

利点

さて、この並列化の最大のメリットは「手軽さ」にあります。あるタスクを並列化させたい場合は、そのタスクの初期化処理あたりで次のように書くだけです。

SetAsynchronous(true, TaskC);

ここで、TaskCは待ち合わせるタスクIDの指定になります。これを指定しなかった場合は最後に待ち合わせることになります。

これは単に記述が簡単というだけではありません。この仕組みの真の手軽さは、並列処理を意識せずに作ったゲームを後から並列化できる事にあります。

並列処理による高速化の手法は他にもたくさんありますが、その多くは並列処理に特化した作りにする必要があります。今回導入する予定のこの仕組みは、ゲームを作った後からでも、パフォーマンス的に問題のある部分に対してピンポイントで対処できるのが最大の特徴です。

そして、先述の通りMGLのタスクシステムはタスクID単位で実行開始の順序が決められています。この特徴により、「どのタスクが」「どの期間並列に動けるか」を導き出しやすいため、同期を取りやすく並列化の計画も立てやすいと言えます。

「このタスクは時間がかかるし、処理結果もすぐには使わないから並列実行しよう」といった具合に、既存の処理を並列化しやすいのが最大のメリットなのです。

欠点

ある程度コンピュータの仕組みを把握している方が本記事を読んだのであれば、記事中に「ん?」となる箇所があったのではないでしょうか?実のところ、先述の「完全に理想的な並列実行が行われた場合の処理時間が10となる」は理論上あり得ない数値です。何故なら、並列に実行して正しく同期するための処理のコスト、すなわちオーバーヘッドが必ず発生するためです。

これは人に例えると分かりやすいでしょう。1人で1日かかる作業を24人で行うことで、果たして1時間で終わるでしょうか?場合によっては、1人で頑張った方が早い事さえあり得ます。これはコンピュータの世界でも同じなのです。

並列実行を有効化した場合、オーバーヘッドは並列化したタスクのみだけでなく、全体に少しづつ発生します。MGLの実装の場合、実行中のタスクIDが切り替わるタイミングで待ち合わせのチェックを行うため、その度に若干のオーバーヘッドが発生します。この時間が並列化による短縮時間よりも大きくなってしまう場合、残念ながら逆効果となってしまうのです。

この並列実行機能を度々「奥の手」と称していた理由はまさにこの点にあります。手軽に使えるからといって、積極的に使っていきましょうとはいかないのです。リスクを承知のうえで、それを上回るリターンがある場合のみに持ち出す、まさに奥の手なのです。

最後に

ちょっと紹介するつもりが長い解説になってしまいました。次のアップデートと同時に更新する予定のドキュメントでは、タスクシステムの基本的な使い方の解説を新たに含める予定です。並列処理を用いなくても有用な機能となっていますので、興味がありましたらぜひ目を通してみてください。


その他

着手予定のドキュメントをiPadで書きたいと思って試行錯誤してみたのですが、日本語入力を満足に行えるSSHクライアントが見つからずに頓挫してしまいました。悔しいので、今週の週報は無意味にiPadで書いています。

この記事が気に入ったらサポートをしてみませんか?