0x00 序言
本文无意比较for和foreach谁效率更高,不会设计到for和foreach取值之类的等等。单纯探讨foreach会不会影响unity3d效率。
事情开端是这样的,之前在看unity优化的时候,遇见了这么一句:
尽量不要使用foreach,而是使用for。foreach其实会涉及到迭代器的使用,而据传说每一次循环所产生的迭代器会带来24 Bytes的垃圾。那么循环10次就是240Bytes。
由于刚接触unity以及c#不久,当时没有仔细研究内在原理,只是简单相信了这个说法。于是接下来我会在代码中尽量避免使用foreach,虽然我觉得foreach真的挺好用的。
然后,现在每次遇见遍历,我都会思考为什么foreach会有这样的行为,我想知道真正的原因,直到今天,我才深入寻找资料去论证这个观点。
0x01 c#中for比foreach指令更加精简,效率更高?
关于这个说法,百度了下(懒得翻墙去google),找到一篇被广泛转发的文章:
里面代码如下:
using UnityEngine;
using System.Collections;
public class ForeachTest : MonoBehaviour {
protected ArrayList m_array;
void Start ()
{
m_array = new ArrayList();
for (int i = 0; i < 2; i++)
m_array.Add(i);
}
void Update ()
{
for (int i = 0; i < 1000; i++)
{
foreach (int e in m_array)
{
//big gc alloc!!! do not use this code!
}
}
for (int i = 0; i < 1000; i++)
{
for (int k = 0; k < m_array.Count; k++)
{
//no gc alloc!!
}
}
}
}
初步看到代码后,很是疑惑,为什么一个简单的foreach会有这么多的内存开销。带着不解,继续搜索,直到看到知乎上这么一篇回答:
作为Unity3D的脚本而言,c#中for是否真的比foreach效率更高?
所以的疑惑在这里几乎都可以得到解答:
每一个foreach都会产生40B的内存,如果是在某个脚本的Update中使用foreach,那么每帧都会有40B的GC ALLOC。
之前在另一个地方看见过2014Unity亚洲开发者大会会议简录上有一个说法:
检测每帧都具有20B以上内存分配的选项
这几乎意味着在Update中使用foreach是不太明智的选择。关于为什么每帧20B以上内存分配不太好,我还没有仔细研究,个人猜测如果每帧20B,累计一段时间会有大量回收内存堆积,而mono回收机制里回收时间点不固定,如果隔一个较长时间点统一回收,必然会导致顿卡现象出现。
0x02 我们就不使用foreach了吗?
回答问题之前,我打算顺着 王剑飞 先生的代码验证一次,代码跟他在知乎回答里的几乎一致:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class GCTest : MonoBehaviour {
List<int> iList = new List<int>();
int count = 10;
void Awake(){
for (int i = 0; i < count; i++){
iList.Add(i);
}
}
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
TestForeach();
//TestNoForeach();
//TestFor();
//TestUsing();
}
void TestForeach()
{
//Debug.Log("TestForeach");
foreach (var e in iList){
//Debug.Log(e);
}
//foreach (var e in iList){
// foreach (var item in iList){
// //Debug.Log(e);
// }
//}
}
void TestNoForeach()
{
//Debug.Log("TestNoForeach");
var e = iList.GetEnumerator();
while (e.MoveNext()){
//Debug.Log(e.Current);
}
}
void TestFor()
{
//Debug.Log("TestFor");
for (int i = 0; i < count; ++i){
//Debug.Log(iList[i]);
}
}
void TestUsing()
{
//Debug.Log("TestUsing");
using(var e = iList.GetEnumerator()){
while (e.MoveNext()){
//Debug.Log(e.Current);
}
};
}
}
请注意,这是直接在unity工程里面添加了C#脚本,使用内置的mono编译器来编译代码。
当我在Update中使用TestForeach()时,结果如下:
可以看见在我Win7 + Unity64 环境下,foreach的确有40B的GC Alloc。
接着TestForeach()中使用注释代码:
foreach (var e in iList){
foreach (var item in iList){
//Debug.Log(e);
}
}
一共440B内存开销,这侧面印证了一个foreach会产生40B的堆内存说法。
接着在Update中使用TestNoForeach(),结果如下:
可以看到没有GC Alloc。
这样的结果似乎还是在说,不要使用foreach啊!
我又回想起来知乎上还说过:
**真相就已经出现了:
在finally里,mono编译出来的代码中有一次将valuetype的Enumerator,boxing的过程!!
What a waste!!!
这就是Unity中所带的老版本mono编译器的一个bug!**
既然是mono老版本的bug,能绕过去吗?
答案是肯定的,因为我们的项目正好是把脚本达成dll包,放入工程使用。编译脚本时,使用的是MS最新的编译器,这样是不是没问题呢?
测试一下,将新编译的dll放入工程,结果如下:
同样使用的是TestForeach()方法,不过这次已经没有GC Alloc了。
0x03 结论?
相信看完上面的朋友已经有了自己的想法了。
1.脚本直接放在Unity工程中,如果不是在Update中每帧调用,使用foreach没有太大顾虑。如果是Update中,而且是多个地方频繁使用foreach,就需要慎重考虑了。
2.脚本放入Dll中,爱咋咋地吧!
版权声明:本文为博主原创文章,未经博主允许不得转载。