For..loop vs Foreach in Unity

Since Unity5.5 have 40kb GC issue.
I’m trying to avoid using Foreach when possible, 

Until Unity3D Team claim they fixed the extra GC issue.
The problem was the “Foreach + List” combination will generator 40kb extra garbage during process.
and this issue should be fixed after 5.5.

foreach loops
In versions of Unity prior to 5.5, a foreach loop iterating over anything other than an array generates garbage each time the loop terminates. This is due to boxing that happens behind the scenes. A System.Object is allocated on the heap when the loop begins and disposed of when the loop terminates. This problem was fixed in Unity 5.5.

Ref: fixing-performance-problems

I don’t believe that, since Foreach are requesting the List<T> to use IEnumerable interface to gain access the data, that should be Unity’s system implementation level bug.
so today I’m trying to run some test on it to find out what actually happen.

and here is the source code that I used for test cases :
for testing we generate a List<int>, List<object> and Hashset<object> to compare.

after run an 90000000 (Ninety million)

  • For..loop test on int list, End : 6393/ms,
  • Foreach (int) test on int list, End : 8010/ms,
  • Foreach test on Hastset, End : 896/ms,
using System.Collections.Generic;
using UnityEngine;
using Stopwatch = System.Diagnostics.Stopwatch;

public class ForeachTest : MonoBehaviour
{
	[SerializeField] int testCount = 90000000;
	[SerializeField] bool testCPU = false;
	[SerializeField] bool testExtraGC = false;

	private List<int> listOfInts = null;

	private class TestObj : Object { }
	private List<TestObj> listOfObjects = null;
	private HashSet<TestObj> setOfObjects = null;

	Stopwatch stopwatch = new Stopwatch();
	MemWatch memWatch = new MemWatch();

	private void Awake()
	{
		ResetMemory();
	}

	private void Update()
	{
		if (Input.GetKeyUp(KeyCode.Alpha1)) TestForloop();
		else if (Input.GetKeyUp(KeyCode.Alpha2)) TestForEach();
		else if (Input.GetKeyUp(KeyCode.Alpha3)) TestForloop2();
		else if (Input.GetKeyUp(KeyCode.Alpha4)) TestForEach2();
		else if (Input.GetKeyUp(KeyCode.Alpha5)) TestForEach3();
		else if (Input.GetKeyUp(KeyCode.Alpha6)) TestForEach4();
	}

	[ContextMenu("reset memory")]
	private void ResetMemory()
	{
		listOfInts = new List<int>(testCount);
		listOfObjects = new List<TestObj>(testCount);
		setOfObjects = new HashSet<TestObj>();
		for (int i = 0; i < testCount; i++)
		{
			listOfInts.Add(i);
			listOfObjects.Add(new TestObj());
			setOfObjects.Add(new TestObj());
		}
		
	}

	[ContextMenu("A1) Test for loop")]
	private void TestForloop()
	{
		int count = listOfInts.Count;
		StartTest();
		for (int i = 0; i < count; i++)
		{
			DoSomething(listOfInts[i]);
		}
		EndTest();
		PrintResult("For..loop test on <b>int list</b>");
		
	}

	[ContextMenu("A2) Test for each")]
	private void TestForEach()
	{
		StartTest();
		foreach (int i in listOfInts)
		{
			DoSomething(i);
		}
		EndTest();
		PrintResult("Foreach (int) test on <b>int list</b>");
	}

	[ContextMenu("B1) Test for loop on object")]
	private void TestForloop2()
	{
		int count = listOfObjects.Count;
		StartTest();
		for (int i = 0; i < count; i++)
		{
			DoSomething(listOfObjects[i]);
		}
		EndTest();
		PrintResult("For..loop test on <b>object list</b>");
	}

	[ContextMenu("B2) Test for each on object")]
	private void TestForEach2()
	{
		StartTest();
		foreach (TestObj i in listOfObjects)
		{
			DoSomething(i);
		}
		EndTest();
		PrintResult("Foreach on <b>Object list</b>");
	}

	[ContextMenu("C2) Test for each on copy list")]
	private void TestForEach3()
	{
		StartTest();
		{
			List<int> tmp = new List<int>(listOfInts);
			foreach (int i in tmp)
			{
				DoSomething(tmp[i]);
			}
		}
		EndTest();
		PrintResult($"Foreach test on <b>(copy int list)</b>");
	}

	[ContextMenu("D2) Test for each on hastset")]
	private void TestForEach4()
	{
		StartTest();
		foreach (TestObj i in setOfObjects)
		{
			DoSomething(i);
		}
		EndTest();
		PrintResult("Foreach test on <b>Hastset</b>");
	}

	private void StartTest()
	{
		stopwatch.Restart();
		memWatch.Start();
	}

	private void EndTest()
	{
		memWatch.Stop();
		stopwatch.Stop();
	}

	private void PrintResult(string prefix)
	{
		Debug.Log($"{prefix}, End : {stopwatch.ElapsedMilliseconds.ToString()}/ms, Memory usage : {memWatch.ToString()}");
	}

	private void DoSomething(TestObj obj)
	{
		if (testCPU)
			obj.GetHashCode(); // CPU time

		if (testExtraGC)
		{
			TestObj tmp = obj; // GC
		}
	}

	private void DoSomething(int value)
	{
		if (testCPU)
			value.GetHashCode();// CPU time

		if (testExtraGC)
		{
			int v = value; // GC
		}
	}
}

public class MemWatch
{
	private long _beforeTotalMemory = 0;
	private long _afterTotalMemory = 0;
	public long MemorySizeChange = 0;

	public MemWatch() { }
	//保留測量開始之基準
	public void Start()
	{
		// System.GC.Collect();
		_afterTotalMemory = 0;
		_beforeTotalMemory = System.GC.GetTotalMemory(true);
	}
	//測量從Start()至今的記憶體變化
	public void Stop()
	{
		// System.GC.Collect();
		_afterTotalMemory = System.GC.GetTotalMemory(true);
		MemorySizeChange = _beforeTotalMemory - _afterTotalMemory;
	}

	public override string ToString()
	{
		string sign = "<b>" + (_beforeTotalMemory > _afterTotalMemory ? ">" : "<") + "</b>";
		return $"Size change : {MemorySizeChangeInKB}\nBefore {_beforeTotalMemory / 1024}KB, {sign} After {_afterTotalMemory / 1024}KB";
	}

	//記憶體使用量變化(以KB計)
	public string MemorySizeChangeInKB => string.Format("{0:N0}KB", MemorySizeChange / 1024);
	//記憶體使用量變化(以MB計)
	public string MemorySizeChangeInMB => string.Format("{0:N0}MB", MemorySizeChange / 1024 / 1024);
}

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

*

這個網站採用 Akismet 服務減少垃圾留言。進一步瞭解 Akismet 如何處理網站訪客的留言資料