1. ホーム 
  2. 備忘録 
  3. Unity

ガベージコレクション

ガベージコレクションとは

誰からも参照のない不要となったメモリは定期的(もしくは手動で)に解放して再び使えるようにしなければいけない

Unity というよりは C# の機能だが、アプリケーションのメモリの割り当てと解放を管理する仕組みをガベージコレクションという

これによってオブジェクトを解放し忘れたためにメモリリークが発生したり、既に解放されているオブジェクトの解放されたメモリにアクセスしようとするなどの問題を回避することができる


ただし、上記の管理対象外となるアンマネージリソースについては明示的に解放処理を呼び出す必要がある

アンマネージリソースの例としては、ファイル ハンドル、ウィンドウ ハンドル、ネットワーク接続などのオペレーティングシステムリソースをラップしたオブジェクトがある

IDisposable を継承したクラスについては基本的に明示的に解放することを望まれるものであるため、不要になったタイミングで Dispose() を呼びだしておくこと


ここまでは主に C# のメモリリークに関する留意事項となるが、Unityでは上記に加えて Leaked Managed Shell について気を付ける必要がある


メモリの割り当てを最適化する

ガベージコレクションは正しく使用されれば自動メモリ管理によって、一般的に手動割り当てと同等か、それ以上の全体パフォーマンスを得られる

しかし、ガベージコレクションプロセスは裏でかなりのCPU時間を必要とするため、プログラマーはコレクターを頻繁に使用する自体にならないように注意する必要がある

以下にいくつかの例を記載する


public class Sample : MonoBehaviour
{
  /// <summary>
  /// 連結処理
  /// </summary>
  public static string Concat(int[] intArray) 
  {
    string line = intArray[0].ToString();
    
    for (i = 1; i < intArray.Length; i++)
    {
      line += ", " + intArray[i].ToString();
    }
    
    return line;
  }
}

この例では、文字列の連結をforループ内にて行うたびに、連結前の文字列分のメモリは不要となり、連結後の文字列分のメモリが再割り当てされることとなる

不要となったメモリはガベージコレクションによって整理されるが、コレクターの頻繁な使用を避けるためにはメモリの使い方を見直すべきである

文字列の連結であれば、System.Text.StringBuilderクラス などは有名である

このクラスは先に必要なメモリをコンストラクタにて指定することができるほか、指定しない(自動とする)場合でも、現在の確保済みのメモリ容量を超える際には2倍の大きさのメモリを再確保することで、メモリの再割り当ての頻度を下げている


他にも Update関数 などのフレーム更新処理は注意する必要がある


public class Score : MonoBehaviour
{
  #region 変数
  //===============================================//
  /// <summary>
  /// スコア
  /// </summary>
  public int Score { get; set; }
  /// <summary>
  /// テキスト
  /// </summary>
  private TextMeshProUGUI textMesh;
  //===============================================//
  #endregion

  void Awake()
  {
    textMesh = GetComponent<TextMeshProUGUI>();
  }

  void Update()
  {
    string scoreText = "Score: " + Score.ToString();
    textMesh.text = scoreText;
  }
}

この例では、文字列の連結は先ほどと異なり1回だけなので大きな問題はないが、毎フレーム呼び出す処理でゴミを生成してしまっていることが問題となっている

上記の処理であればスコアが変わったタイミングだけ更新するように変更すれば最適化できる


public class Score : MonoBehaviour
{
  #region 変数
  //===============================================//
  /// <summary>
  /// スコア
  /// </summary>
  public int Score { get; set; }
  /// <summary>
  /// 旧スコア
  /// </summary>
  private int oldScore;
  /// <summary>
  /// テキスト
  /// </summary>
  private TextMeshProUGUI textMesh;
  //===============================================//
  #endregion

  void Awake()
  {
    textMesh = GetComponent<TextMeshProUGUI>();
  }

  void Update()
  {
    if( Score != oldScore )
    {
      string scoreText = "Score: " + Score.ToString();
      textMesh.text = scoreText;
      oldScore = Score;
    }
  }
}


関数が配列の値を返す時も注意が必要である

繰り返し呼び出されることで配列用の新しいメモリが再度確保されてしまうこととなるため、配列が参照型であることを活用して値のみを更新する仕組みにできないか検討すること


public class ArraySample : MonoBehaviour
{
  /// <summary>
  /// 配列の中身を乱数に置き換える(最適化前)
  /// </summary>
  public float[] CreateRandomList(int size)
  {
    // 毎回生成する必要がある
    float[] result = new float[size];

    for ( int index = 0; index < size; ++index )
    {
      result[index] = Random.value;
    }

    return result;
  }

  /// <summary>
  /// 配列の中身を乱数に置き換える(最適化後)
  /// </summary>
  public void RandomList(float[] arrayToFill)
  {
    for ( int index = 0; index < arrayToFill.Length; ++index )
    {
      arrayToFill[index] = Random.value;
    }
  }
}

自動?それとも手動?

ガベージコレクションはデフォルトで有効になっており、周期的にガベージコレクションが実行される(正確なタイミングはプラットフォームによって異なる)

Unity2021.2 以降のガベージコレクションはインクリメンタルガベージコレクションが採用されているため、昔に比べてGCスパイクは緩和されている

しかしながら、アプリによってはGCタイミングを制御できたほうが都合が良い場合もある

ガベージコレクションを無効にして手動で動かすことができ、活用方法として大きくわけて以下の二つの方法がある


  • 小さなヒープで速く頻繁なガベージコレクション
  • 大きなヒープで遅いが頻繁でないガベージコレクション

小さなヒープで速く頻繁なガベージコレクション

この方法がベストであるのは長時間プレイし、スムーズなフレームレートを実現することが重要であるゲームの場合である

このようなゲームは一般的に小さなブロックを頻繁に割り当てするが、これらのブロックは短期間しか使用されない

定期的なフレームでガベージコレクションを要求することで、回数は増えてしまうが1回ごとの処理負荷を下げられるため、ゲームへの影響を最小で済ませることができる


if (Time.frameCount % 30 == 0)
{
  // フルGC. インクリメンタルGCについては別の方法で呼び出す
  System.GC.Collect();
}

大きなヒープで遅いが頻繁でないガベージコレクション

ガベージコレクションを有効にしている場合、ヒープのサイズが足りなくなった時点で一度ガベージコレクションを行い、それでもサイズが足りなければヒープの拡張が行われる

ほとんどのプラットフォームではヒープが拡張される場合、前回の拡張の2倍の量に拡張される

しかし、この拡張は以下の点に注意する必要がある


  • Unity ではマネージヒープが定期的に拡張される際、そのマネージヒープに割り当てられたメモリが解放されることはない。拡張したヒープの大部分が空であったとしても Unity は拡張したヒープを維持し続ける
  • ほとんどのプラットフォームでは、Unity はある時点で、マネージヒープの空部分に使用されているメモリを解放し、OSに返す。これが行われる間隔は不確実で一定ではない

メモリ割り当て(ひいてはコレクション)が相対的に頻繁ではなく、ゲームの一時停止の際に処理できるようなゲームであれば、題目の方法が有効である

ヒープを手動で拡大するには、起動中、プレースホルダーによってスペースを事前に割り当てする (すなわち、“無意味な” オブジェクトをインスタンス化して、メモリ管理の効果のために割り当てる)


public class Sample : MonoBehaviour 
{
  void Start()
  {
    var tmp = new System.Object[1024];
    
    // 大きなブロックのためにデザインされた特殊な方法で処理されるのを避けるために、小さなブロックに割り当てします
    for (int i = 0; i < 1024; i++)
    {
      tmp[i] = new byte[1024];
    }

    // 参照を解放
    tmp = null;
  }
}

    参考文献

  1. .NET ガベージコレクションの基礎
  2. Unityドキュメント 自動メモリ管理
  3. Unityドキュメント マネージメモリ