2014年5月3日土曜日

[C++]メモリ確保済みタスクシステム

頭の整理のためのメモ書きとして記そうとしたものの、やはり大長編になってしまった。
せっかく書いたのに消すのももったいないので残すことにするが、内容も結論もなんともつまらないし、誤りも多々含まれていると思われるので、何言ってんだこいつと思いながら読んだほうがいい。
また、後日記事の内容が変わる可能性もある。


■ タスクシステム
昔、C/C++とDirect Xをひと通り覚えて何かゲームでも作ってみるかと思った時、『弾幕 最強のシューティングゲームを作る!』(松浦健一郎・司ゆき, 2009)という本を購入してSTGゲームの作り方を学んだ。
この本の想定読者層は、まさに当時の私のようなC/C++プログラミングが書けるゲームプログラミング初心者を想定しているようで、ゲームプログラミングの入門書として大変参考になった。(ただし、この本ではSTGゲーム部分のみを扱っているため、この本だけでゲーム単品を作るのは難しいとも思う。)

そして、この本で私はタスクシステムを知った。
タスクシステムについての説明は偉大な先人方を参照されたい。次のサイトには大変お世話になった。

タスクシステムの解説は次のサイトが大変実用的にまとめている。
タスクシステム解説 - Ideals and Reality http://d.hatena.ne.jp/alwei/20110117/1295290033

実際の簡単な実装と運用は、次のサイトがわかり易く解説している。
TNKソフトウェア - 11:タスク処理を実装する http://www.tnksoft.com/reading/classgame/engine/01/011.php

また、実用的なタスクシステムの実装には次のサイトが役に立つかもしれない。
近代的タスクシステムの構築 http://d.hatena.ne.jp/yaneurao/20090203


■ メモリ確保済みタスクシステム
昔はいさ知らず、現代のC/C++ではその利便性からSTLでタスクシステムを実装することが多いと思う。特にPCゲームでは、メモリや速度などの面を見ても何ら問題は無いだろう。
しかし、本で紹介されていたタスクシステムは少し変わった実装をしていた。
タスクの最大サイズと最大数を指定して、その分のメモリを予め確保するというものだ。
著書の文中では単にタスクシステムとしか書いてなかったため、これを勝手に「メモリ確保済みタスクシステム(MATS: Memory Allocated Task System)」と呼ぶ。

MATSは、内部でアクティブリストとフリーリストの2つのリストを持ち、初期化した時にはフリーリストに全てのタスク(の入れ物)を保持しておく。
タスクを追加する際は、フリーリストからタスクの入れ物を取り出してその中にタスクを格納し、アクティブリストに加える(fig.1)。
格納したタスクを得るときは、アクティブリストの要素を順に返すというわけだ。
タスクを削除するときはアクティブリストからフリーリストへタスクを戻す。
fig.1 メモリ確保済みタスクシステムへのオブジェクト(TaskA)の格納
さて、実際のゲームではタスククラスから派生させた様々なオブジェクトをタスクリストに格納する。当然、オブジェクトのサイズにばらつきが出てくる。このタスクリストでは、格納するオブジェクトのサイズのうち最も大きなサイズをタスクのサイズとしているため、それ以外のサイズのオブジェクトをタスク内に格納するとその差分の無駄メモリが生じる。これが数百個ともなれば大きなサイズの無駄メモリが生じる。

なぜ、一番大きなサイズに合わせてメモリを確保するのだろうか。
それは本の内容と関係がある。

STG(特に弾幕系と呼ばれるもの)では弾を秒間何百回と生成、削除する場面がありうる。もし弾をstd::listなどで管理していた場合、生成、削除が繰り返されるうちにどんどんメモリが分断されてメモリ枯渇が起こる可能性がある。そのため、タスクシステムにデフラグの機能が必要な場合がある。(もっとも、2Dゲームでは現代のPCでメモリが枯渇するような場面はまず無いように思う。)
一方、予めメモリを確保するこのタスクシステムではメモリ分断の恐れは無い。幾らタスクを生成、削除しても、予め確保した分のメモリしか使わない。デフラグも不要だ。

このような立場からみると、一見無駄が多いように見えるこのMATSは実はなかなかスマートな実装だといえる。

ただし、このMATSを実際に運用するには課題も多い。
基本的に特定のタスクを探し出すには線型探索となり、スマートポインタも使えない。そして当然、使い方を誤れば必要以上にメモリを食い潰してしまう。

利用方法は限定されるが、しかし運用方法でカバーできる部分も多く、有用な場面はあると思う。


■ 書籍のMATSの厳しすぎる制約
ここからが私が記録に残したかったことである。
つい先ほど有用な場面はあると書いたばかりだが、この本で実装されたMATSはそのまま実用できるような代物ではない。
それについて記すために、まず著書でどのように実装されているかを説明する必要がある。

本では、タスクとリストを次のように定義している。(この記事の説明に必要な部分だけ記した)

class Task
{
private:
 Task *Prev, Next;
  
 // デフォルトのnew/delete演算子を無効
 void* operator new(size_t t) {}
 void operator delete(void* p) {}
  
public:
 Task(TaskList* list);
 virtual ~Task();
};
 
class TaskList
{
private:
 // タスクリストが使う全メモリ領域
 char* Buffer;
  
 // タスクの最大サイズと最大数
 size_t TaskSize;
 size_t TaskCount;
  
 Task* ActiceTask;
 Task* FreeTask;
  
public:
 TaskList(size_t size, size_t count);
 ~TaskList();
  
 void* New(size_t size);
 void Delete(void* p);
};

そしてこれらの主な実装は次のようになっている。

Task::Task(MallocedTaskList* list)
{
 Prev = list->ActiveTask->Prev;
 Next = list->ActiveTask;
 Prev->Next = Next->Prev = this;
}
 
Task::~Task()
{
 Prev->Next = Next;
 Next->Prev = Prev;
}
 
void* TaskList::New(size_t size)
{
 assert(size <= TaskSize);
 if(FreeTask->Next == FreeTask)
 {
  return NULL;
 }
 
 Task* task = FreeTask->Next;
 FreeTask->Next = task->Next;
 return task;
}
 
void TaskList::Delete(void* p)
{
 Task* task = (Task*)p;
 assert(task->Prev != NULL);
  
 task->Prev = NULL;
 task->Next = FreeTask->Next;
 FreeTask->Next = task;
}

注目すべきはタスクのコンストラクタ・デストラクタとタスクリストのNew・Delete関数である。
タスクを追加するとき、まずタスクリストのNew関数を呼び出しフリーリストからタスクをもらう。
そのタスクの領域にてタスクのコンストラクタを呼び出し、タスクをタスクリストのアクティブリストに追加する。タスクを削除するときはこれを逆順で行う。

しかし、この一連の手順は上記の実装では実現できない。タスクリストのNew関数で貰ったタスク領域にてタスクのコンストラクタを呼び出す手続きが実装されていないためだ。
そして著書では、さらに次のように実装を行った。

// グローバル変数
TaskList* MoverList;
 
class Mover : public Task
{
public:
 void* operator new(size_t n)
 {
  return MoverList->New(n);
 }
  
 void operator delete(void* p)
 {
  MoverList->Delete(p);
 }
  
 Mover();
};

まさかの実体参照である。
確かにこれで一連の手順は実装された。次のようにしてタスクリストへ追加・削除することができる。

Mover* mover = new Mover();
delete mover;

しかしその結果、タスクは決められたタスクリストに強制的に格納される。タスクリストもグローバル変数として管理するほかなく、生存範囲が広すぎて大変に手間がかかる。

予めプログラム全体のクラス設計がしっかり出来ていればともかく、世の中のプログラムは悉く途中で設計が変わる。そうしたプログラムを書きながらの設計では、このタスクシステムは実際に使うにはとても制限が厳しすぎる。


■ なぜこんなことになったのか

結論からいうと、New関数とタスクの生成を橋渡しするためには、通常のC/C++の文法ではこう記述するほかないからである。

New関数内部で一連の手続きを完結させることは難しい。
テンプレートを使えばタスクリストからタスクの実体である派生タスクのコンストラクタを呼ぶことができる。ただし、派生タスクのコンストラクタに渡したい引数の数、種類が常に同じとはならないだろう。すると、そのパターンの数だけ分岐やNew関数のオーバーライドが生じる。どんなマゾヒストでもこの分岐だらけのソースコードを管理したくはないだろう。
そもそも、これではポリモーフィズムが完全に失われている。タスクリストがタスクの実体を把握する必要はないし、そんな必要は無いほうがいい。

別の記述方法として、タスクのインスタンスをどこかで生成してからタスクリストへpushするといった方法もあるだろう。その場合、MATSは実体をリスト内のメモリに格納するため、pushする度にインスタンスをディープコピーする必要がある。
毎秒100個のインスタンスが生成・消滅するようなゲームでこのメモリコピーのオーバーヘッドがどれほど影響するかはわからない。また、リストに加えるためだけに同じインスタンスが2つ生成されるとなると、バグの温床になりかねない。

そして私がたどり着いた結論は、new演算子に引数としてタスクリストを渡す手法だ。


■ placement new のオーバーロード

placement new/delete(配置new/delete)は大抵の場合使わないほうが良いとされる。確かに周知された機能ではないだろうが、知っておいて損はないと思う。(New演算子 - Wikipedia

今回注目すべきは、このplacement new をオーバーロードして利用する際、その引数は任意に取れる点である。
つまり、今回の場合はこのようにすれば上記の問題が解決する。

class Task
{
public:
 void* operator new(size_t n, TaskList* list)
 {
  return list->New(n);
 }
};

リストに対してタスクを追加するときは次のように記述する。

TaskList list(MAX_SIZE, MAX_COUNT);
Task* task = new(&list) Task(&list);

これで任意のタスクリストにタスクを追加することができる。なんと簡単な方法だろう。(newとコンストラクタそれぞれに同じ引数を指定しなければいけないのが少し気持ち悪いが)

ただし、ここで更に問題が生じる。placement newを呼び出すことはできるが、placement deleteを呼び出す事はできない。すなわち、placement deleteを次のように定義してもこれは動作しない。

class Task
{
public:
 void* operator delete(void* p, TaskList* list)
 {
  return list->Delete(p);
 }
};

delete(&list) task; // 動作しない

解決方法としては、iteratorを作成してそこから list.erase(iterator) などとして操作するのがいいと思う。もちろん、これよりベターな方法はあると思う。
iteratorの実装については実装例を見てもらいたい。

この実装の問題点は、独自ルールすぎることである。
あまり認知度の高くないplacement newをさらにオーバーロードしてタスクリストを引数に取るようになっておりdelete演算子は使えないなど、書いた本人以外にだれがわかるものか。

結局、これを利用する機会はごく限られているだろう。
すなわち、生成・削除を大量に繰り返すタスク(とそれを管理するリスト)を一人で実装する場合である。


最後に、今回のMATSの実装例を載せておく。

MallocedTaskSystem

あの本を読んでプログラムを組もうとしてこのタスクシステムの制約に苦しんだ人がいれば、何かの参考になる可能性があるかもしれない。


まったく頭のなかの整理は追い付いていないが、とりあえず文章として残しておくのはここまで。

2014年4月23日水曜日

わたる&おーみんのだらだら呑んで食べるだけ雑記#1


【前回までのあらすじ】
数々の試練を乗り越えついに最終面接までたどり着いたオーミンだったが、それはワタルの卑劣な罠だった。「ネットに履歴書と昔のプロフィール帳(4年分)をばら撒くぞ」と脅され、「それはゴーストライターが書いたもの」と反論するも、実験ノートがないことを指摘されついに絶体絶命の危機に!しかしオーミンはワタルが化粧品会社会長に支援を求めるメールを見つけて・・・



なぜ日本では乾杯がビールなんだろう。と思って調べたらすぐに答えが出てきた[1][2]
簡潔にまとめると、昭和30年~40年代の高度経済成長期に最も乾杯に適したお酒として広まったということだろう。
しかし半世紀も昔の文化は、もはや今の風潮には合わないのではないか?
つまり、私はビールがそこまで好きじゃないのだ。

しかし、世の中にはビールを旨い旨いといって呑む人たちが大勢いる。
私がコーヒーを好むように、経験と知識によってそう認識するのだと思う。
だとすると、まだまだ私はビールを呑む経験が浅いということになる。

否、断じて否である。
浴びるほど呑んだとまでは言わないが、最も多く呑んだ酒は他でもないビールで間違いない。
同時に最も吐き出した酒でもある。
しかし(だから?)、ビールはマイ酒類ランキングにおいていい結果を残せていない。
これは一体どういうことだろう。まだまだ経験が浅いというのか。実は世の人々は毎日欠かさずビール2リットルを呑み干しているのだろうか。それが普通だという人がいるのは知ってるが。

そんなことを考えていた時分、ある「地ビール」専門店の存在を知った。
なんでも、ビールが苦手な人でもするするいけちゃう美味しさだとか。
大変興味を惹かれる感想ではないか。

大変前置きが長くなったが、そんなわけで飯田橋にある「クラフトビア サーバーランド」にやって来たのだ。


因みに高橋は日本酒やジンベースのカクテルをよく呑むが、別段好き嫌いは無いようで、大抵なんでも美味しいといって呑む。便利である。
しかし、奇を衒ったものに眼がないうえにしばしば自滅する。



その店は隠れ家のように雑居ビルの地下一階に構える。
雑居ビルの1階は松屋で、2階はカラオケになっている。
はっきり言って、この地下に地ビールを楽しむ空間があるようには到底思えない。
しかし、松屋の前に控えめに置かれたメニューの掲示ボードがそこにあることを控えめに教えてくれる。

細い階段を降りて店に入ったのは平日の19時前。まだ客は一人もおらず、その雰囲気を堪能することができた。
店内は明るい色調ながら落ち着いた雰囲気。入り口からは想像できない30人くらいは入れると思える広さにまず驚く。
細長い店内の奥がカウンターとなっており、その奥の壁にはビアサーバーの蛇口がずらっと並んでいる。
実際にここからビールが注がれ、それを見ているだけでもワクワクしてくる。

日本各地の地ビールを味わえると謳うだけあり、その数は豊富だ。時々によるとのことだが、ざっと20種類近くはある。
「迷ったらコレ!」「軽くて飲みやすい!」といった分かりやすい種類分けがされているのも嬉しいポイント。

私が最初の一杯に選んだのは地元、栃木の地ビール「プレストンエール ペールエール」。
ぐいっと一口目で、衝撃を受けた。
「旨い」と思わず声を出した。
ビール特有の強い苦味は殆ど感じない。むしろ、スッキリとした麦芽の香りが口の中を満たす。
泡も非常に細かく、舌触り、喉越しに爽快感がある。
ビールでありながらビールのイメージを壊す、真のビール。これが地ビールか!!
洒落たグラスがまた一層ビールを際立たせる。

高橋が最初の一杯に選んだのは北海道の「のぼりべつ地ビール フランボワーズ」。
呑んだ後の高橋の第一声は「すごくフルーティ」だった。
一口呑んで、その意味がわかった。本当にさわやかな風味が口に広がるのだ。まるでシャンパンのように!
それもそのはず、このビールには名前の通りフランボワーズ(木苺)が入っている。(なので厳密にはビールではない)
しかし、フルーティであれどその味、その喉越しは紛れも無くビール。そして旨い。
シャンパンのように飲めるこのビールにただただ感嘆するばかりだった。
特にビールが苦手な女性などはこのビールをとても気に入ると思う。

料理も逸品が揃っていた。
特に舌鼓を打ったのは「牛肉とニラの落とし焼き」。
見た目は一口大のニラ入りハンバーグで、好みでタレとからしをつけて食べる。
その小さな見た目に反して味は非常にジューシーで、牛の肉汁が口いっぱいに広がる。
噛むと味わい広がるニラのアクセントが絶妙に絡み合う。
これがビールにベストマッチ。どちらも相乗効果でより美味しく楽しめる。

その他のメニューも創意工夫が随所に散りばめられた満足度の高い品々ばかりで、ハズレは無かった。

地ビールの種類と美味しさ、料理の満足度と並んで、この店の高評価な点がある。
そのコストパフォーマンスの高さだ。
地ビールはどれでもグラス1杯500円、料理も値段に見合った量で出てくる。
自然と心と財布の紐が緩んでしまう。

この日は2人で4杯ずつ呑んだ。グラス1杯250mlなので、1リットルずつ呑んだことになる。
普段はビールをそのくらい呑むと若干グロッキーになるのだが、この時は充実感に満たされていた。
ビールは幸せになれる飲み物だ。



久々の食べ歩きだけあって、積もる話もいろいろあった。
個人的に最近(いまさら)後悔している学生コーチの件の認識を共有できたのは思いの外嬉しかった。

コミュニケーションが持て囃される現代だが、私はミーティングやプレゼンテーションなどよりも、くだらない話こそ重要だと思っている。(かなり誇張している)
どうでもいい話でこそ、相手を知ることができる。
この日もくだらない話ばかりの有意義な時間だった。



今回のお店はこちら。
『CRAFT BEER SERVER LAND』
(食べログ) http://tabelog.com/tokyo/A1309/A130905/13160783/

また、文中で紹介した地ビールはこちら。
『プレストンエール』
http://item.rakuten.co.jp/ac-fan/c/0000000132/

『のぼりべつ地ビール』
http://www.wakasaimo.com/onidensetsu/index.html



次 #2


[1] http://r25.yahoo.co.jp/fushigi/rxr_detail/?id=20090302-90006392-r25
[2] http://www.nomooo.jp/blog/?p=24501