关于这个话题,网络上讨论的很多,我也收集了一些资料,都不是很齐全,所以自己亲自测试,这里把结果分享给大家。
锚点foreach究竟怎么了?
研究过这个问题的人都应该知道,就是它会引起频繁的GC Alloc。也就是说,使用它之后,尤其在Update方法中频繁调用时,会快速产生小块垃圾内存,造成垃圾回收操作的提前到来,造成游戏间歇性的卡顿。
问题大家都知道,也都给出了建议,就是尽可能不要用。在start方法里倒无所谓,因为毕竟它只执行一次。Update方法一秒钟执行大概50-60次,这里就不要使用了。这个观点整体上是正确的,因为这样做毕竟避开了问题。
不过有一点点不是很方便的就是,foreach确实带来了很多便捷性的编码。尤其是结合了var之后,那么我们究竟还能不能使用它,能使用的话,应该注意哪些问题?带着这些问题,我做了以下的测试。
锚点重现GC Alloc问题
首先,我写了一个简单的脚本来重现这个问题。
这个类中包括一个int数组,一个泛型参数为int的List。
代码如下:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class ForeachTest : MonoBehaviour {
int[] m_intArray;
List<int> m_intList;
ArrayList m_arryList;
public void Start ()
{
m_intArray = new int[2];
m_intList = new List<int>();
m_arryList = new ArrayList();
for (int i = 0; i < m_intArray.Length; i++)
{
m_intArray[i] = i;
m_intList.Add(i);
m_arryList.Add(i);
}
}
void Update ()
{
testIntListForeach();
}
void testIntListForeach()
{
for (int i = 0; i < 1000; i++)
{
foreach (var iNum in m_intList)
{
}
}
}
}
锚点应用于IntList的foreach
首先我们看应用于泛型List的情况,如下图:
这里确实是在产生GC Alloc,每帧产生39.1KB的新内存。我使用的Unity版本是64位的5.4.3f1,可能不同的版本产生的内存大小有些差别,但是产生新内存是不可避免的。
锚点应用于IntList的GetEnumerator
接下来,我又做了另外一种尝试,就是用对等的方式写出同样的代码。将测试代码部分改成如下:
for (int i = 0; i < 1000; i++)
{
var iNum = m_intList.GetEnumerator();
while (iNum.MoveNext())
{
}
}
原本以为,这个结果与上面的方式应该相同。不过结果出乎意料。
它并没产生任何的新内存。于是,我准备使用IL反编译器来了解它的GCAlloc是如何产生的。
我们知道,List是动态数组,是可以随时增长、删减的,而int[]这种形式,在C#里面被编译成Array的子类去执行。为了有更多的对比,我将foreach和GetEmulator也写一份同样的代码,应用于Int数组和ArrayList,先查看运行的结果,然后一起查看他们的IL代码。
锚点应用于IntArray的foreach
for (int i = 0; i < 1000; i++)
{
foreach (var iNum in m_intArray)
{
}
}
结果是没有产生GC Alloc。
锚点应用于IntArray的GetEnumerator
for (int i = 0; i < 1000; i++)
{
var iNum = m_intArray.GetEnumerator();
while (iNum.MoveNext())
{
}
}
结果是这里也在产生GC Alloc,每帧产生31.3KB的新内存。
锚点应用于ArrayList的foreach
for (int i = 0; i < 1000; i++)
{
foreach (var iNum in m_intArray)
{
}
}
结果是这里也在产生GC Alloc,每帧产生23.4KB的新内存(在32位版Unity5.3.4f1测试)。
锚点应用于ArrayList的GetEnumerator
for (int i = 0; i < 1000; i++)
{
var iNum = m_intArray.GetEnumerator();
while (iNum.MoveNext())
{
}
}
结果是这里也在产生GC Alloc,每帧产生23.4KB的新内存(在32位版Unity5.3.4f1测试)。
锚点GC Alloc产生情况小结
小结 int[] (Array) List< int > ArrayList
foreach 不产生 产生 产生
GetEnumerator 产生 不产生 产生
锚点探索原因
我们知道GC Alloc就是产生了新的堆内存,C#中也就意味着产生了新的对象。因此,在上面的表中,应该是意味着,只有对Array应用foreach的情况,和对泛型List应用GetEnumerator的情况下,过程中不会产生新GC Alloc,其它情况均有产生新的GC Alloc。
接下来,我找来ILSpy,将工程目录下的:
Library\ScriptAssemblies\Assembly-CSharp.dll
1
1
文件拖入其中,并且找到Unity安装目录下的:
Unity\Editor\Data\Mono\lib\mono\2.0\mscorlib.dll
1
1
也将其拖入ILSpy。(如果你使用不同的.net版本打包,则可以选择相匹配的库来看)
锚点testIntArrayForeach
.method private hidebysig
instance void testIntArrayForeach () cil managed
{
// Method begins at RVA 0x2eb4
// Code size 54 (0x36)
.maxstack 3
.locals init (
[0] int32,
[1] int32,
[2] int32[],
[3] int32
)
IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: br IL_002a
// loop start (head: IL_002a)
IL_0007: ldarg.0
IL_0008: ldfld int32[] ForeachTest::m_intArray
IL_000d: stloc.2
IL_000e: ldc.i4.0
IL_000f: stloc.3
IL_0010: br IL_001d
// loop start (head: IL_001d)
IL_0015: ldloc.2
IL_0016: ldloc.3
IL_0017: ldelem.i4
IL_0018: stloc.1
IL_0019: ldloc.3
IL_001a: ldc.i4.1
IL_001b: add
IL_001c: stloc.3
IL_001d: ldloc.3
IL_001e: ldloc.2
IL_001f: ldlen
IL_0020: conv.i4
IL_0021: blt IL_0015
// end loop
IL_0026: ldloc.0
IL_0027: ldc.i4.1
IL_0028: add
IL_0029: stloc.0
IL_002a: ldloc.0
IL_002b: ldc.i4 1000
IL_0030: blt IL_0007
// end loop
IL_0035: ret
} // end of method ForeachTest::testIntArrayForeach
虽然代码比较长,不熟悉IL的同学也不需要完整理解它们,我们只要知道少数几个重要的IL字段就可以:
newobj 指令,如果出现newobj 指令,如果跟随值类型,说明它在栈上新建对象,它不会产生GCAlloc;如果后面参数跟随对象类型,则说明它在堆上新建对象,会产生GC Alloc
callvirt 指令,它表示函数调用,后方会跟随某个类的某个函数,被调用的函数中也可能会产生GC Alloc
box指令,装箱,将值类型封装成指定的对象类型,流程是,弹出计算堆栈上的值类型参数,并使用新建立的一个引用类型对象进行并包装,将包装结果返回计算堆栈。本过程产生GC Alloc。
更具体的指令解释可以参见我的另外一篇博客《我所理解的IL指令》。
在上面常常的代码中,没有出现这三个指令,那么也就是说,这方法没有产生新的内存,符合之前的UnityProfiler中的结果。
锚点testIntArrayGetEmulator
.method private hidebysig
instance void testIntArrayGetEmulator () cil managed
{
// Method begins at RVA 0x2ef8
// Code size 51 (0x33)
.maxstack 7
.locals init (
[0] int32,
[1] class [mscorlib]System.Collections.IEnumerator
)
IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: br IL_0027
// loop start (head: IL_0027)
IL_0007: ldarg.0
IL_0008: ldfld int32[] ForeachTest::m_intArray
IL_000d: callvirt instance class [mscorlib]System.Collections.IEnumerator [mscorlib]System.Array::GetEnumerator()
IL_0012: stloc.1
IL_0013: br IL_0018
// loop start (head: IL_0018)
IL_0018: ldloc.1
IL_0019: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_001e: brtrue IL_0018
// end loop
IL_0023: ldloc.0
IL_0024: ldc.i4.1
IL_0025: add
IL_0026: stloc.0
IL_0027: ldloc.0
IL_0028: ldc.i4 1000
IL_002d: blt IL_0007
// end loop
IL_0032: ret
} // end of method ForeachTest::testIntArrayGetEmulator
原文链接:http://blog.csdn.net/andrewfan