Foreach实现原理及性能分析

foreach、GC及协程

Posted by Luffy on January 2, 2017

前言

考虑以下在U3D中的代码:

public List<int> testList = new List<int>();
	void Start()
	{
		for(int i = 0; i<10; i++)
			testList.Add(i);
	}
	// Update is called once per frame
	void Update () 
	{
		int testInt;
		//使用for循环
		for(int i = 0; i < testList.Count;i++)
		{
			testInt = i;
		}
		//使用foreach循环
		foreach(int i in testList)
		{
			testInt = i;
		}
	}

通过分别使用for和foreach进行循环操作,会发现,在使用foreach进行循环迭代时,会在每帧产生40B的GC Alloc。产生这种现象的原因是什么?如何进行优化?下文将详细讲解。

这里有相关问题的相关讨论。

Foreach实现原理

Foreach是c#的语法糖,通过封装迭代过程的实现细节,可以更加便捷的实现迭代。以下,参考自coderbetter一篇文章来探索foreach实现细节。

foreach(Student student in myClass)
Debug.Log(student);

为实现foreach,myClass需要实现__IEnumerable__接口,其中需要实现public IEnumerator GetEnumerator(),而IEnumerator接口需要实现:

  • public object Current;
  • public void Reset();
  • public bool MoveNext();

而其中IEnumerator的实现如下所示:

	private class ClassEnumerator : IEnumerator
		{
	    private ClassList _classList;
        private int _index;
        public ClassEnumerator(ClassList classList)
        {
            _classList = classList;
            _index = 1;
        }
        public void Reset()
        {
            _index = 1;
        }
        public object Current
        {
            get
            {
                return _classList._students[_index];
            }
        }
        public bool MoveNext()
        {
            _index++;
            if (_index >= _classList._students.Count)
                return false;
            else
                return true;
        }
	}

Foreach语法糖可以更方便的进行迭代,否则需要手动实现IEnumerable和IEnumerator接口。

Foreach与GC

回到开头的问题,为什么在使用foreach时会产生额外的GC呢?参考自这篇博客,可以得出为什么在U3D中会出现这种问题。
先假设有这样一段代码:

void Update()
{
	var fibonacci = new List<int>{1,1,2,3,5,8,13};
	foreach(var f in fibonacci)
		Debug.Log(f);
}

__Foreach__在实现时,等同于:

using(var enumerator = fibonacci.GetEnumerator())
{
	while(enumerator.MoveNext())
   {
     var current = enumerator.Current;
     Console.WriteLine(current);
   }
}

而在老版本的mono编译器中,将using中的表达式,例如using (ResourceType resource = expression) 转换为:

{
	ResourceType resource = expression;
	try
	{
	   statement;
	}finally{
		((IDisposable)resource).Dispose();
	}
}

而将resource(值类型)转换为IDisposable接口类型时,会发生装箱操作,这也就是引起GC的原因。

c#编译器优化

在新版本的编译器中,会判断resource类型,若为值类型,则直接finally{resource.Dispose();}避免装箱操作。关于具体的using相关转换,可参考这篇博文

##StartCoroutine U3D中StartCoroutine也利用了迭代器的原理,yield关键字使程序在确定时间点停止给定时间–>保存状态–>一定时间后继续运行。使用协程可以实现延迟操作。在如_pathfinding_等操作,分为多帧运行可以缓解运行压力。 yield关键字实现通过使用状态机实现

如:

public IEnumerable<int> CountFrom(int start)
{
	for(int i = start;i<=limit;i++)
		yield return i;
}

内部转换:

public bool MoveNext()
{
	switch(state $0)
	{
		case0:goto resume$0;
		case1:goto resume$1;
		case2:goto false;
	}
	resume$0:;
	for(int i=start;i<=this.$0.limit;i++)
	{
		curent$0 = i;
		state&0=1;
		return true;
	}
	resume$1:;
}

大概思路如上所示,通过状态机来记录程序运行的位置,已完成yield关键字操作。

在U3D中,StartCoroutine吸收IENumerator和yield核心思想,实现延迟操作。在stackoverflow上,有一篇关于实现原理的分析

总结

Foreach出发,分析实现原理引出IENumerator实现和yield的原理。最后,在给出关于StartCoroutine实现原理的一篇文章。