Unity 3 D里foreach,usingとCoroutineのGC問題の探究と解決策


Foreach:
多くのUnity 3 Dの最適化テクニックは、いくつかの会社の筆記試験問題でforeachがGC Allocを生成することに関連しています.そのため、ゲームの実行時、特にUpdateではforeachを使用するこの注意事項をできるだけ避けなければなりません.
foreachは本当にGC Allocを生みますか?次のテストを行います.(Unity 3 D 5.4.0)
スクリプトの作成cs:
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

public class TestForeach : MonoBehaviour 
{
	private int[] _arr = new int[] { 1, 2, 3 };
	private List _list = new List();
	private Dictionary _dic = new Dictionary();
	//private IEnumerator _i;
	private Dictionary.Enumerator _i;
	
	void Start()
	{
		_list.Add(1);
		_list.Add(2);
		_list.Add(3);
		
		_dic.Add(1, 1);
		_dic.Add(2, 2);
		_dic.Add(3, 3);
	}
	
	void Update()
	{
		#region Array
		// code Array for
		//for (int ii = 0; ii < _list.Count; ++ii)
		//{}
		
		// code Array foreach
		//foreach (int i in _arr)
		//{}
		#endregion
		
		#region List
		// code List for
		//for (int ii = 0; ii < _list.Count; ++ii)
		//{}
	
		// code List foreach
		//foreach (int i in _list)
		//{}

        //  foreach unity C# :
        //IEnumerator iter = _list.GetEnumerator();
        //while (iter.MoveNext())
        //{ }

        //  foreach unity5.3.5p8 C# :
        //List.Enumerator iter = _list.GetEnumerator();
        //while (iter.MoveNext())
        //{ }
		
		//_i = _list.GetEnumerator();
		//while (_i.MoveNext())
		//{}
		#endregion
		
		#region Dictionary
		//foreach (var m in _dic)
		//{}
		
		//_i = _dic.GetEnumerator();
		//while (_i.MoveNext())
		//{}

        //using(Dictionary.Enumerator iter = _dic.GetEnumerator())
        //{
        //    while (iter.MoveNext())
        //    { }
        //}

        //  using unity C# :
        //Dictionary.Enumerator iter = _dic.GetEnumerator();
        //try
        //{ }
        //finally
        //{
        //    ((IDisposable)iter).Dispose();
        //}

        //  unity5.3.5p8  :
        //Dictionary.Enumerator iter = _dic.GetEnumerator();
        //try
        //{ }
        //finally
        //{
        //    iter.Dispose();
        //}
		#endregion
	}
}

新しい空のシーンのカメラにスクリプトを掛け、各コードブロックのコメントを順番に削除し、Profilerを開き、CPUモジュールを表示し、GC Alloc項目でソートして、次の結果を得ます.
 
Array for
 
Array foreach
 
List for
 
List foreach
 
List GetEnumerator
 
Dictionary foreach
 
Dictionay GetEnumerator
以上からforeach対配列にはGC Allocはないことが分かるが,ListとDictionaryコンテナにはGC Allocがあり,foreachと反復によるGC Allocはいずれも40 Bである.
C#では、foreach文はマイクロソフトが提供する文法糖であり、C#内蔵反復器の使用複雑さを簡略化することができます.コンパイラはforeach文のコンパイル時にGetEnumeratorとMoveNextメソッドとCurrent属性を呼び出すコードを生成します.これらのコードと属性はC#内蔵反復器で提供されているので、上のforeach文と対応する反復器文は等価なので、GC Allocも同じです.
では、反復文はなぜGC Allocを生み出すのでしょうか.Dictionayを例にとると(同様にListに適用),dic.GetEnumerator()はDictionaryを返します.Enumerator、これはStructですが、iはIEnumeratorタイプの参照です.ここでは、値タイプ変数を参照タイプ変数に変換する必要があります.梱包操作を1回実行する必要があります.梱包操作はCPUとメモリリソースを追加で消費する必要があります(参照:http://www.cnblogs.com/yukaizhao/archive/2011/10/18/csharp_box_unbox_1.html)ということでGC Allocが生まれます.
では、私たちは_i Dictionaryと宣言する.Enumeratorタイプは、箱詰め操作はありませんが、GCALOcはありますか?テスト結果は次のとおりです.
private Dictionary.Enumerator _i;
本当にGC Allocはありません!
ではforeachはなぜこのような箱詰め操作を必要としないコードを生成しないのでしょうか.これは実は初期Mono C#コンパイラのバグで、その後のバージョンは修復されましたが、現在Unity 3 D用は未修復のバージョンです.の
Unity 5.3.5 f 1は1つのアップグレードパッケージを提供して、5.3.5 p 8にアップグレードすることができて、中にはMono C#コンパイラに対するアップグレードが含まれて、Mono 4.4にアップグレードしてこの問題を修復することができて、リンクアドレス:
http://forum.unity3d.com/threads/upgraded-c-compiler-on-5-3-5p8.417363/
このアップグレードパッケージをUnity 3 D 5.3.5 f 1にインストールすると、foreachは確かにGC Allocがないことがわかりますが、5.3.6および現在最新の5.4.0でforeachのGC Allocの問題がまだ存在し、このアップグレードパッケージは5.3.5テスト用だけで、後続のUnity 3 DバージョンのMono C#コンパイラを正式にアップグレードしていないと推測されます.では、コンパイラのバージョンを自分でアップグレードできますか?もちろんいいです.真似してもいいです.https://bitbucket.org/jbruening/unity-c-5.0-and-6.0-integrationより新しいコンパイラにアップグレードする方法を提供します.
もう一つの折衷的な解決策は、一般的に私たちのC#論理コードがAssembly-Charpにコンパイルされます.dllでは、このコンパイルプロセスはUnity 3 Dが指定したC#コンパイラで自動的に行われます.論理コードをVSでdllにコンパイルし、Assembly-CSharpにします.dll引用でGC Allocは生まれないのでしょうか?テストした結果、この方法は実行可能である:)~
また、上記のusing()の使い方でもGC Allocが発生し、箱を分解して箱詰めしたもの(具体的には上のコードを参照)でも、同じ5.3.5 p 8コンパイラのアップグレード後にGC Allocは発生しません.
だからforeachがGC Allocを生み出す問題については以下のように提案します.
1.ListやDictionaryなどの容器の遍歴にはforeachは使用せず、forと反復器(上で最適化したもの)を使用でき、配列foreachには安心して使用できる:)
2.Unity 3 D 5.3.5 f 1バージョンについて、公式提供の5.3.5 p 8アップグレードパッケージをインストールしてみると、foreachとusingを安心して使用できます:)
3.C#コンパイラバージョンのアップグレード(現在のプロジェクトのみ、Unity独自のC#コンパイラのアップグレードではありません)
4.論理コードはVS単独でdllにコンパイルすれば、foreachとusingを安心して使える
5.もしあなたが5.3.5バージョンでなくてもコードを単独でdllにコンパイルしたくないなら、foreachとusing(便利だと思います)を使いたいなら、Updateでは絶対に使わないことをお勧めします!!!
 
Coroutine:
CoroutineでもGC Allocは発生しますか?私たちは以下のテストを行います:(Unity 3 D 5.3.5 f 1、なぜこのバージョンを選んだのか、以下を見てから分かります:))
コードを作成するcs:
using UnityEngine;
using System.Collections;

public class TestCoroutine : MonoBehaviour
{
	void Start()
	{
		StartCoroutine(_UnityCoroutine());
	}
	
	IEnumerator _UnityCoroutine()
	{
		while (true)
		{
			yield return null;
		}
	}
}

新しい空のシーンのカメラにスクリプトを掛け、Profilerを開き、CPUモジュールを表示し、GC Allocアイテムでソートして、次の結果を得ます.
Coroutineは確かにGC Allocが発生するようですが、この問題は5.3.6バージョンで修正され、5.3.6バージョンの発行説明を確認します(https://unity3d.com/cn/unity/whats-new/unity-5.3.6)が表示されます.
5.3.6および最新の5.4.0 CoroutineではGC Allocが発生しないことが確認されました.また、上記の5.3.5 f 1をインストールしたアップグレードパッケージが5.3.5 p 8にアップグレードされた後も、このGCの問題はなくなりました.
では、5.3.5以前のバージョンの開発者はどうすればいいのでしょうか.答えはUnity 3 DのCoroutineを使わず、自分でセットを実現すること..ただし、AssetStoreで重複造輪が代替案More Effective Coroutines(MECと略称)を見つけないようにするには、次の手順に従います.https://www.assetstore.unity3d.com/en/#!/content/54975は、名前の通りGCの問題を回避するのでより効率的です.プラグインには詳細なチュートリアルがあります.ここでは説明しないで、直接テストします.
TestCoroutine.csは以下のように変更されました.
using UnityEngine;
using System.Collections.Generic;
using MovementEffects;

public class TestCoroutine : MonoBehaviour
{
	void Start()
	{
		Timing.RunCoroutine(_UnityCoroutine());
	}
	
	IEnumerator _UnityCoroutine()
	{
		while (true)
		{
			yield return 0;
		}
	}
}

テスト結果は次のとおりです.
確かにGC Allocは発生しませんので、5.3.5以前のバージョンではUnity 3 DのCoroutineの代わりにこのプラグインを使用できます.また、いくつか注意すべき点があります.
1.このプラグインで使用するAPIの一部はUnity 5のみであるため.xならではなのでUnity 4を使えばxこれらのAPIに使用されるコードを#if UNITY_とする必要がある5#endifパッケージを無効にします.
2.直接Timingを使用する場合.RunCoroutineの場合は、まず彼の戻りを保存し、現在のスクリプトが破棄されるときにOnDestroy()にTimingを追加する必要があります.KillCoroutines(前の戻り)は、現在のスクリプトのgameObjectに直接Timingコンポーネントを掛け、gameObject.GetComponent.RunCoroutineは、廃棄時にKillCoroutinesを使わなくなります.自動的に廃棄されるからです.