2011年05月

コピーしてやってみた

前の記事(Singleton的にやってみた)で、Singleton的にクラスを作ったが、別にクラス1つにつき1つのインスタンスに制限しなくてもいいし、1つに制限すると逆に面倒くさいことになりそう。
重要なのは生成時に毎回ミノを構成するブロックを作成処理が動かないことだ。
ってことで今度はコピーする方法でやってみる。

□Minoクラス(抽象クラス)
・IDプロパティ(抽象)
・ミノを構成するブロック情報変数
・「ミノを構成するブロック情報」を生成するメソッド(抽象)
・位置情報
・方向番号
・落下とか回転とか移動の処理
・コンストラクタ
「ミノを構成するブロック情報」を生成するメソッドを呼んで、
それをミノを構成するブロック情報変数に代入する
・浅いコピー処理(シャローコピー)
Minoクラスの浅いコピー処理を実行する。

↓(継承)

□Mino0~Mino6(棒ミノや四角ミノの種類ごとに1つ)
・IDプロパティの実装
棒ミノを表すmino0クラスなら、return 0;を返す 等

・「ミノを構成するブロック情報」を生成するメソッドの実装
棒ミノを表すmino0クラスなら、
方向0・・・ (-1,0 0,0 1,0 2,0)
方向1・・・ (0,-1 0,0 0,1 0,2)
方向2・・・ (-2,0 -1,0 0,0 1,0)
方向3・・・ (0,-2 0,-1 0,0 0,1)
といったようなデータを返す


□TetrominoGeneratorRandomクラス
・Mino0~Mino6までのインスタンスを保持しておく変数

・初期化
Mino0~Mino6までのインスタンスを生成する

・GetNewMino()
ランダムにMino0~Mino6までのインスタンスの浅いコピー(シャローコピー)を返す。



Minoとそれを継承したMino0~Mino6は、前の記事(XNAテトリスで悩んでること)とほぼ同じだ。
ブロックの保持とミノの回転・移動・落下などの各種処理を行っている。
コンストラクタでブロックを生成するメソッドを呼び出して、実際の生成は継承したクラスのメソッドが行っている。

前と違うところは浅いコピー(シャローコピー)を行うメソッドを追加したところだ。
インスタンスのコピーとは、インスタンスを新しく生成して、フィールドの値を複製することだ。
その手法に、浅いコピー(シャローコピー)と深いコピー(ディープコピー)がある。

浅いコピーは、値型のフィールドはそのままコピーされるが、参照型のフィールドは参照だけがコピーされる。つまり中身のコピーを行わない。これは、コピー先でも同じ参照型のインスタンスを使うわけだ。

例えば、Aというクラスの中にBというクラスの参照型のフィールドがあったとする。
AのインスタンスA'の中にBのインスタンスB’があるとき、
インスタンスA'の浅いコピーを行うと、新しくインスタンスA''ができるが、その中のBのインスタンスはA'の中にあったB'の参照がそのままコピーされる。B''が新しく生成されてコピーされるわけではない。
A''の中のB'をいろいろいじると、A'のなかのB’も変わってしまうわけだ。同じものだから。

深いコピーは、値型のフィールドはそのままコピーされ、参照型のフィールドもインスタンスが新しく生成されて、中身がコピーされる。
先ほどの浅いコピーの例で言えば、A''の中に新しくB’のコピーB''が生成される。
A''の中のB''をいじっても、A’の中のB'は変更されない。


浅いコピーはTetrominoGeneratorRandomクラスで利用している。
このクラスは、GetNewMinoメソッドを呼び出すとランダムでMino0~Mino6までのインスタンスを返してくれる生成クラスだ。

このクラスでは、初期化時にあらかじめMino0~Mino6までのインスタンスを生成して保持している。
Mino0~Mino6は生成時にミノを構成するブロックの情報も作成する。
GetNewMinoメソッドでは、この保持されたMino0~Mino6のインスタンスの浅いコピーを返している。

なぜこのようなことをするかというと、ミノを構成するブロック情報の生成をやりたくないからだ。
浅いコピーなら参照型のフィールドは参照だけがコピーされる。
ミノを構成するブロック情報は参照型なので、この参照がコピーされるわけだ。

つまり最初にMino0~Mino6を生成したときにだけブロック情報が構築され、あとはコピーコピーでやっていけるので、コストが安いというわけ。
ついでに参照だけコピーなのでメモリ消費も少なくてすむ。

浅いコピー処理を作ったのにはもう1つ理由がある。
テトリスの落下地点を表示するガイドミノの表示のためだ。
ガイドミノの位置は、現在落ちているミノの落下地点にあるので、Minoクラスの落下処理がそのまま使える。
現在落ちているミノを落下させるわけにはいかないので、コピーしたものを落下させて、それを表示させている。


コードを簡単に張っておく。
 TetrominoGeneratorRandomクラス

       private Mino[] MinoInstances = 
        {
            new Mino0(),
            new Mino1(),
            new Mino2(),
            new Mino3(),
            new Mino4(),
            new Mino5(),
            new Mino6(),
        };
  
      public Mino GetNewMino()
        {   //ランダム
            int minoNumber = randomForTetromino.Next(MinoInstances.Length);
     // 浅いコピーしたものを返す
            Mino newMino = MinoInstances[minoNumber].ShallowCopy();

            return newMino;
        }

Minoクラス
        public abstract int ID { get; }
        public IList<IList<MinoBlock>> MinoBlocks { get; private set; }

        private int _minoAngleNumber = 0;
        public int MinoAngleNumber
        {
            get
            {
                return _minoAngleNumber;
            }
            private set
            {
                _minoAngleNumber = value;

                if (_minoAngleNumber < 0)
                {
                    _minoAngleNumber = MinoBlocks.Count - 1;
                }
                else
                {
                    _minoAngleNumber %= MinoBlocks.Count;
                }
            }
        }

        private Point _location;
        public Point Location
        {
            get
            {
                return _location;
            }
            set
            {
                _location = value;
            }
        }
        
        public TetrisField TetrisField { get; set; }

        private float fallSpeedSum = 0;
        public float FallSpeed { get; set; }

        public bool Finished { get; private set; }

        public Mino()
        {
            MinoBlocks = CreateMinoBlocks();
        }

       // ミノを構成するブロック情報の保持変数
        protected abstract IList<IList<MinoBlock>> CreateMinoBlocks();
        public IList<MinoBlock> CurrentMinoBlock
        {
            get
            {
                if (MinoBlocks.Count >= 1)
                {
                    return MinoBlocks[_minoAngleNumber];
                }
                else
                {
                    return null;
                }
            }
        }
   // 浅いコピー
        public Mino ShallowCopy()
        {
            return (Mino)MemberwiseClone();
        }

        // 以下回転とか移動とか

Mino0クラス

        public override int ID
        {
            get { return 0; }
        }
        protected override IList<IList<MinoBlock>> CreateMinoBlocks()
        {
            IList<IList<MinoBlock>> minoBlocks = new List<IList<MinoBlock>> 
            { 
                new List<MinoBlock> 
                { 
                    new MinoBlock(new Point(0, 0), 0),
                    new MinoBlock(new Point(-1, 0), 0),
                    new MinoBlock(new Point(1, 0), 0),
                    new MinoBlock(new Point(2, 0), 0)
                },
                new List<MinoBlock> 
                { 
                    new MinoBlock(new Point(0, 0), 0),
                    new MinoBlock(new Point(0, -1), 0),
                    new MinoBlock(new Point(0, 1), 0),
                    new MinoBlock(new Point(0, 2), 0)
                },
                new List<MinoBlock> 
                { 
                    new MinoBlock(new Point(0, 0), 0),
                    new MinoBlock(new Point(-1, 0), 0),
                    new MinoBlock(new Point(-2, 0), 0),
                    new MinoBlock(new Point(1, 0), 0)
                },
                new List<MinoBlock> 
                { 
                    new MinoBlock(new Point(0, 0), 0),
                    new MinoBlock(new Point(0, -1), 0),
                    new MinoBlock(new Point(0, -2), 0),
                    new MinoBlock(new Point(0, 1), 0) 
                },
            };

            return minoBlocks;
        }


以上。

C#のGCについてなんとなくまとめてみる2

前回の続き。

さて、ここまでGCの仕組みについてまとめてみたが、次にゲームにおいてGCをどのような方針で扱っていけばよいのか考えたことをまとめてみる。
とりあえずPC上やWindowsPhone7のMangoで採用されている世代別GCについてまとめる。

とはいえ、世代別GCをつかったゲーム開発の指針は下記のページで有難くまとまっている。
これをもうちょっと自分の頭の中で考えてまとめてみた。
いや、普通の人は上のページだけでも理解できると思うが、自分は考えてみないと分からなかったもので・・。


上のページでは、
GCの発生頻度を1フレーム、すなわち1/60秒よりも遅らせることができるかもしれない。この場合、1フレーム以下の寿命を持つオブジェクトの大多数は、「短寿命オブジェクト」として軽量な第0世代GCで回収されることが期待できる。
とかいてある。

これはどういうことかというと、短命なインスタンスは第0世代のみのGCが発生する前に使い終わりましょう、ってこと・・かな。
GCの仕組みで書いたとおり、第0世代のみを対象にしたGCは軽い。
ある程度大量にインスタンスを生成したとしても、第0世代のみを対象にしたGCが発生する前に使い終われば、GCで素早く回収できて、ゲームの動作にもあまり影響を及ぼさないはず。

逆に短命なのに第0世代のみを対象にしたGCが発生するまでに使い終わらなければ、それらの大量のインスタンスはすべて第1世代に「昇格」してしまう。
第0世代と第1世代を対象にしたGCは、第0世代のみのGCより重いので、ゲームの動作に影響を及ぼしてしまうかもしれない。
あるいは第2世代に「昇格」してしまえば、最も重いGCが動くことになる。

第0世代のみのGCではある程度大量のオブジェクトを素早く破棄できると書いたが、第0世代のみGCは大量のインスタンスを生成するほど発生タイミングが早くなる。
つまり、短命なインスタンスは第0世代のみのGCが発生する前にすべて使い終わろう、というより使い終わることができる程度の量を生成するのが望ましいということだ。

ゲームでは、1フレーム内で各種計算などで用いられて使い終わるインスタンスが大量にある。これらの短命なインスタンスはフレーム内で大量に使われ、フレームが終わる頃には大量に使い終わる。

1フレームより長い間隔で第0世代のみのGCが発生する程度の短命なインスタンスの量なら、第0世代のみのGCで素早く破棄できるということだ。

例えば、1.1フレームごとに第0世代のみのGCを発生するとなれば、1フレーム分の短命なインスタンスは第0世代のみのGCで破棄される。
この例だと0.1フレーム分の短命なインスタンスが第1世代に「昇格」するが、昇格するインスタンスは1フレームごと少量なので、それほど頻繁には第0世代と第1世代のGCは発生しない。

逆に0.9フレームごとに第0世代のみのGCが発生するとなれば、0.9フレーム分の短命なインスタンスがすべて第1世代に「昇格」してしまう。
次の第0世代のみのGCでは、0.8フレーム分の短命なインスタンスが第1世代に「昇格」する。
第1世代のみにどんどん短命なインスタンスができてしまうので、第0世代のみのGCより重い、第0世代と第1世代のGCが頻繁に発生することになる。

って感じだと思う。

どうも調べていくと、各GCの速度的には以下のような感じみたい。
第0世代のみのGC < 第0世代と第1世代のGC <<< 越えられない壁 <<< すべての世代のGC

上のページには
最も停止時間の長い第2世代GCの発生頻度は、どれぐらいの割合でオブジェクトが第2世代にたまっていくかに大きく依存する。第2世代GCの発生間隔を、ゲームの1ステージである18000フレームに比べて十分長くできれば理想的だ。
とある。

これはすべての世代のGCは重くて、ゲーム中に発生してしまうと大変なので、しばらく動作が停止することが許される場所、たとえばゲーム開始前や終了後のロード画面で発生すればいいよね、って感じなんだと思う。
ステージが終わって、次のステージ中ですべての世代のGCが発生したらやはり残念だし。

もしくは各ステージに1回程度のすべての世代GCなら重くても許容されるよね、ってことかな。

すべての世代のGCを発生させないようにするためには、短命なインスタンスは、第0世代のみのGC、または第0世代と第1世代のGCで破棄してしまうことが重要である。なぜならインスタンスが大量に第2世代に「昇格」してしまうことが、すべての世代のGCを発生させる条件だからだ。
長命なインスタンスが少量なら、ゲーム中に生成しても大丈夫かもしれない。
少量なら第2世代になったとしても、すべての世代のGCは発生しないからだ。

長命なインスタンスが大量に生成される場合は、ゲーム中ではなく開始時にあらかじめインスタンスを生成しておいて、使いまわすのがいいのかもしれない。長命なインスタンスはゲーム中に生成すれば、いずれ第2世代になり、すべての世代のGCを発生させてしまう。ゲーム開始時に第2世代としておけば、ゲーム中にすべての世代のGCが発生される可能性は少ない。

短命なインスタンスをゲーム中ではなく開始時にあらかじめインスタンスを生成しておいて、使いまわすことも考えられる。しかし、長命なインスタンスを大量に生成する場合より効果は薄いかもしれない。短命なインスタンスはゲーム中にnewして生成しても、第0世代のみのGCや第0世代と第1世代のGCでも比較的素早く破棄できるからだ。
逆に常に使うか使わないか分からないインスタンスを大量に保持しておくことによるメモリの浪費が問題になるかもしれない。
とはいえ性能がシビアで、大量のインスタンスが生成される大規模なゲームになれば検討することになるかもしれない。


C#には構造体というものが用意されている。
構造体は値型というもので、1万とかの配列にしてもインスタンスは1つとなる。
インスタンスの数が減らせるということだ。

GCが発生したとき、不要かどうかチェックしなければならないインスタンスが減るので、結果GCの実行が軽くなる。構造体には欠点もあるので、適切なところで使わなければGC以外の部分で遅くなってしまう可能性があるので、注意が必要である。


ここまで世代別GCでの方針について考えてまとめてみたが、次にXBOX360のGCについてまとめてみる。
XBOX360のGCは「マークアンドスイープGC」である。
世代別GCのように新しいインスタンスだけチェックするという賢い動作はせず、GCが発生するとすべてのインスタンスについてチェックを行うために、重い。

世代別GCのすべての世代のGCについてのまとめたところが役立つと考えられる。
第0世代のみのGCや第0世代と第1世代のGCについてのところはまったく役に立たない。
そんなものはないのだから。

大量にインスタンスがある場合は、インスタンスの使い回しをして、ゲーム中では一切newをしないという方針を検討する必要があると思う。ゲーム中で大量にインスタンスを生成してしまえば、GCが発生してゲームが止まってしまうからだ。

構造体の配列を検討するのもいいかもしれない。
前述したとおり1万の配列でもインスタンスは1つだからだ。
インスタンスが少なければGCが発生してもその時間は短くなる。
GCの時間が短ければ、ゲーム中にインスタンスを生成しても問題がないという方針をとってもいいかもしれない。



というわけで、ここまでGCについて調べて考えてみたことをまとめてみた。
ここに書かれていることすべてが正しいことではない。
というのも、自分自身まだゲームを作っておらず、XNAでゲームを作る上で必ず必要になると思ったので、まず調べただけだからだ。

最終的にはPCだったりXBOXだったりでツールを使ってパフォーマンスを計測しながら、ゲーム全体を見渡して、インスタンスの扱い方を変えてみたり、構造体を使ってみたりと試行錯誤することになるのが普通だ。

これからゲームを作っていって、ここに書いたことを確認していきたい。

C#のGCについてなんとなくまとめてみる1

XNAでゲームを作るうえで欠かせない気がするC#のGC(ガベージコレクション)。
ここまで色々調べてきて考えたことをまとめてみる。
ただし大規模なゲームとか作ったわけではないので、調べた結果の「妄想」ということで。
そして抽象的な表現が含まれてるので詳しい動作は調べてほしい。

まず、基礎知識として、GCとは、使わなくなったオブジェクトを自動的に破棄してくれる機能である。
C++ではnewしたインスタンスは、deleteしないとメモリを開放してくれない。
しかしC#ではnewしたインスタンスは、他のオブジェクトから参照されなくなったら、そのうち、自動的にGCによって破棄される。「参照されなくなる」というのは、インスタンスの参照を保持していた変数をnullにするとか、スコープを抜けるとかして、そのインスタンスにアクセスできなくなったときのことをいう。あるインスタンスの参照を持っていた別のオブジェクトのインスタンスが参照されなくなったときも同じ。
参照されなくなったインスタンスはゴミなので、ゴミを回収するということで、ガベージ(=ゴミ)コレクション(=回収)と呼ばれる。

GCが発生する(インスタンスを破棄すること)タイミングは基本的には分からない。
GC自身のルールに従って、そのうち自動的に、不要になったインスタンスの破棄を行う。

GCの動作の条件や破棄の仕方は、C#の処理系によって違う。
ある環境ではバックグラウンドで動いたり、ある環境ではメインの処理をいったんとめて、一気にインスタンスを破棄したりする。XBOX360では、処理をいったんとめてGCを行うらしいので、ゲーム中にGCが発生して時間がかかるとゲームとして大変なことになることは想像に難くない。

こんな動作がいつ起こるか分からないのというのだから、C#でゲームを作るときは、GCの動作を把握しておくことは大変重要ということである。


次に詳しいGCの動作について見ていこう。
PC上のC#のGCは、世代別GCというものが採用されている。
これは、最近生成したインスタンスほどすぐに破棄され(短命)、古いインスタンスほどずっと生き残っている(長命)可能性が高いという特徴があることに注目した方式らしい。
第0世代、第1世代、第2世代があり、生成されたインスタンスはいずれかの世代に分類される。
新しく短命な可能性が高いインスタンスは第0世代、古く長命な可能性が高いインスタンスは第2世代となる。


世代別GCでは、第0世代のみを対象にしたGC、第0世代と第1世代を対象にしたGC、そしてすべての世代を対象にしたGCがそれぞれ発生する。

もっとも頻繁に発生するGCは、第0世代のみを対象にしたGCだ。
これは、第0世代のインスタンスが最も短命である可能性が高いからだ。
すべての世代を対象にGCをすると、確かに確実にすべての不要なインスタンスを破棄することはできるが、すべてのインスタンスをチェックして不要かどうか確認しなければならないので、とても重い処理になる。

そこで長命な可能性が高い第1世代と第2世代のインスタンスをとりあえず「必要」ということにして、第0世代のインスタンスだけを対象にすることで処理を軽くしている。
すべての世代を対象にGCをしたときのように、すべてのインスタンスを確実に破棄することはできないが、第0世代のインスタンスは短命の可能性が高いので、多くのインスタンスを破棄することができる。
効率がよいのだ。

ここで破棄されなかった第0世代のインスタンスは第1世代のインスタンスへと「昇格」する。
つまり古いインスタンスとなり、第0世代のみを対象にしたGCの対象からは外れることになる。


第0世代と第1世代を対象にしたGCについても理由は同じである。
このGCは、第0世代のみを対象にしたGCよりも頻繁には発生しない。
第2世代よりも短命な可能性が高い第0世代と第1世代だけを対象にすることで、すべてのインスタンスを対象にしたときよりも処理を軽くして、効率がよいGCを行っている。
ここで破棄されなかった第1世代のインスタンスは第2世代へと「昇格する」


すべての世代を対象にしたGCは、最も発生する回数が少ない。
第2世代のインスタンスは長命な可能性が高く、必要なインスタンスが多いときに何度も発生してもあまり意味がないし、前述のとおり発生すると処理が重いためだ。
第2世代より古い世代はないので、ここで破棄されなかった第2世代のインスタンスはそのまま第2世代となる。


GCの発生条件は、ある世代のインスタンスをいっぱい生成し、その世代のヒープ領域が消費されて、各世代ごとに設定された使用量を上回ればその世代までのGCが発生するみたい。第1世代のインスタンスの量が設定されたヒープ使用量を上回れば、第0世代と第1世代を対象にしたGCが発生する、的な。
多分・・。他にも色々な条件はあると思う。具体的な方法は調べていただきたい。
各世代ごとのヒープ使用量はシステムの状況を踏まえて勝手に設定される。

newで生成した新しいインスタンスはすべて第0世代になるので、第0世代のみを対象にしたGCが最も頻繁に発生することになる。
第2世代のインスタンスは第0世代から第1世代、第1世代から第2世代までの昇格を経ないといけないので、すべての世代のGCはそれほど頻繁に発生しないわけだ。


XBOX360ではこのような世代別のGCは搭載されていない。
「マークアンドスイープGC」という方式だけでインスタンスを破棄するようになっている。
これは世代別GCにおける、すべての世代を対象にしたGCだけを毎回行ってるような感じだ。
GCの発生条件は、ヒープを1MB消費するかOutOfMemoryException例外(メモリ不足)が発生したときとなる。

最近出てきたWindows Phone 7のMangoでは、世代別GCが採用されるらしい。
WindowsPhone7には個人的に興味があるので(というかXNAをやってるのはいずれ発売されるであろうWindowsPhone7で動かしたいからという欲求があったりなかったり)、PCとWindows Phone 7がGCについては同じ考え方で開発できそうなのがうれしい。


次回へ続く。



記事検索
livedoor プロフィール
QRコード
QRコード

トップに戻る