2014年10月30日木曜日

3次元デバイスメモリ配列から3次元cudaArrayへのコピー方法

そりゃ知っている人には当たり前なんだろうけど、知らない人には分かるわけない。
まあプログラムなんてそんなもの。



CUDAのデバイスの動的メモリ確保の手段は、入門書にはcudaMallocしか書いてないが、サンプルコードではむしろcudaMallocはあまり見かけない。とにかくコピー関数が色々あって、最初に見た時にそれの使い方がわからない。
幸い、最近はCUDAのドキュメントも充実してきたので大体は解決するが、今回のようにドキュメントをよく読まないと見落とすような落とし穴があったりする。
愚痴はここまでにして、今回の落とし穴を記録に残す。



事件はプログラムを3次元に拡張しようと、cudaMemcpy2DToArrayをcudaMemcpy3Dにしようとした時に起こった(cudaMemcpy3DToArrayを用意してればこんなことにはならなかったんじゃないのかNVIDIA)。
元の2次元のコードは次のとおりである。

typedef float2 DataType; 
DataType *data = NULL;
cudaArray *array = NULL;
size_t pitch = 0;

cudaMallocPitch((void **)&data, &pitch, sizeof(DataType)*SIZE_X, SIZE_Y);
cudaChannelFormatDesc desc = cudaCreateChannelDesc<DataType>();
cudaMallocArray(&array, &desc, SIZE_X, SIZE_Y);

cudaMemcpy2DToArray(array, 0, 0, data,
    pitch, SIZE_X*sizeof(DataType), SIZE_Y, cudaMemcpyDeviceToDevice);

このコードの2を3にすればいいんじゃないかという私のような甘い考えだと、このコードを修正するのに一日かかる。

今回の私の場合、arrayはテクスチャメモリにバインドされていて、テクスチャはtex2D関数で参照される。これを3次元にした時、テクスチャを参照するにはtex3D関数を用いるが、tex3D関数の戻り値にfloat3は用意されていない。float4しかない。ないものはしょうがないので、今回は3次元データとしてfloat4を使うことにした。

それぞれの関数に3次元用の関数が用意されている。
2D 3D
cudaMallocPitch cudaMalloc3D
cudaMallocArray cudaMalloc3DArray
cudaMemcpy2DToArray cudaMemcpy3D

cudaMallocArrayにcudaMalloc3DArrayが対応するのはとてもよく分かる。
cudaMalloc3Dは一見どこにも関数名にPitch要素が含まれていないが、確保したメモリのポインタはしっかりcudaPitchedPtrで返してくる。
cudaMemcpy3Dは、もう2次元メモリコピー関数全てを受け入れたというような関数になっている。
cudaMemcpy3Dは、各情報を保持するcudaMemcpy3DParmsという構造体を渡して実行する。

以上を踏まえて、コードを書き換えてみる。

typedef float4 DataType; 
DataType *data = NULL;
cudaArray *array = NULL;
size_t pitch = 0;

cudaMalloc3D((void **)&data,
    make_cudaExtent(SIZE_X*sizeof(DataType), SIZE_Y, SIZE_Z));
pitch = ((cudaPitchedPtr *)&dvfield)->pitch;
cudaChannelFormatDesc desc = cudaCreateChannelDesc<DataType>();
cudaMalloc3DArray(&array, &desc, make_cudaExtent(SIZE_X, SIZE_Y, SIZE_Z));

cudaMemcpy3DParms parms = { 0 };
parms.dstArray = array;
parms.srcPtr = *(cudaPitchedPtr *)&data;
parms.extent = make_cudaExtent(SIZE_X*sizeof(DataType), SIZE_Y, SIZE_Z);
parms.kind = cudaMemcpyDeviceToDevice;

cudaMemcpy3D(parms);

ピッチ数の取得法やcudaPitchedPtr*へのキャストが大変カオスになっているが、型を合わせるにはこうするしかない。綺麗に見せるならmake_cudaPitchedPtrとかするんだろうが、ポインタのキャストのほうが低コストだし、美しさより早さを求めなければCUDAを使う意味が無い。

が、問題はキャストではない。このコードはコンパイルこそ出来るが実行するとcudaMemcpy3Dでエラーを吐く。(cudaErrorInvalidValueとかcudaErrorInvalidPitchValueとか)
ドキュメントを読んでもエラーの原因がさっぱりわからず、CUDAの3Dデータを扱う情報が少ないことで心が折れかけたが、次のサイトが助けになった。
http://stackoverflow.com/questions/9399451/invalid-argument-in-cudamemcpy3d-using-width-in-bytes

CUDA ArrayではX次元のサイズをバイト数ではなく要素数で指定するらしい。確かに、改めてcudaMemcpy3Dの公式ドキュメントを改めて確認すると、If a CUDA array is participating in the copy, the extent is defined in terms of that array's elements.とある。
というわけで、正解はこうなる。

typedef float4 DataType; 
DataType *data = NULL;
cudaArray *array = NULL;
size_t pitch = 0;

cudaMalloc3D((cudaPitchedPtr *)&data,
    make_cudaExtent(SIZE_X*sizeof(DataType), SIZE_Y, SIZE_Z));
pitch = ((cudaPitchedPtr *)&dvfield)->pitch;
cudaChannelFormatDesc desc = cudaCreateChannelDesc<DataType>();
cudaMalloc3DArray(&array, &desc, make_cudaExtent(SIZE_X, SIZE_Y, SIZE_Z));

cudaMemcpy3DParms parms = { 0 };
parms.dstArray = array;
parms.srcPtr = *(cudaPitchedPtr *)&data;
parms.extent = make_cudaExtent(SIZE_X, SIZE_Y, SIZE_Z);
parms.kind = cudaMemcpyDeviceToDevice;

cudaMemcpy3D(parms);

結局、ドキュメントは一行一行しっかり読みましょうという話。

2014年9月25日木曜日

[C#]複製可能読み取り専用リスト

嵌ったのでメモ


タイトルの通り、読み取り専用だが複製(ディープコピーで作成されたインスタンス)を作ることができるリストを作りたかった。
C#様々ならちょろっと書けると思ったら丸一日掛かった。別にC#で作るのが困難だったからではなく、単に知識が著しく不足していたせいだが。


まず、複製可能なListを作るのにいろいろと発見があった。
最初、丁寧に一つ一つforeachでコピーしていくという方法を思い浮かべていた。

public class CopyableList<t> : List<t>
{
    public CopyableList<t> Copy()
    {
        var result = new CopyableList<t>();
        foreach( var data in this )
        {
            result.Add(data);
        }
        return result;
    }
}

これで複製はちゃんとできるしなかなかシンプルだと思ったが、なんというかC#らしさが欠片もない。で、普通はどんな風に実装するのか調べたらこの実装が非常に美しかった。(下に引用)

public class CloneableList<T> : List<T>, ICloneable<CloneableList<T>> where T : ICloneable
{
    public CloneableList<T> Clone()
    {
        var result = new CloneableList<T>();
        result.AddRange(this.Select(item => (T) item.Clone()));
        return result;
    }

    object ICloneable.Clone()
    {
        return ((ICloneable<CloneableList<T>>) this).Clone();
    }
}

public interface ICloneable<T> : ICloneable where T : ICloneable<T>
{
    new T Clone();
}

ジェネリック、LINQなどなど、これぞC#といって文句ないだろう。
本題から少し逸れるが、ICloneableというインターフェースを初めて知ったのでこれもいろいろ調べたが、何とも悩ましい限りのインターフェースだった。
ジェネリック実装前に作成されたインターフェースなので当然ジェネリックは考慮されてない(なので上の人は自分でジェネリック対応インターフェースを定義しているわけだ)。
で、このインターフェースはCloneメソッドを提供しているのだが、この実装がシャローコピーかディープコピーかは実装側の自由となっている[1]。あくまでインスタンスのコピーを作るメソッドということらしい。当然こんなインターフェースを外部に公開出来るはずもなく、APIなどにICloneableを使わないでくれと開発者直々にお達しが出されている[2]


Listを書き込み不可にする場合、AsReadOnlyメソッドで書き換え不可にするんだよなとか、でもそれだと一見普通のListと区別がつかないんだよなとか思ってたら、IReadOnlyListなんてまさに求めているものそのものが存在していた。こんな汎用性の高いインターフェースなんで知らなかったんだと思ったら、.NET 4.5で追加されたインターフェースだった(.NET 4.5が公開されてから既に2年が経っているが)。
IReadOnlyListのItemプロパティは読み取り専用で、AddメソッドやSortメソッドは定義されていない。
これでうっかり読み込み専用にしたListでAddメソッド呼び出して怒られるという悲劇からもおさらばだ。


ということで、複製可能読み取り専用リスト(のインターフェース)は次のように実装できる。

public class CloneableList<T> : List<T>, IReadOnlyCloneableList<T>, ICloneable<CloneableList<T>> where T : ICloneable
{
    public CloneableList<T> Clone()
    {
        var result = new CloneableList<T>();
        result.AddRange(this.Select(item => (T)item.Clone()));
        return result;
    }

    object ICloneable.Clone()
    {
        return ((ICloneable<CloneableList<T>>)this).Clone();
    }
}

public interface ICloneable<T> : ICloneable where T : ICloneable<T>
{
    new T Clone();
}

public interface IReadOnlyCloneableList<T> : IReadOnlyList<T>, ICloneable<CloneableList<T>> where T : ICloneable
{}

もっとスマートな実装が有るんだろうと思うけど、目的は叶ったしちゃんと動くし、全く問題はない。どのみちICloneableをなんとかしないかぎり自分用で使うし。


[1] http://msdn.microsoft.com/library/system.icloneable(v=vs.110).aspx
[2] http://blogs.msdn.com/b/brada/archive/2003/04/09/49935.aspx

2014年9月20日土曜日

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



現代を生きる人々は、鹿を追いかけドングリを拾った頃と比べてあまりに生死の危機感に貧しいため、時に自らスリルを求めて生を実感するのではないか。
例えば、辛いものを食べることである。

上記の主張は色々と苦しいが、しかし、人間が他の動物と違って辛いものを好んで食べようとするのは、食べ物に余裕があることと、辛いものを楽しむ複雑な知性があるからだろうと私は思う。
となると、人間に次ぐ知性を持つと言われるゾウやイルカなどは、そのうち動物園や水族館で来場者に暴君ハバネロや激辛ペヤングを要求したりするのだろうか。

そんな杞憂を思いながら、私たち二人は秋葉原へ来た。
今回の標的は、『担々麺』である。

因みに、担々麺は本来「擔擔麵」と表記される四川料理を代表する一品で、19世紀の成都で天秤に麺と調理器具を担(擔)いで売り歩いたことを起源[1]とする、労働者向けのジャンクフードである。
当時の清の労働者たちも、きっとこの美辛い肉味噌餡の乗った麺を啜って英気を養っていたのだろう。
そうした歴史に思いを馳せつつ担々麺を食べると、意欲や元気が湧いてくるのは気のせいだろうか。我ながらなんとも都合のいい思考回路だと思いつつ。



19時を過ぎた頃、靖国通りと新幹線が交差する高架下に構えるその店は、小じんまりながらも存在感があった。
今回訪れた「雲林坊(ユンリンボウ) 秋葉原店」は、前を通れば誰もが目に止める、活気に満ちた店だ。

私たちが店内に入った時には既にお客さんで一杯だった。
店内はそこまで広くなく、カウンター10席が並ぶのみである。そう、これでいいのだ。仕事帰りのサラリーマンがカウンターで麺を無心に啜る。これこそ都心のラーメン屋だ。
しかし店内は清潔感もあり、決してむさ苦しい男の溜まり場という雰囲気でもない。実際、私たちが店を訪れたときも女性客は少なくなかった。この誰でも気楽に立ち寄れる雰囲気は素晴らしい。ただし、3人以上の団体での入店は難しいだろう。私たち2人でぎりぎりだったので。

私たち二人が注文したのは「汁なし担担麺」(¥830-)。
品書きには汁ありもあるが、今回はこの汁なしが目当てだ。
本場中国のスタイルに倣ったと言えば格好付くが、残念ながら関係ない。
この頃、高橋は長年愛してきたつけ麺にマンネリを感じているようだったが、遂にこの夏担々麺に心移りした。つけ麺に代わり日夜追い求めているらしい。
しかし、その担々麺の「汁なし」に拘るあたり、まだつけ麺への未練がたらたらなのが高橋らしさだ。

注文の回転率は高く、席に着いてから約10分、丁度良いタイミングで注文の品が出てくる。
まずその見た目、椀の形に目を見張る。アシンメトリーの白い器はまるで現代美術のオブジェか何かにしか見えない。しかし、その白磁の椀が麺を実に美味そうに色を引き立てるのだ。
そんな視覚に次いで、程なく嗅覚に刺激が走る。
花椒や八角などの香辛料がふんだんに掛けられており、椀を目の前に置くとすぐに中華らしいスパイシーで豊かで複雑な香りに包まれる。
この時点で期待は高まるばかりである。

麺を一口啜ると、もう素晴らしい。
麺に絡まった辛味の効いた肉餡とスープは、辛味をはっきり主張しつつ決して舌がヒリヒリするまではいかない。その奥に広がる肉とダシの旨味が辛味を抑えてくれる。いや、辛味と旨味が互いに見事に調和しているのだ。そして更にそこに香辛料の香りが口一杯に広がると、そこにはまさに中華のシンフォニーが生まれる。複雑で、優雅で、激しいハーモニーだ。
中太麺は食感、歯ごたえに存在感はあるが決して自己主張せず、肉餡とスープと香辛料をしっかりと引き立てている。
また、端に添えられた小松菜は色といい味といいまさに清涼剤のような存在だ。味付けはほとんどされていないが、麺を啜る合間に口に頬張ることで旨味がより一層引き立つ。
もう一つの引き立て役が、大量にまぶされたアーモンドだ。辛味の効いた餡と上手く中和して辛味を損ねることなくマイルドな風味になる。

細かい心配りも非常に感心した。
レンゲには3種類あり、白い陶器製のレンゲ(恐らく汁あり用)、そして金属製のレンゲには普通のと穴あきの二種類ある。辛味の強い汁が得意でない人には痛み入る配慮だろう。私たちのように得意な人は、普通のレンゲで最後の汁一杯まで堪能してほしい。

今回残念だったのは、白飯を注文しなかったことだ。
周りの客を見ていると、半数以上の人がご飯や麻婆丼などのセットで注文していた。個人的に麺とご飯を一緒に食べることをあまりしないので気に留めなかったのだが、汁なし担担麺を食べているうちにこれが非常にご飯に合うことに気付いてしまった。
もっと早く気づいていれば途中注文もできただろうが、麺を啜るのに夢中になってしまったのが敗因だ。
また、辛いのが苦手な人は卵を一緒に注文するといいだろう。これもまた非常に相性がいいだろうと途中で気付いた。

皿の底まで平らげて店を後にした私たちは、いかにあの店の担々麺が旨かったかを語り合っているうちに気付けば上野まで歩いていたのだった。
その後、HUB上野昭和通り店で3時間を呑み過ごして危うく終電を逃すところだったのだがそれはまた別の話。



人生は常に移ろい変わる。
いよいよ長かった学生生活に終わりが見え、社会へ歩み出す時が来た。
HUBで高橋の人生プランを聞いているうちに、お互い部を去って長い月日が経ったのだと感じずにはいられなかった。

思えばこの食べ歩きも随分遠くまで来たものだ。
ただ、ここまでだらだら続いたのだから、人生に変わらないものを持っておくのも悪くないだろう。



今回のお店はこちら。
『雲林坊 秋葉原店』
(食べログ) http://tabelog.com/tokyo/A1310/A131002/13141638/


#1 / 次 #2


[1] http://ja.wikipedia.org/wiki/担担麺#中国の担担麺

2014年7月19日土曜日

[C++]unique_ptrとvector

思わぬところで躓いたのでメモ



unique_ptrを知ってから大変重宝している。
ほぼコスト0なのにdeleteをいちいち書かずに済むのでまさにスマートなポインタ。
が、しかし、見えないが故にエラーが出ると厄介だ。

次のようなプログラムを書いた。
Dataクラスにunique_ptrで確保したメモリがありそこにデータを入れる。
それをvectorにいれてデータ配列を作ろうとした。よくありそうな場面だ。

#include <memory>
#include <vector>

class Data
{
public:
  std::unique_ptr<char> Buffer;
  size_t Size;

  Data(char* _buf, size_t size)
    : Buffer(new char [size])
    , Size(size)
  {
    memcpy(Buffer.get(), _buf, size);
  };
};

int main()
{
  std::vector<Data> list;

  const int MEM_SIZE = 128;
  std::unique_ptr<char> mem(new char [MEM_SIZE]);

  Data d(mem.get(), MEM_SIZE);
  list.push_back(d);
 
  return 0;
}

しかしこれをコンパイルしたところ次のエラーが表示された。(Visual Studio 2012)


error C2248: 'std::unique_ptr<_Ty>::unique_ptr' : private メンバー (クラス 'std::unique_ptr<_Ty>' で宣言されている) にアクセスできません。


ネットで調べてみると、次のページがヒントになった。
http://stackoverflow.com/questions/22788275/stdvectorstdunique-ptrint-does-not-compile

どうやらコピーコンストラクタに原因がありそうだということだが、一体どこから呼びだそうとしているのか、地道に一行ずつコメントアウトして調べていくとpush_back()が原因だとわかった。(すぐに気付きたかった)

vectorにpuch_backで要素を入れる際、要素をコピーして渡す。その時、コピーコンストラクタが働く。そして、今回のDataクラスにはunique_ptrで確保されたメンバ変数がある。unique_ptrは代入不可・コピー不可なので、デフォルトコピーコンストラクタではコピーすることができず、上記のエラーが発生したということだった。

原因が分かったところで、Dataクラスにコピーコンストラクタを実装したところコンパイルに成功した。

  Data(const Data& rhs)
    : Buffer(new char [rhs.Size])
    , Size(rhs.Size)
  {
    // Buffer = std::move(d.Buffer);
    memcpy(Buffer.get(), rhs.Buffer.get(), Size);
  }

unique_ptrを移動させる定石であるmove関数はここでは使えない。引数がconstなので変更を加えることができないためだ。
ということで、昔ながらのmemcpyを使用した。

スマートポインタは便利なだけに、ハマったときの解決策が分からなすぎて怖い。

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