9

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

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

我也在这里看到了这些信息

但这是真的吗?

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

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

4

3 回答 3

14

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()方法,但实现IEnumerable<SomeType>了),我们强制它使用IEnumerable[<T>]/ IEnumerator<T>API,这在很大程度上必然涉及一个对象。

编辑警告:注意我在这里假设统一可以保留自定义迭代器的值类型语义;如果统一被迫将结构提升为对象,那么坦率地说,所有的赌注都没有了。

碰巧的是,如果每次分配都很重要,我很乐意说“确定,for在这种情况下使用索引器”,但从根本上说:笼统的陈述foreach是无益的和误导的。

于 2013-09-10T12:20:21.313 回答
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 分析器中的分配:

Unity Profiler 显示每次更新分配 24k

根据以下链接,这是 Unity 使用的 mono 版本中的一个错误,它强制 struct 枚举器被装箱,这似乎是一个合理的解释,尽管我没有通过代码检查进行验证:博客文章讨论装箱错误

于 2014-03-25T17:42:25.793 回答
6

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

实际上,我不得不对那篇文章持保留态度,因为它声称“调用对象上的标签属性分配并复制额外的内存” - 这里的标签是一个字符串 - 抱歉,但这根本不是真的 - 所有发生的情况是对现有字符串对象的引用被复制到堆栈上 - 这里没有额外的对象分配。——</p>

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

它不合情理,因为标签属性可以很容易地从该内部表中返回字符串,而不是创建一个新字符串,但无论出于何种原因,这就是它的工作原理。

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

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 分配(在 Unity Profiler 中)没有影响。事实上,我们看到 GC 分配随着迭代次数的增加而线性增加。

还值得注意的是, name 属性的行为方式完全相同(同样,我无法理解的原因)。

于 2014-02-15T05:16:37.540 回答