Unity3D系列1 : foreach对于性能到底有没有影响

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),找到一篇被广泛转发的文章:

Unity里面应尽量避免使用foreach

里面代码如下:

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()时,结果如下:
Unity3D系列1 : foreach对于性能到底有没有影响_第1张图片
可以看见在我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放入工程,结果如下:
Unity3D系列1 : foreach对于性能到底有没有影响_第2张图片

同样使用的是TestForeach()方法,不过这次已经没有GC Alloc了。

0x03 结论?

相信看完上面的朋友已经有了自己的想法了。

1.脚本直接放在Unity工程中,如果不是在Update中每帧调用,使用foreach没有太大顾虑。如果是Update中,而且是多个地方频繁使用foreach,就需要慎重考虑了。

2.脚本放入Dll中,爱咋咋地吧!

你可能感兴趣的:(unity3d)