前回記事に引き続いてUnityメモリ管理についてUnity Gemsより「構造体とクラスの比較」について翻訳を紹介したい:
http://unitygems.com/memorymanagement/
October 21, 2012
構造体とクラスの比較
それぞれどの状況で使用すべきだろうか?クラスを使用することでオーバーヘッドとなる変数が追加されることが分かった。もし1000個のオブジェクトを作成するのであれば1000個の変数を作成するが、構造体の場合は生成しない。
次の例をみてほしい:
public class DogC{ public string name { get; set;} public int age { get; set;} } public struct DogS { public string name; public int age; } using UnityEngine; using System.Collections; public class Memory : MonoBehaviour { DogC dogC = new DogC(); DogS dogS = new DogS(); void Update () { if(Input.GetKeyDown(KeyCode.Alpha1)) Print (dogS); if(Input.GetKeyDown(KeyCode.Alpha2)) Print (dogC); if(Input.GetKeyDown(KeyCode.Alpha3)) print ("Struct: The dog's name is "+dogS.name +" and is "+dogS.age); if(Input.GetKeyDown(KeyCode.Alpha4)) print ("Class: The dog's name is "+dogC.name +" and is "+dogC.age); } void Print(DogS d){ d.age = 10; d.name = "Rufus"; print ("Struct:The dog's name is "+d.name +" and is "+d.age); } void Print(DogC d){ d.age = 10; d.name = "Rufus"; print ("Class:The dog's name is "+d.name +" and is "+d.age); } }まず、3つ目の引数が期待される内容、すなわちdog構造体のnameとageがなくなり、出力できてないことに気付く。関数の終わりになくなっているのである。
クラスは、元のデータをアクセスできる参照を渡され、関数の中で修正し、これらの修正は関数が終わった後も残る。
この結果をどう捉えるべきか。もし構造体が非常に大きく例えば10変数があったとすると、案数に渡すときにスタックは10個のロケーションに広がる。もし構造体の配列を50個渡すと、スタックは500ロケーション大きくなり、スタックはサイズに制限があるため、もし配列がさらに大きければ、スタックオーバーフローが起きるかもしれない。
もしクラスの参照を渡すとき、ひとつの変数のみ渡す。クラスのインスタンスを50個渡す場合はスタックを50変数のみ大きくさせる。
このためクラスはメモリを節約するが構造体は圧倒的に早い。ポイントはこの二つのバランスを取ることだ。構造体を使う判断をするのは、5つから8つより少ないときで、それ以上の場合はクラスを使うべきだ。
常に頭に入れる必要があることとして、構造体である変数をアクセスするとき(例えばtransform.positionとするとき)、その構造体の複製をスタック上に作成していて、その複製作業は作成に時間を要するのだ。
クラスを作成するときは必ずヒープ上にメモリを割り当て、これによりすぐに破棄した値のガーベージコレクションのサイクルにつながる。構造体は必ずスタック上に割り当てられ、逆にガーベージコレクションを引き起こすことがない。
Unityの構造体は普段3つから4つの変数のときに使用されていることに気付く、例えばposition、color、quternion、rotation、scaleなど、これら全てが構造体だ。
生存期間の短く、頻繁に参照されるオブジェクトは構造体にするのが良く、その理由はガーベージコレクションを引き起こさないためです。生存期間が長くサイズが大きいオブジェクトはクラスにするのが良く、その理由はアクセスする度にコピーされることがなく、継続的に存在することでガーベージコレクションを引き起こさない。Vector3などたくさんの小さいオブジェクトやRaycastHitはまさにこの理由でUnityのなかで構造体なのである。
先に進む前に、一番大事なことを最後にとっておいた。構造体を参照で渡すことは可能であり、それにより構造体を修正し、値を保持することが出来る。
もともと変数を定義した関数が終了した後に、構造体への参照を保持することは出来ず、これにより問題が引き起こされるケースはたくさんあり、例えばクロージャの使用(ラムダ関数やインライン関数により構成される)があるが、かなり上級者向けのテクニックである。
void PrintRef(ref DogS d){ d.age = 10; d.name = "Rufus"; print ("Structure:The dog's name is "+d.name +" and is "+d.age); }次のようにこの関数の呼び出しを行う:
void Update(){ PrintRef(ref dogS); print ("Structure:The dog's name is "+dogS.name +" and is "+dogS.age); }キーワードのrefによってコンパイラに構造体へ参照をするように指令していて、スタック上で複製されていない。関数の外では、構造体は中で与えられた値を保持している。このキーワードはどのような値の種類でも使用できる。
------
メモリ管理の基礎を積み上げていこうぜ!
メモリ管理の基礎を積み上げていこうぜ!