每个“foreach”循环的每次迭代都会生成24字节的垃圾内存?

时间:2013-09-10 11:59:10

标签: c# unity3d foreach mono garbage-collection

我的朋友在C#上使用Unity3D。他特意告诉我:

  

每个“foreach”循环的每次迭代都会产生24字节的垃圾   存储器中。

我也看到了这些信息here

但这是真的吗?

与我的问题最相关的段落:

  

用简单的“for”循环替换“foreach”循环。由于某些原因,   每个“foreach”循环的每次迭代都会产生24字节的垃圾   记忆。一个简单的循环迭代10次,留下240字节的内存   准备收集,这是不可接受的

3 个答案:

答案 0 :(得分:12)

foreach是一个有趣的野兽;很多人错误地认为它与IEnumerable[<T>] / IEnumerator[<T>]有关,但事实并非如此。因此,毫无意义地说:

  

每个“foreach”循环的每次迭代都会生成24字节的垃圾内存。

特别是,它取决于你正在循环的内容。例如,很多类型都有自定义迭代器 - 例如List<T> - 有一个基于struct的迭代器。以下分配零对象

// assume someList is a List<SomeType>
foreach(var item in someList) {
   // do something with item
}

但是,这个会进行对象分配:

IList<SomeType> data = someList;
foreach(var item in data) {
   // do something with item
}

看起来相同但不是 - 通过更改foreach来迭代IList<SomeType>(没有自定义GetEnumerator()方法,但实现{{1} })我们强制它使用IEnumerable<SomeType> / IEnumerable[<T>] API,这很大程度上涉及一个对象。

编辑警告:注意我在这里假设unity可以保留自定义迭代器的值类型语义;如果团结被迫将结构提升为物体,那么坦率地说所有的赌注都会被取消。

碰巧,我很乐意说“确定,在这种情况下使用IEnumerator<T>和索引器”如果每个分配都很重要,但从根本上说:for上的一揽子声明是无益的,误导。

答案 1 :(得分:10)

您的朋友是正确的,从版本4.3.4开始,以下代码将在Unity中每帧生成24k垃圾:

using UnityEngine;
using System.Collections.Generic;

public class ForeachTest : MonoBehaviour 
{
    private string _testString = "this is a test";
    private List<string> _testList;
    private const int _numIterations = 10000;

    void Start()
    {
        _testList = new List<string>();
        for(int i = 0; i < _numIterations; ++i)
        {
            _testList.Add(_testString);
        }
    }

    void Update()
    {
        ForeachIter();
    }

    private void ForeachIter()
    {
        string s;

        for(int i = 0; i < 1000; ++i)
        {
            foreach(string str in _testList)
            {
                s = str;
            }
        }
    }
}

这显示了Unity Profiler中的分配:

Unity Profiler showing 24k of allocation per update

根据以下链接,这是Unity使用的mono版本中的一个错误,它强制结构枚举器被装箱,这似乎是一个合理的解释,虽然我没有通过代码检查验证:Blog post discussing the boxing bug

答案 2 :(得分:5)

关于foreach,这里有一些很好的建议,但我必须对Mark上面关于标签的评论表示不满(不幸的是,我没有足够的代表在他的评论下面发表评论),他说,

  

实际上,我必须用一小撮盐来拿这篇文章,因为它   声明“调用对象上的tag属性分配和复制   额外的内存“ - 这里的标签是一个字符串 - 抱歉,但是   根本不是真的 - 所有发生的事情都是对它的引用   现有的字符串对象被复制到堆栈上 - 没有额外的   对象分配在这里。 -

实际上,调用tag属性确实会产生垃圾。我的猜测是内部标签存储为整数(或其他简单类型),并且当调用tag属性时,Unity使用内部表将int转换为字符串。

它无视原因,因为tag属性可以很容易地从该内部表返回字符串而不是创建新字符串,但无论出于何种原因,这都是它的工作方式。

您可以使用以下简单组件自行测试:

public class TagGarbageTest : MonoBehaviour
{
    public int iterations = 1000;
    string s;
    void Start()
    {
        for (int i = 0; i < iterations; i++)
            s = gameObject.tag;
    }
}

如果gameObject.tag没有产生垃圾,那么增加/减少迭代次数对GC Allocation(在Unity Profiler中)没有影响。实际上,随着迭代次数的增加,我们会看到GC分配的线性增长。

值得注意的是,name属性的行为方式完全相同(再次,为什么我不知道)。