【Unity优化】Unity中究竟能不能使用foreach?
关于这个话题,网络上讨论的很多,我也收集了一些资料,都不是很齐全,所以自己亲自测试,这里把结果分享给大家。
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?的更多相关文章
- [Unity优化] Unity CPU性能优化
前段时间本人转战unity手游,由于作者(Chwen)之前参与端游开发,有些端游的经验可以直接移植到手游,比如项目框架架构.代码设计.部分性能分析,而对于移动终端而言,CPU.内存.显卡甚至电池等硬件 ...
- 【Unity优化】构建一个拒绝GC的List
版权声明:本文为博主原创文章,欢迎转载.请保留博主链接:http://blog.csdn.net/andrewfan 上篇文章<[Unity优化]Unity中究竟能不能使用foreach?> ...
- Unity优化方向——优化Unity游戏中的图形渲染(译)
CPU bound:CPU性能边界,是指CPU计算时一直处于占用率很高的情况. GPU bound:GPU性能边界,同样的是指GPU计算时一直处于占用率很高的情况. 原文:https://unity3 ...
- Unity优化方向——优化Unity游戏中的垃圾回收(译)
介绍 当我们的游戏运行时,它使用内存来存储数据.当不再需要该数据时,存储该数据的内存将被释放,以便可以重用.垃圾是用来存储数据但不再使用的内存的术语.垃圾回收是该内存再次可用以进行重用的进程的名称. ...
- [Unity 3D] Unity 3D 性能优化 (一)
听到过很多用Unity 3D开发游戏的程序员抱怨引擎效率太低,资源占用太高,包括我自己在以往项目的开发中也头疼过.最近终于有了空闲,可以仔细的研究一下该如何优化Unity 3D下的游戏性能.其实国外有 ...
- 再议Unity优化
0x00 前言 在很长一段时间里,Unity项目的开发者的优化指南上基本都会有一条关于使用GetCompnent方法获取组件的条目(例如14年我的这篇博客<深入浅出聊Unity3D项目优化:从D ...
- Unity优化之GC——合理优化Unity的GC
转载请标明出处http://www.cnblogs.com/zblade/ 最近有点繁忙,白天干活晚上抽空写点翻译,还要运动,所以翻译工作进行的有点缓慢 =.= PS: 最近重新回来更新了一遍,文 ...
- Unity项目开发过程中常见的问题,你遇到过吗?
最近看到有朋友问一个unity游戏开发团队,需要掌握哪些知识之类的问题.事实上Unity引擎是一个很灵活的引擎,根据团队开发游戏类型的不同,对人员的要求也有差异,所以不能一概而论.但是,一些在Unit ...
- 面向英特尔® x86 平台的 Unity* 优化指南: 第 1 部分
原文地址 目录 工具 Unity 分析器 GPA 系统分析器 GPA 帧分析器 如要充分发挥 x86 平台的作用,您可以在项目中进行多种性能优化,以最大限度地提升性能. 在本指南中,我们将展示 Uni ...
随机推荐
- 类TreeMap
TreeMap类 import java.util.Set; import java.util.TreeMap; public class IntegerDemo { public static vo ...
- MFC下一个通用非阻塞的等待执行结束的对话框类
头文件:CPictureEx用于显示一个等待动画 #pragma once #include "afxwin.h" #include "resource.h" ...
- Postman + Newman 生成测试报告
1.安装Node.js 下载地址: https://nodejs.org/download/ 2.安装Newman 1) 打开cmd,输入:npm install -g newman 2) 安装支持N ...
- 关于 5.4 Eloquent ORM first() 与 get() 判断是否为空
例如: $model = Model::first(); 可以通过is_null()来判断 $model = Model::get(); laravel自带了一个方法 $model->isEm ...
- ubuntu 上用virtualenv安装python不同版本的开发环境。
1.用pip安装virtualenv apt-get install python-virtualenv 2.创建python2的虚拟环境,进入要创建虚拟环境的目录下,我是放在/home/pyenv/ ...
- iptables防火墙常用命令
iptables防火墙启动停止和基本操作 iptables是centos7之前常用的防火墙,在centos7上使用了firewall 防火墙基本操作: # 查询防火墙状态 service iptabl ...
- C#异步:AsyncCallback和IAsyncResult
在线程池异步的执行委托,异步编程模型 msdn官方解释:https://msdn.microsoft.com/zh-cn/library/ms228972(VS.80).aspx 使用AsyncCal ...
- [AtCoder ARC076] F Exhausted?
霍尔定理 + 线段树? 咱学学霍尔定理... 霍尔定理和二分图完美匹配有关,具体而言,就是定义了二分图存在完美匹配的充要条件: 不妨设当前二分图左端集合为 X ,右端集合为 Y ,X 与 Y 之间的边 ...
- mysql考试复习
基础创建 字段自动编号auto_increment ( 单词补充:increment 定期的加薪; 增量; 增加) 考点 添加自增 alter table [表名] modify [字段(id)] i ...
- OC学习--面向对象的个人理解
1. 什么是面向对象? 以下一段话是我在百度上找的解释: 面向对象(Object Oriented,OO)是软件开发方法.面向对象的概念和应用已超越了程序设计和软件开发,扩展到如数据库系统.交互式界面 ...