简体   繁体   English

从列表创建枚举器时 GC collections<t> 和 IList<t></t></t>

[英]GC collections when creating Enumerator from List<T> and IList<T>

I was reading this comment by casablanca in https://softwareengineering.stackexchange.com/a/411324/109967 :我正在阅读 casablanca 在https://softwareengineering.stackexchange.com/a/411324/109967中的评论:

There's another caveat here: value types are boxed onto the heap if accessed via an interface, so you'd still incur a heap allocation if enumerating via IList or IEnumerable.这里还有一个警告:如果通过接口访问,值类型将被装箱到堆上,因此如果通过 IList 或 IEnumerable 枚举,您仍然会产生堆分配。 You'd have to be holding onto a concrete List instance to avoid the allocation.您必须持有一个具体的 List 实例以避免分配。

I wanted to test this theory using this .NET 5 console app (I've tried swapping between the List<T> and IList<T> ):我想使用这个 .NET 5 控制台应用程序来测试这个理论(我尝试在List<T>IList<T>之间交换):

using System;
using System.Collections.Generic;

namespace ConsoleApp10
{    
    class Program
    {
        static void Main(string[] args)
        {
            IList<int> numbers = new List<int> { 0, 1, 2, 3, 4, 5 };
            //List<int> numbers = new List<int> { 0, 1, 2, 3, 4, 5 };

            for (int i = 0; i < 2000; i++)
            {
                foreach (var number in numbers)
                {

                }
            }
            GC.Collect();
            Console.WriteLine("GC gen 0 collection count = " + GC.CollectionCount(0)); //Always 1
            Console.WriteLine("GC gen 1 collection count = " + GC.CollectionCount(1)); //Always 1
            Console.WriteLine("GC gen 2 collection count = " + GC.CollectionCount(2)); //Always 1
                
        }
    }
}

It appears as though all generations get full and are GCed.似乎所有世代都已满并被 GCed。

If List<T> uses a struct Enumerator , then there should not be any heap allocations happening up until GC.Collect() is called (afterwards string objects are created in the calls to Console.WriteLine() but that happens after the GC runs).如果List<T>使用 struct Enumerator ,那么在调用GC.Collect()之前不应该发生任何堆分配(之后在调用Console.WriteLine()时会创建字符串对象,但这会在 GC 运行之后发生)。

Question 1: Why are heap allocations happening when using List<T> ?问题 1:为什么在使用List<T>时会发生堆分配?

Question 2: I understand that there would be heap allocations due to the reference type Enumerator used when iterating a List<T> via the IList<T> interface (assuming the comment in the linked question is correct), but why does a collection happen in all 3 generations?问题 2:我知道由于通过IList<T>接口迭代List<T>时使用的引用类型Enumerator会存在堆分配(假设链接问题中的注释是正确的),但为什么会发生集合在所有 3 代?

2000 Enumerator objects is a lot of objects, but once the foreach loop completes, it's ready to be GCed because nothing is referring to the object after the foreach completes. 2000 Enumerator objects 是很多对象,但是一旦 foreach 循环完成,它就可以被 GCed 了,因为在 foreach 完成后没有任何东西引用 object。 Why are the objects making it through to Gen 1 and Gen 2?为什么物体会进入第 1 代和第 2 代?

You're confused by what GC.CollectionCount() is showing you, I think.我认为,您对GC.CollectionCount()向您展示的内容感到困惑。

Every time you call GC.Collect() , you force a full collection of all generations.每次调用GC.Collect()时,都会强制收集所有代。 You then call GC.CollectionCount() , which is showing you that all generations have just been collected.然后调用GC.CollectionCount() ,它显示所有代都刚刚被收集。 You're just observing the effects of calling GC.Collect() !您只是在观察调用GC.Collect()的效果!

It's true that the IList version is allocating enumerators, but those are dying quickly in gen0.确实, IList版本正在分配枚举器,但它们在 gen0 中很快就消失了。

The proper tool to exmaine this sort of stuff is BenchmarkDotNet with the MemoryDiagnoser .检查这类东西的合适工具是带有MemoryDiagnoserBenchmarkDotNet

I put together a simple benchmark:我整理了一个简单的基准:

public static class Program
{
    public static void Main()
    {
        BenchmarkRunner.Run<Benchmarks>();
    }
}

[MemoryDiagnoser]
public class Benchmarks
{
    [Benchmark]
    public int IList()
    {
        int sum = 0;
        IList<int> numbers = new List<int> { 0, 1, 2, 3, 4, 5 };

        for (int i = 0; i < 2000; i++)
        {
            foreach (var number in numbers)
            {
                sum += number;
            }
        }
        return sum;
    }

    [Benchmark]
    public int List()
    {
        int sum = 0;
        List<int> numbers = new List<int> { 0, 1, 2, 3, 4, 5 };

        for (int i = 0; i < 2000; i++)
        {
            foreach (var number in numbers)
            {
                sum += number;
            }
        }
        return sum;
    }
}

This produced the results:这产生了结果:

BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19042.985 (20H2/October2020Update)
Intel Core i5-6300U CPU 2.40GHz (Skylake), 1 CPU, 4 logical and 2 physical cores
.NET SDK=5.0.300-preview.21180.15
  [Host]     : .NET 5.0.5 (5.0.521.16609), X64 RyuJIT
  DefaultJob : .NET 5.0.5 (5.0.521.16609), X64 RyuJIT
Method方法 Mean意思是 Error错误 StdDev标准差 Gen 0 0代 Gen 1第一代 Gen 2第 2 代 Allocated已分配
IList列表 149.42 μs 149.42 微秒 2.902 μs 2.902 微秒 3.563 μs 3.563 微秒 51.0254 51.0254 - - - - 80,128 B 80,128 乙
List列表 48.98 μs 48.98 微秒 0.961 μs 0.961 微秒 1.524 μs 1.524 微秒 - - - - - - 128 B 128乙

The 128B allocation that's common to both will be the List<T> instance itself.两者共有的 128B 分配将是List<T>实例本身。 Other than this see how the IList version is allocating about 80KB more, causing 51 new gen0 collections per 1000 operations?除此之外,看看IList版本如何分配大约 80KB 的空间,导致每 1000 次操作产生 51 个新的 gen0 collections?

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM