关于这个话题,网络上讨论的很多,我也收集了一些资料,都不是很齐全,所以自己亲自测试,这里把结果分享给大家。

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

文件拖入其中,并且找到Unity安装目录下的:

Unity\Editor\Data\Mono\lib\mono\2.0\mscorlib.dll

也将其拖入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

虽然这个代码里面也没有newobj 字段,但是含有调用其它函数的字段callvirt instance class [mscorlib]System.Collections.IEnumerator [mscorlib]System.Array::GetEnumerator(),我们翻查这个函数调用,代码如下:

.method public final hidebysig newslot virtual
instance class System.Collections.IEnumerator GetEnumerator () cil managed
{
// Method begins at RVA 0xffd8
// Code size 7 (0x7)
.maxstack 8 IL_0000: ldarg.0
IL_0001: newobj instance void System.Array/SimpleEnumerator::.ctor(class System.Array)
IL_0006: ret
} // end of method Array::GetEnumerator

果然是出现了newobj 字段,且跟随对象类型System.Array/SimpleEnumerator,新的GC Alloc由此产生。

testIntListForeach

.method private hidebysig
instance void testIntListForeach () cil managed
{
// Method begins at RVA 0x2dfc
// Code size 77 (0x4d)
.maxstack 11
.locals init (
[0] int32,
[1] int32,
[2] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
) IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: br IL_0041
// loop start (head: IL_0041)
IL_0007: ldarg.0
IL_0008: ldfld class [mscorlib]System.Collections.Generic.List`1<int32> ForeachTest::m_intList
IL_000d: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator()
IL_0012: stloc.2
.try
{
IL_0013: br IL_0020
// loop start (head: IL_0020)
IL_0018: ldloca.s 2
IL_001a: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
IL_001f: stloc.1 IL_0020: ldloca.s 2
IL_0022: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
IL_0027: brtrue IL_0018
// end loop IL_002c: leave IL_003d
} // end .try
finally
{
IL_0031: ldloc.2
IL_0032: box valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
IL_0037: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_003c: endfinally
} // end handler IL_003d: ldloc.0
IL_003e: ldc.i4.1
IL_003f: add
IL_0040: stloc.0 IL_0041: ldloc.0
IL_0042: ldc.i4 1000
IL_0047: blt IL_0007
// end loop IL_004c: ret
} // end of method ForeachTest::testIntListForeach

同样的,这里虽然没有出现newobj 字段,却出现了:

callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator()

也就是调用了List的GetEnumerator()方法。我们翻查此方法如下:

.method public hidebysig instance valuetype System.Collections.Generic.List`1/Enumerator<!T> GetEnumerator () cil managed
{
// 方法起始 RVA 地址 0xe4928
// 方法起始地址(相对于文件绝对值:0xe2b28)
// 代码长度 7 (0x7)
.maxstack 8 // 0xE2B29: 02
IL_0000: ldarg.0
// 0xE2B2A: 73 4B 01 00 0A
IL_0001: newobj instance void valuetype System.Collections.Generic.List`1/Enumerator<!T>::.ctor(class System.Collections.Generic.List`1<!0>)
// 0xE2B2F: 2A
IL_0006: ret
} // 方法 List`1::GetEnumerator 结束

这里同样也出现了newobj指令,但是应用于值类型:

System.Collections.Generic.List`1/Enumerator

所以,这这函数调用指令也不会产生GCAlloc。

那么GCAlloc是在哪里产生的,回过头去,我们再检查上面的代码,发现在finally代码块中,有:

IL_0032: box valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>

这样一句话。它调用了box指令,尽管它box的是值类型,但此时值类型对象依然会被放至堆上,GC Alloc在由此产生。

testIntListGetEmulator

.method private hidebysig
instance void testIntListGetEmulator () cil managed
{
// 方法起始 RVA 地址 0x28e0
// 方法起始地址(相对于文件绝对值:0x0ae0)
// 代码长度 52 (0x34)
.maxstack 7
.locals init (
[0] int32,
[1] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
) // 0x0AEC: 16
IL_0000: ldc.i4.0
// 0x0AED: 0A
IL_0001: stloc.0
// 0x0AEE: 38 21 00 00 00
IL_0002: br IL_0028
// 循环开始 (head: IL_0028)
// 0x0AF3: 02
IL_0007: ldarg.0
// 0x0AF4: 7B 1A 00 00 04
IL_0008: ldfld class [mscorlib]System.Collections.Generic.List`1<int32> ForeachTest::m_intList
// 0x0AF9: 6F 53 00 00 0A
IL_000d: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator()
// 0x0AFE: 0B
IL_0012: stloc.1
// 0x0AFF: 38 00 00 00 00
IL_0013: br IL_0018
// 循环开始 (head: IL_0018)
// 0x0B04: 12 01
IL_0018: ldloca.s 1
// 0x0B06: 28 55 00 00 0A
IL_001a: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
// 0x0B0B: 3A F4 FF FF FF
IL_001f: brtrue IL_0018
// 循环结束 // 0x0B10: 06
IL_0024: ldloc.0
// 0x0B11: 17
IL_0025: ldc.i4.1
// 0x0B12: 58
IL_0026: add
// 0x0B13: 0A
IL_0027: stloc.0 // 0x0B14: 06
IL_0028: ldloc.0
// 0x0B15: 20 E8 03 00 00
IL_0029: ldc.i4 1000
// 0x0B1A: 3F D4 FF FF FF
IL_002e: blt IL_0007
// 循环结束 // 0x0B1F: 2A
IL_0033: ret
} // 方法 ForeachTest::testIntListGetEmulator 结束

这里没有newobj和box指令,而callvirt 调用的函数如前所述,是含有一个newobj指令,但是应用于值类型:

System.Collections.Generic.List`1/Enumerator

所以,这这函数调用指令也不会产生GCAlloc。所以整个函数没有GCAlloc,符合预期结果。

foreach和GetEnumerator 使用总结

我们再回过头看一下这个表格:

小结 int[] (Array) List< int > ArrayList
foreach 不产生 产生 产生
GetEnumerator 产生 不产生 产生

现在我们已经知道:

  • Array中的Enumerator是对象类型,这是intArray调用GetEnumerator产生GCAlloc的原因。
  • 泛型List中的Enumerator是值类型,所以它不会产生GCAlloc。而foreach应用于List时,由于增加了一个box装箱操作,所以产生了GCAlloc。
  • 那么我们就得出最终的如下结论:
1、 如果能使用数组,就直接使用数组,对它直接使用foreach不产生GC Alloc。
2、 尽可能不要使用数组的GetEnumerator 方法,会产生新GC Alloc。
3、 当我们需要动态数组时,最好使用List这种泛型格式。当遍历它们时,我们不要使用foreach,而应该改用GetEnumerator。
4、 尽可能避免使用ArrayList,对它的遍历操作均会产生新的GC Alloc。

版权声明:本文为博主原创文章,欢迎转载。请保留博主链接:http://blog.csdn.net/andrewfan

【Unity优化】Unity中究竟能不能使用foreach?的更多相关文章

  1. [Unity优化] Unity CPU性能优化

    前段时间本人转战unity手游,由于作者(Chwen)之前参与端游开发,有些端游的经验可以直接移植到手游,比如项目框架架构.代码设计.部分性能分析,而对于移动终端而言,CPU.内存.显卡甚至电池等硬件 ...

  2. 【Unity优化】构建一个拒绝GC的List

    版权声明:本文为博主原创文章,欢迎转载.请保留博主链接:http://blog.csdn.net/andrewfan 上篇文章<[Unity优化]Unity中究竟能不能使用foreach?> ...

  3. Unity优化方向——优化Unity游戏中的图形渲染(译)

    CPU bound:CPU性能边界,是指CPU计算时一直处于占用率很高的情况. GPU bound:GPU性能边界,同样的是指GPU计算时一直处于占用率很高的情况. 原文:https://unity3 ...

  4. Unity优化方向——优化Unity游戏中的垃圾回收(译)

    介绍 当我们的游戏运行时,它使用内存来存储数据.当不再需要该数据时,存储该数据的内存将被释放,以便可以重用.垃圾是用来存储数据但不再使用的内存的术语.垃圾回收是该内存再次可用以进行重用的进程的名称. ...

  5. [Unity 3D] Unity 3D 性能优化 (一)

    听到过很多用Unity 3D开发游戏的程序员抱怨引擎效率太低,资源占用太高,包括我自己在以往项目的开发中也头疼过.最近终于有了空闲,可以仔细的研究一下该如何优化Unity 3D下的游戏性能.其实国外有 ...

  6. 再议Unity优化

    0x00 前言 在很长一段时间里,Unity项目的开发者的优化指南上基本都会有一条关于使用GetCompnent方法获取组件的条目(例如14年我的这篇博客<深入浅出聊Unity3D项目优化:从D ...

  7. Unity优化之GC——合理优化Unity的GC

      转载请标明出处http://www.cnblogs.com/zblade/ 最近有点繁忙,白天干活晚上抽空写点翻译,还要运动,所以翻译工作进行的有点缓慢 =.= PS: 最近重新回来更新了一遍,文 ...

  8. Unity项目开发过程中常见的问题,你遇到过吗?

    最近看到有朋友问一个unity游戏开发团队,需要掌握哪些知识之类的问题.事实上Unity引擎是一个很灵活的引擎,根据团队开发游戏类型的不同,对人员的要求也有差异,所以不能一概而论.但是,一些在Unit ...

  9. 面向英特尔® x86 平台的 Unity* 优化指南: 第 1 部分

    原文地址 目录 工具 Unity 分析器 GPA 系统分析器 GPA 帧分析器 如要充分发挥 x86 平台的作用,您可以在项目中进行多种性能优化,以最大限度地提升性能. 在本指南中,我们将展示 Uni ...

随机推荐

  1. How to use Nlog for ASP.NET Core with csproj

    1. Add dependency in csproj manually or using NuGet Install the latest: NLog.Web.AspNetCore 4.5+ Upd ...

  2. Dynamic Programming and Policy Evaluation

    Dynamic Programming divides the original problem into subproblems, and then complete the whole task ...

  3. SSM001/构建maven多模块项目

    一.Idea构建maven多模块项目 1.创建maven项目--创建父模块 [1].File->New->Module... [2].点击next,填写:GroupId,ArtifactI ...

  4. oracle--少见操作、如何调整dos窗口大小、字符集设置

    如何调整dos窗口大小 1.set linesize 400; 2.右键 --默认值 断开.连接 disconn ; conn ww/ww; 关于字符集 操作系统环境变量针对语言项设置有几个,我经常设 ...

  5. Java数据结构之稀疏数组(Sparse Array)

    1.需求 编写的五子棋程序中,有存盘退出和续上盘的功能.因为该二维数组的很多值是默认值0,因此记录了很多没有意义的数据,为了压缩存储所以采用稀疏数组. 2.基本介绍 当一个数组中大部分元素为0,或者为 ...

  6. [Bzoj2004][Hnoi2010]Bus 公交线路(状压dp&&矩阵加速)

    题目链接:https://www.lydsy.com/JudgeOnline/problem.php?id=2004 看了很多大佬的博客才理解了这道题,菜到安详QAQ 在不考虑优化的情况下,先推$dp ...

  7. INPUT输入子系统——按键

    一.什么是input输入子系统? 1.1. Linux系统支持的输入设备繁多,例如键盘.鼠标.触摸屏.手柄或者是一些输入设备像体感输入等等,Linux系统是如何管理如此之多的不同类型.不同原理.不同的 ...

  8. 详解 vue 双向数据绑定的原理,并实现一组双向数据绑定

    1:vue 双向数据绑定的原理: Object.defineProperty是ES5新增的一个API,其作用是给对象的属性增加更多的控制Object.defineProperty(obj, prop, ...

  9. C#学习——控件

    Windows应用程序控件的基类是位于System.Windows.Forms命名空间的Control类. Control类定义了控件类的共同属性.方法和事件,其他的控件类都直接或间接到派生自这个类. ...

  10. C#设计模式:中介者模式(Mediator Pattern)

    一,什么是中介者模式(Mediator Pattern)? 中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性.比如:如果我们实现两个人的交互,难道我们要定义两个对象 ...