我的朋友在 C# 上使用 Unity3D。他特别告诉我:
每个“foreach”循环的每次迭代都会产生 24 字节的垃圾内存。
我也在这里看到了这些信息
但这是真的吗?
与我的问题最相关的段落:
用简单的“for”循环替换“foreach”循环。出于某种原因,每个“foreach”循环的每次迭代都会产生 24 字节的垃圾内存。一个简单的循环迭代 10 次留下 240 字节的内存准备被收集,这是不可接受的
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
是无益的和误导的。
你的朋友是对的,从版本 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 使用的 mono 版本中的一个错误,它强制 struct 枚举器被装箱,这似乎是一个合理的解释,尽管我没有通过代码检查进行验证:博客文章讨论装箱错误
这里有一些关于 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 属性的行为方式完全相同(同样,我无法理解的原因)。