从 List<T> 和 IList<T> 创建 Enumerator 时的 GC 收集
GC collections when creating Enumerator from List<T> and IList<T>
我正在 https://softwareengineering.stackexchange.com/a/411324/109967 阅读 casablanca 的评论:
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. You'd have to be holding
onto a concrete List instance to avoid the allocation.
我想使用这个 .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
}
}
}
似乎所有代都已满并进行了 GC。
如果 List<T>
使用结构 Enumerator
,那么在调用 GC.Collect()
之前不应该发生任何堆分配(之后在对 [= 的调用中创建字符串对象) 18=] 但这发生在 GC 运行之后。
问题 1:
为什么在使用 List<T>
时会发生堆分配?
问题二:
我知道由于通过 IList<T>
接口迭代 List<T>
时使用的引用类型 Enumerator
会进行堆分配(假设链接问题中的注释是正确的),但为什么所有 3 代都有一个集合?
2000 枚举器对象是很多对象,但是一旦 foreach 循环完成,它就可以进行 GC,因为在 foreach 完成后没有任何对象引用该对象。为什么对象会进入第 1 代和第 2 代?
我想你对 GC.CollectionCount()
向你展示的内容感到困惑。
每次调用GC.Collect()
,都会强制所有代的全集。然后你调用 GC.CollectionCount()
,它告诉你所有的世代都被收集了。您只是在观察调用 GC.Collect()
!
的效果
确实 IList
版本正在分配枚举器,但这些枚举器在 gen0 中很快就会消亡。
检查此类内容的正确工具是 BenchmarkDotNet with the MemoryDiagnoser。
我整理了一个简单的基准:
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;
}
}
这产生了结果:
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
Gen 1
Gen 2
Allocated
IList
149.42 μs
2.902 μs
3.563 μs
51.0254
-
-
80,128 B
List
48.98 μs
0.961 μs
1.524 μs
-
-
-
128 B
两者共有的 128B 分配将是 List<T>
实例本身。除此之外,看看 IList
版本如何多分配大约 80KB,导致每 1000 次操作产生 51 个新的 gen0 集合?
我正在 https://softwareengineering.stackexchange.com/a/411324/109967 阅读 casablanca 的评论:
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. You'd have to be holding onto a concrete List instance to avoid the allocation.
我想使用这个 .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
}
}
}
似乎所有代都已满并进行了 GC。
如果 List<T>
使用结构 Enumerator
,那么在调用 GC.Collect()
之前不应该发生任何堆分配(之后在对 [= 的调用中创建字符串对象) 18=] 但这发生在 GC 运行之后。
问题 1:
为什么在使用 List<T>
时会发生堆分配?
问题二:
我知道由于通过 IList<T>
接口迭代 List<T>
时使用的引用类型 Enumerator
会进行堆分配(假设链接问题中的注释是正确的),但为什么所有 3 代都有一个集合?
2000 枚举器对象是很多对象,但是一旦 foreach 循环完成,它就可以进行 GC,因为在 foreach 完成后没有任何对象引用该对象。为什么对象会进入第 1 代和第 2 代?
我想你对 GC.CollectionCount()
向你展示的内容感到困惑。
每次调用GC.Collect()
,都会强制所有代的全集。然后你调用 GC.CollectionCount()
,它告诉你所有的世代都被收集了。您只是在观察调用 GC.Collect()
!
确实 IList
版本正在分配枚举器,但这些枚举器在 gen0 中很快就会消亡。
检查此类内容的正确工具是 BenchmarkDotNet with the MemoryDiagnoser。
我整理了一个简单的基准:
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;
}
}
这产生了结果:
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 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|
IList | 149.42 μs | 2.902 μs | 3.563 μs | 51.0254 | - | - | 80,128 B |
List | 48.98 μs | 0.961 μs | 1.524 μs | - | - | - | 128 B |
两者共有的 128B 分配将是 List<T>
实例本身。除此之外,看看 IList
版本如何多分配大约 80KB,导致每 1000 次操作产生 51 个新的 gen0 集合?