2013年1月16日水曜日

Unityメモリ管理「ヒープのフラグメンテーション、オブジェクトプール、およびガーベージコレクション」

オブジェクトプールの利便性についてはこのブログでも過去に何度か取り上げてきた。

関連記事




今回の記事では

  1. Unity Gemsならではのオブジェクトプールに関するまとまった考え方
  2. 記事の一番最後にダウンロードできるサンプルのUnityプロジェクト

があるので是非そちらも試してみて欲しい。

なお、筆者環境ではサンプルシーンのうち1つだけがUnity4.0でうまく動作しなかったが、オブジェクトプールをプログラミングせずに雰囲気だけでも知るのに良いサンプルだとおもう!

-------
http://unitygems.com/memorymanagement/

October 21, 2012

ヒープ フラグメンテーション、ガーベージコレクション、およびオブジェクト プーリング


ヒープは大きなメモリのゾーンであり、データがランダムに格納する。実際にはそれほどランダムだなく、OSはメモリをみて必要なデータを当てはめるのに十分な最初のメモリゾーンを探す。ヒープのサイズおよび場所は選択したプレイヤーのプラットフォームにより幅がある。

例えば、整数をリクエストすると、OS は 4 バイト連続で空きメモリの箇所を探す。実際のところ、プログラム実行に伴い、メモリのロケーションは実際のロジックなしに使用および解放され、自身のプログラムはヒープのフラグメンテーションが生じる可能性がある。

図でヒープ フラグメンテーションの例を示す。明らかにこれは端的に誇張しているが、考え方は変わらない。data3 を割り当てているが、 state1 では state2 で破棄された data3 があった。
state1 と最後で 同じ量のデータあり、データは追加されていないことに留意してほしい。動作によってヒープ フラグメンテーションが作成されている。

Unity は .NET 管理メモリを使用する。 C プログラムではヒープ フラグメンテーション により、空きメモリ自体は十分にあっても十分な大きさの連続ブロックが見つからないために、メモリブロックを割り当てることが不可能な状況が作り上げられることがあった - 管理メモリはこの制約がない。もしメモリのブロックが .NET 管理メモリで見つからない場合は、ガーベージコレクション システムが使用されてアイテムを移動してヒープのフラグメンテーションを取り除く。これは当然ながら、時間消費の大きい演算であり、継続的に高いパフォーマンスを求められるゲームにおいて顕著なフレームレート低下を引き起こす可能性がある。

プログラマにとっての解決策はオブジェクト プーリングの概念を使用することだ。もし date3 を state1 で破棄する代わりに、後で使用できるように無効にしたらどうだろう。それによってヒープ フラグメンテーションが避けられたはずだ。

データを破棄せず、スリープさせて保持し、必要なときに揺り起こす。

この解決策には多くの利点があり、ひとつめとして前述のケースが回避でき、二つめとして Instantiate および Destory 関数のコールを回避できて gameObject.SetActiveRecursevely (true/false); を使用するのみだ。

最後に、破棄をしないため、高価な処理であるガーベージコレクションが行われない。
ObjectScript.cs
using UnityEngine;
using System.Collections;
 
public class Test : MonoBehaviour {
    public GameObject prefab;
    GameObject[] objectArray = new GameObject[5];  
        public static int counter; 
 
         void Start(){
        for(int i = 0;i < objectArray.Length;i++){
            objectArray[i] = (GameObject)Instantiate(prefab,new Vector3(0,0,0),Quaternion.identity);
            objectArray[i].SetActiveRecursively(false);
        }
        }
        void Createobject(){
        if(counter < objectArray.Length){    
                // 配列の中を反復
            for(int i = 0;i < objectArray.Length;i++){  
                // 無効なオブジェクトを探す
                if(!objectArray[i].active){    
                    // カウンタを増加させ、オブジェクトを有効化し、位置を決定
                    counter++;                                              
                    objectArray[i].SetActiveRecursively(true);             
                    objectArray[i].transform.position = new Vector3(0,0,0);  
                    return;
                }
            }return;
        }else return;
    }
}
void OnCollisionEnter(CollisionEnter){
    if(other.gameObject.tag=="Object"){
        other.gameobject.SetActiveRecursively(false);       //オブジェクトを無効化
        ObjectScript.counter--;                             //カウンタを減らす

シーンには同時に 5 オブジェクト以上はなく、ひとつが無効化されるとどこでも再度有効化できる。ガーベージコレクションは必要でない。

このテクニックを敵に使用できる。もし敵の波が向かってきたときに、やっつけた敵を破棄するかわりに、その敵を後ろの順番ににまわして、そのうち先ほどと同じ敵を気付かずにやっつけるようなことになる。

弾数についても同様で、あなたまたはNPCキャラクターが撃った弾丸を破棄するかわりに、無効化する。もう一回撃つときに銃口の前の同じ位置と正しい速度で有効化すればよい。

メモリ レベルでのプーリングはOSにより管理される別の処理であり、オブジェクトのサイズに関わらず同じ量のメモリを予約する。結果的にメモリ上に小さな空きがたくさん出来ることは防げる。

配列の再利用


オブジェクトの再利用に加えて、自身のコードにより異なるコールや同じコールの中で何回も再利用されるバッファを作成することは検討の価値がある。これはつまり、配列または List はヒープ上に割り当てられ、そして Update毎または何らか頻繁に発生する状況で、配列を作成することはガーベージコレクションのサイクルにつながってしまう。

void Update()
{
      if(readyToFire && Input.GetKeyDown(KeyCode.F))
      {
          var nearestFiveEnemies = new Enemy[5];
 
          // 最も近い敵をみつけて配列の
          // リストを埋める動作を行う
 
          TargetEnemies(nearestFiveEnemies);
      }
}

この例ではもっとも近い 5 人の敵をターゲットとしていて、自動小銃で撃っています。問題は発射ボタンを押すたびにメモリを割り当ててることだ。もしその代わりに生存期間の長い、コードのどこででも使用できる敵の配列を作成すれば、メモリを割り当てる必要がなく、結果的にスローダウンを防止できる。

次がもっともシンプルなケースとなる:
Enemy[] _nearestFiveEnemies = new Enemy[5];
 
void Update()
{
      if(readyToFire && Input.GetKeyDown(KeyCode.F))
      {
          // 最も近い敵をみつけて配列の
          // リストを埋める動作を行う
 
          TargetEnemies(_nearestFiveEnemies);
      }
}
しかしもしかすると、敵の配列が頻繁に必要であればもっと汎用的とすることができるかもしれない。
Enemy[] _enemies = new Enemy[100];
 
void Update()
{
      if(readyToFire && Input.GetKeyDown(KeyCode.F))
      {
          // 最も近い敵をみつけて配列の
          // リストを埋める動作を行う
 
          TargetEnemies(_enemies, 5);
      }
}
TargetEnemies のコードを書き直してカウントをとるようにすることで、効率的に一般的な目的でコードのどこからで使用できる、敵のバッファを使用して、さらなる割り当ての必要性を回避できる。

二つめの例はかなり極端なケースで、これは本当に大きくメモリ問題を引き起こすようなデータコレクショがある場合に限るべきだ - 一般的には数バイトのメモリ節約よりもつねに読みやすく、分かりやすいコードを優先すべきだ。
static 変数およびメモリ管理のデモのビデオをふたつ用意した。

このビデオで使用されたプロジェクトはここからダウンロードできます。


----

長らく翻訳してきたUnity Gemsメモリ管理についてはこれでひととおり完了だ。

機会があればまとめ記事として整理してみるぜ!

0 件のコメント:

コメントを投稿

ブックマークに追加

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

自己紹介

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

ページビューの合計

過去7日間の人気投稿