2012年12月18日火曜日

Unityメモリ管理「参照型とヒープ」

Unityでゲームオブジェクトのインスタンス化をする際に、バックグラウンドでのメモリ管理はどうなっているのだろうか。

前回記事に引き続いてUnity Gemsよりメモリ管理について「参照型とヒープ」について翻訳を紹介したい:

http://unitygems.com/memorymanagement/

October 21, 2012


参照型とヒープ


参照型の変数はメモリに参照として格納されたオブジェクトだ。新しいクラスのインスタンスを作成する際に、データはヒープに格納されるとともに、そのデータの番地に関する参照も格納される。この参照は、オブジェクトのアドレスを値として持つ変数だ。クラスのインスタンスを作成する際は、new キーワードを指定して、これにより、オブジェクトの型に対応したメモリの領域確保のためOSへコール要求を行い、さらにオブジェクト初期化に必要なコードを実行する。下の例で犬のクラスDogのインスタンス化を見ていく。
public class Dog{
     public string name { get; set;}
     public int age { get; set;}
     public Dog(string s, int n){
           name = s;
           age = n;
    }                          
}
public class Test : MonoBehaviour {
    Dog dog1 = new Dog("Rufus",10);
    Dog dog2 = new Dog("Brutus",8);
 
    void Update (){                                                  
        if(Input.GetKeyDown(KeyCode.Alpha1))
            print ("The dog1 is named "+dog1.name+" and is "+dog1.age);
        if(Input.GetKeyDown(KeyCode.Alpha2))
             print ("The dog2 is named "+dog2.name+" and is "+dog2.age);
    }
}
dog1とdog2変数は、これらのオブジェクトが格納されるメモリの番地に対する参照に該当する。

次にInstantiate関数の活用を見ていく。例えばEnemyというPrefabがあり、3Dモデルといくつかのコンポーネント、スクリプトが含まれるとする。
using UnityEngine;
using System.Collections;
 
public class Test : MonoBehaviour{
    public GameObject enemyPrefab;
 
    void Update(){
        if(Input.GeyKeyDown(KeyCode.Space))
               Instantiate(enemyPrefab,new Vector3(0,0,0), Quaternion.identity);
    }
}
enemyPrefabの新しいインスタンスを作成し、実行も出来ている。問題はこのオブジェクトに対してアクセスしたくとも参照をもっていない点である。新しく作成したオブジェクトにアクションを加えたい場合、参照とするための変数を作成することになる。
void Update(){
    if(Input.GeyKeyDown(KeyCode.Space))
       GameObject enemyRef = Instantiate(enemyPrefab, new Vector3(0,0,0), Quaternion.identity);
        enemyRef.AddComponent(Rigidbody);
        Destroy(enemyRef);
     }
} 
この例では、enemyRefが宣言され、新しいオブジェクトの番地を割り当てしている。enemyRefがその参照となっている。変数がオブジェクトの番地を知ることが出来るため、てコンポーネントを追加できて、オブジェクトのパブリック変数を追加またはアクセスすることが出来る。最終行では、実践的ではないものの説明の都合上、すぐにオブジェクトを破棄している。

参照が{}の中でのみ存在する自動変数であることに留意すべきだ。その外では、参照変数はもはや存在していない。enemyオブジェクトを取得するためには、例えば次のような方法で探す必要がある:
GameObject enemyRefAgain = GameObject.Find(“Enemy”);
全ての参照型変数はヒープに格納される:

  • クラス
  • オブジェクト
  • 文字列(stringは変数に見えるが、実際はStringクラスのインスタンス)

値型変数と参照型変数の主な差異は、データが格納されている場所を示す、参照型変数の追加である。データを探す前に、この参照にアクセスする必要がある。これにより直接アクセスすることが出来る構造より若干遅くなる。

 他に、問題となってくるポイントは、次のような処理である:
using UnityEngine;
using System.Collections;

public class Memory : MonoBehaviour {
    DogC dogC1 = new DogC();
    DogC dogC2 = new DogC();
    int number1;
    int number2;
 
    void Update () {
        if(Input.GetKeyDown(KeyCode.Space)){
           number2=number1 = 10;
           number1 = 20;
           print (number1+" "+number2);
           dogC1.name = "Rufus";
           dogC1.age = 12;
           dogC2 = dogC1;
           dogC1.name = "Brutus";
           print (dogC1.name+" "+dogC1.age+" "+dogC2.name+" "+dogC2.age);
        }
    }
}

二つのnumberが独立していて、片方を変更しても、もう片方が変更されないことに気付く。

逆に、もうひとつにクラスを割り当てると、片方への変更がもう片方へ反映される。

これは dogC2 = dogC1; としたときにdogC1のアドレスをdogC2の参照変数に割り当てるためである。dogC2データの元の番地はメモリで失われ、dogC1またはdogC2どちらを変更しても同じになる。これにより同一のデータ番地に対する二つの参照が出来たことになる。

生存期間、有効範囲、およびガーベージコレクション


参照型変数の場合、生存期間は明確でなく、ガーベージコレクションがその管理を行う。CやC++の古き時代において、プログラマが動的に割り当てられた変数がメモリから解放されることはプログラマの仕事であった(C言語ではfree()関数、C++言語では~記号)。

C#ではガーベージコレクションにより変数が未だ使用されているかチェックを行い、もし参照が実行コード中にない場合、メモリの番地が解放できるものとしてマーキングを行う。COMインタフェース、あるいは参照カウンタのシステム、になじみのあるプログラマにとってはガーベージコレクションに違和感を感じることがあり、なぜなら参照カウンタのシステムでは2つのオブジェクトが互いに参照しあうことは(例えば親が子への参照を保持し、その子が親に対する参照を保持する)、メモリが永久に解放されない要因となるためであるが、ガーベージコレクションにおいてはそのような制約は存在しない。

ガーベージコレクションは大きなテーマであって、このコレクションのプロセスの実装にあたって、伝統的なC#になじみがある人にとってUnityはいくつかおかしな点があるように感じられ、それは短い生存期間のオブジェクトにとってガーベージコレクションの負荷は小さいと期待するためであるが、これはUnityでは当てはまらない。

メモリのブロックが埋められるとき、システムはヒープに割り当てられた全てのオブジェクトに到達できるか、をチェックする必要があり、すなわち到達出来る場合はオブジェクトはretainされそうでない場合はreleaseされる。通常の.NETガーベージコレクションはGenerationと呼ばれる単位で行われ、この到達できるかチェックする処理負荷を抑えるために、新しく作成されたオブジェクトを先にチェックし、その他の古いオブジェクトをチェックするかどうかはメモリが不足している場合のみチェックしている。Unityは常に全てのオブジェクトをチェックしている様子であるため、ガーベージコレクションは.NET言語で一般に期待するよりも遥かに大きなオーバーヘッドとなる。

到達出来るかのテストはガーベージコレクションの鍵を握っていて、これによりコードが実行中であるか、その可能性があるかどうかをコレクションの際に、有効な参照があるかどうか、コレクション対象のオブジェクトに対する参照があるかどうか、などで判断している。これは関数の中にクロージャがあるかどうかも含まれ、このことは非常に複雑で強力であるがゲームで必要とされるスピードという観点では不足している。

クロージャは、関数の中で定義された無名関数の中でローカル変数またはパラメータを参照することにより作成される。変数の値は、無名関数が呼び出しされたときにルーチンが実行されたときと同じ変数が使用できるよう、保持される。
List<action> actionsToPerformLater = new Listt<action>();

void DisplayAMessage(string message)
{
      var t = System.DateTime.Now;
      actionsToPerformLater.Add(()=>{
          Debug.Log(message + " @ " + t.ToString());
      });
}

void SomeOtherFunction()
{
      DisplayAMessage("Hello");
      DisplayAMessage("World");
      SomeFunctionThatTakesAWhile();
      DisplayAMessage("From Unity Gems");

      foreach(var a in actionsToPerformLater)
      {
           a();
      }

}
このコードの中ではDisplayAMessageをコールする度に、渡されたメッセージおよび現在時刻にもとづいて、クロージャを作成する。最後にforeachループを実行するとき、先の関数を呼び出ししたときに渡された変数をリストに追加して、デバッグメッセージのログが作成される。クロージャは非常に強力なツールで、将来の記事でより深く取り上げたい。

ガーベージコレクションは、いくつかの処理のためにシステムが十分にメモリがないと判断したときに実行され、すなわちオブジェクトが破棄されても、かなり長い期間の間コレクションの対象とならないことがあるということで、通常はオブジェクトが必要なくなったときに、明示的に外部接続をクローズすることや、外部リソースを解放することを行う。

内部オブジェクト(プロジェクト内のクラス)を明示的にリリースすることは必要がないが、プロジェクトの外にあるもの、例えばストリーム(ファイル)およびデータベース接続の場合などシステムに影響がある場合はそれを行うべき理由がある。例えばファイルをクローズしないことで、ファイルが破棄されてもオープンのままでいた場合、後続のアクションにてファイルのアクセスに失敗する可能性があるためだ。

ガーベージコレクションは高価な処理で、ゲームの実行に影響のないタイミングで実行することは有意義で、例えばレベルロードのとき、プレイヤーがポーズメニューを選択したときや、その他プレイヤーが気付きにくいタイミングで行うべきだ。ガーベージコレクションを手動でトリガーするためには System.GC.Collect(); を用いる。
参照型変数の有効範囲について、適切なアクションにて探すことが出来ることを前提に、プログラムのどこからでもアクセスすることが出来て、どのスクリプトからもGameObject.Find()を使用してオブジェクトに対する参照を見つけることが出来る。

ゲーム実行中にインスタンスを探す場合の詳細についてはGetComponentチュートリアルを参照してほしい。
------

少し長かったとおもうが、色つき部分を読み返してみて、あらためて初めて知った内容について復習すると良いとおもう。

Unityメモリ管理の学習を進めていこうぜ!

0 件のコメント:

コメントを投稿

ブックマークに追加

このエントリーをはてなブックマークに追加

自己紹介

自分の写真
Unity3D公式マニュアル翻訳やってる人がスマホ(iPhone, Android)のゲーム開発しています。気軽に面白く初心者が遊べる内容がモットー。Blogでは開発情報をひたすら、Twitterではゲーム作成の過程で参考にしている情報を中心につぶやきます

ページビューの合計

過去7日間の人気投稿

ブログ アーカイブ