C#中迭代器的内存使用情况

8
使用大量迭代器在C#中对内存使用的影响是什么?假设一个程序执行了数千个foreach循环 - 每个循环是否通过调用GetEnumerator在堆上分配临时对象?CLR是否执行任何类型的优化(例如,IEnumerator对象的堆栈分配)?还是这根本不是一个值得担心的重要问题?
2个回答

13

对于大多数情况来说,它并不足够重要,不必担心。正如Eric所指出的那样,在我的经验中,这可能是一些特殊情况下才显得重要,但这种情况相当罕见。

如果你正在执行成千上万个foreach循环,那么这些循环内部实际上在进行工作,这几乎肯定比迭代器本身更加重要。

请注意,使用foreach遍历数组(已在编译时确定为数组)不使用IEnumerable<T>,而是使用直接索引。但我不会因此改变我的代码。

同以往一样,如果您关注性能,您需要对其进行测量和分析。瓶颈几乎永远不会出现在您预期的位置。


15
尽管Jon提供的建议总是很好,但我们的性能测试显示,在某些实际情况下,迭代器的堆分配/垃圾回收会对性能产生显著影响。这就是为什么List<T>的枚举器是一个邪恶的可变值类型而不是引用类型的原因。当然,关键短语是“我们的性能测试”--正如您所指出的,没有数据做出性能决策是愚蠢的。 - Eric Lippert
2
啊,可恶的可变 List<T> 迭代器 - 我记得有一篇新闻组帖子,其中有一个看起来合理的代码片段,但由于这个设计决策导致了一些非常奇怪的结果 :) 不过我还是会编辑答案...笼统地声称“这不重要”很少是一个好主意。 - Jon Skeet

2

通常编译器可以将foreach循环优化为简单循环,只需要一个索引变量在堆栈中(或处理器寄存器中)。

如果仍使用迭代器,则大多数迭代器都是结构体,因此它们只分配在堆栈上而不是堆上。

那些作为类的少数迭代器仍然非常小且快速。您可以创建数百万个它们而没有明显的影响。


听起来很不错... 快速跟进问题:如果我实现IEnumerable接口,根据定义GetEnumerator方法返回IEnumerator。 如果我的自定义迭代器是一个结构体,那么我假设它将作为装箱对象返回? - Tony the Pony
6
Jen,仔细看一下List<T>是如何做的,你就会知道如何避免装箱。诀窍在于“foreach”循环实际上不需要GetEnumerator返回IEnumerator。只要返回具有正确属性和方法的东西,就可以了。因此,通过公开一个返回结构的GetEnumerator,以及一个显式的IEnumerable<T>.GetEnumerator返回一个装箱的结构,就可以避免装箱。 - Eric Lippert
4
但是,正如Jon所说,除非你有充分的数据证明在堆上创建枚举类型会导致现实世界中影响客户性能的主要问题,否则不要做这种傻乎乎的事情。可变值类型会引起各种奇怪的问题,这些问题很难调试和理解。 - Eric Lippert
2
值得一提的是,任何由C#编译器从迭代器方法(带有yield return语句等)创建的IEnumerable/IEnumerator实现通常都是类,尽管这并不完全由规范保证。 - Jon Skeet
1
@Eric:假设在foreach表达式中使用的静态类型为List<T>而不是(比如)IList<T>,否则编译器不会知道另一个返回结构体的GetEnumerator调用,只能使用普通的IEnumerable<T>.GetEnumerator调用...因此,根据变量的静态类型,您可能会获得不同的性能特征(垃圾回收方面)以及不同的实际行为(可变性/复制)。 - Jon Skeet
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接