【问题标题】:Memory allocation when using foreach loops in C#在 C# 中使用 foreach 循环时的内存分配
【发布时间】:2013-09-04 08:23:40
【问题描述】:

我了解 C# 中 foreach 循环如何工作的基础知识 (How do foreach loops work in C#)

我想知道是否使用 foreach 分配的内存可能会导致垃圾回收? (适用于所有内置系统类型)。

例如在System.Collections.Generic.List<T>类上使用Reflector,下面是GetEnumerator的实现:

public Enumerator<T> GetEnumerator()
{
    return new Enumerator<T>((List<T>) this);
}

每次使用都会分配一个新的枚举器(以及更多垃圾)。

所有类型都这样做吗?如果是这样,为什么? (不能重复使用单个 Enumerator 吗?)

【问题讨论】:

  • a) 这是一个实现细节。那些无关紧要。 b) GC 存在。
  • 我猜,单枚举器的主要问题是线程安全。
  • 这不限于foreach()。拥有 GC 的整个想法是小的、短寿命的对象非常便宜。我们一直在“浪费”它们,例如text = text.ToLower();。别担心。大多数重复使用方案的成本更高。
  • 任何到达此页面的人都可能有充分的理由来到这里。有时候,每次分配都很重要,如果可能的话应该避免。 (“别担心”可能在 99% 的情况下都是正确的,但当它很重要时,它可能很重要。)
  • 许多使用我最喜欢的语言 C# 的人的反性能心态,以及他们的不断抨击,一个非常大的群体,仅仅暗示有人关心低级别的性能问题(正如您在上面的第一条评论中看到的那样),过去已经有害到足以考虑这对于我们这些关心的人来说是否是正确的语言。这种心态导致 C# / .NET 十多年来一直不专注于精简和卑鄙。我很高兴现在已经用 netcore 取代了心态,我只是希望它能更快地通过队伍。

标签: c# .net loops foreach clr


【解决方案1】:

因为枚举器保留了当前项。与数据库相比,它就像一个游标。如果多个线程将访问同一个枚举器,您将失去对序列的控制。每次 foreach 查询它时,您都必须将其重置为第一项。

【讨论】:

    【解决方案2】:

    正如 cmets 中所述,这通常不是您需要担心的问题,因为这是垃圾收集的重点。也就是说,我的理解是,是的,每个 foreach 循环都会生成一个新的 Enumerator 对象,最终将被垃圾回收。要了解原因,请查看接口 here 的文档。如您所见,有一个函数调用请求下一个对象。这样做的能力意味着枚举器具有状态,并且必须知道下一个是哪个。至于为什么这是必要的,图像你正在引起集合项目的每个排列之间的交互:

    foreach(var outerItem in Items)
    {
       foreach(var innterItem in Items)
       {
          // do something
       }
    }
    

    在这里,您在同一个集合上同时有两个枚举器。显然,共享位置无法实现您的目标。

    【讨论】:

      【解决方案3】:

      不,枚举列表不会导致垃圾回收。

      List&lt;T&gt; 类的枚举器不从堆中分配内存。它是一个结构,而不是一个类,因此构造函数不分配对象,它只是返回一个值。 foreach 代码会将该值保留在堆栈上,而不是堆上。

      其他集合的枚举器可能是类,它会在堆上分配一个对象。您需要检查每种情况的枚举器类型以确定。

      【讨论】:

        【解决方案4】:

        Foreach 可能会导致分配,但至少在较新版本的 .NET 和 Mono 中,如果您正在处理具体的 System.Collections.Generic 类型或数组,则不会。这些编译器的旧版本(例如 Unity3D 在 5.5 之前使用的 Mono 版本)总是生成分配。

        C# 编译器使用duck typing 来查找GetEnumerator() 方法并在可能的情况下使用它。 System.Collection.Generic 类型上的大多数 GetEnumerator() 方法都有返回结构的 GetEnumerator() 方法,并且对数组进行了特殊处理。如果您的 GetEnumerator() 方法没有分配,您通常可以避免分配。

        但是,如果您正在处理接口IEnumerableIEnumerable&lt;T&gt;IListIList&lt;T&gt; 之一,您将始终获得分配。即使您的实现类返回一个结构,该结构也会被装箱并转换为IEnumeratorIEnumerator&lt;T&gt;,这需要分配。


        注意:自从 Unity 5.5 更新到 C# 6 后,我知道目前没有任何编译器版本仍然具有第二次分配。

        还有一个更难理解的分配。使用这个 foreach 循环:

        List<int> valueList = new List<int>() { 1, 2 };
        foreach (int value in valueList) {
            // do something with value
        }
        

        在 C# 5.0 之前,它会扩展为类似这样的内容(有一些小的差异):

        List<int>.Enumerator enumerator = valueList.GetEnumerator();
        try {
            while (enumerator.MoveNext()) {
                int value = enumerator.Current;
                // do something with value
            }
        }
        finally {
            IDisposable disposable = enumerator as System.IDisposable;
            if (disposable != null) disposable.Dispose();
        }
        

        虽然List&lt;int&gt;.Enumerator 是一个结构,不需要在堆上分配,但转换enumerator as System.IDisposable 将结构装箱,这是一个分配。规范随着 C# 5.0 更改,禁止分配,但 .NET broke the spec 并更早地优化了分配。

        这些分配非常少。请注意,分配与内存泄漏非常不同,使用垃圾收集,您通常不必担心它。但是,在某些情况下,您甚至会关心这些分配。我从事 Unity3D 工作,直到 5.5,我们无法在每个游戏帧发生的操作中进行任何分配,因为当垃圾收集器运行时,你会得到明显的颠簸。

        请注意,数组上的 foreach 循环经过特殊处理,不必调用 Dispose。据我所知,foreach 在遍历数组时从未分配过。

        【讨论】:

        • 欢迎来到 SO,并祝贺您获得如此优质的第一个答案。很少有人指出这一点。
        • 为什么你说 TestHash 不分配,如果在 IL 列表中你可以清楚地看到与 TestIList 中相同的 IDisposable 转换?还是您的意思是循环之前没有分配,但之后仍然有一个?你能澄清一下吗?
        • 也许 ildasm 现在混淆了这个问题。当我第一次回答这个问题时,我并不了解 IDisposable 分配的全部内容。 ildasm 仅显示现代 Visual Studio,它不再通过转换为 IDisposable 进行分配。如果它在那里,你会看到一个盒子说明。相反,IList 上的分配被隐藏在 GetEnumerator 调用中,因为它必须将其返回值装箱到 IEnumerator&lt;T&gt;
        • 打破规范”链接的文本更易读的版本:ericlippert.com/2011/03/14/to-box-or-not-to-box
        • 几年前,在发现产生了多少垃圾后,我不得不将游戏中的所有 foreach 循环替换为常规 for 循环。很高兴知道,谢谢。
        猜你喜欢
        • 2021-05-18
        • 1970-01-01
        • 2016-02-19
        • 2019-03-31
        • 2016-09-17
        • 1970-01-01
        • 2015-07-15
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多