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

jim*_*zer 8 c# mono foreach garbage-collection unity-game-engine

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

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

我也在这里看到这些信息

但这是真的吗?

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

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

Mar*_*ell 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
}
Run Code Online (Sandbox Code Playgroud)

但是,这个对象分配:

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

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

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

碰巧的是,for如果每个分配都很重要,我会很高兴地说"确定,使用和在这种情况下使用索引器",但从根本上说:全面的声明foreach是无益的和误导性的.


Chr*_*ell 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;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这显示了Unity Profiler中的分配:

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

根据以下链接,它是Unity使用的单声道版本中的一个错误,它强制结构枚举器被装箱,这似乎是一个合理的解释,虽然我没有通过代码检查验证:博客文章讨论拳击错误

  • 我在Unity 5.2.0.f3中重复了这个测试,我发现它每帧生成~39.1 KB(每次迭代40个字节). (2认同)

Kyl*_*e G 5

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

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

实际上,调用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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

还值得注意的是,name属性的行为方式完全相同(再次,为什么我不能这样做).