今年,我写了一篇文章,比较了创建不会变异的短期集合时的性能,通常用于在迭代临时 LINQ 转换时防止多次枚举,或者确保在相应的应用程序层内抛出映射异常。
这些测试是使用 .NET Framework 4.8、.NET 7 和 .NET 8 执行的,得出的结论是,对于几乎所有集合大小,它们都明显更快,内存效率更高,唯一的例外是 .NET 8 中非常大的集合速度更快,但仍然使用更多内存。
假设一切按计划进行,Microsoft 应该在 2024 年底之前发布 .NET 9。这是他们最受欢迎的开发框架的下一个主要版本,它将带来许多新功能(C# 13 就是其中之一)和性能改进。
由于我们已经有可用的 .NET 9 预览版 5,其中包含内部使用的更优化的 ,我认为现在是比较这两种方法在 .NET 9 中的性能的好时机,同时使用 .NET 8 作为基线。
再一次,我将使用众所周知的 C# 库来运行测试,环境如下所示:BenchmarkDotNet
BenchmarkDotNet v0.13.10, Windows 11 (10.0.22631.3737/23H2/2023Update/SunValley3)
AMD Ryzen 7 3700X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.100-preview.5.24307.3
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
.NET 8.0 : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
.NET 9.0 : .NET 9.0.0 (9.0.24.30607), X64 RyuJIT AVX2
.NET Framework 4.8.1 : .NET Framework 4.8.1 (4.8.9241.0), X64 RyuJIT VectorSize=256
测试包括创建一个集合,该集合将保存大小由测试参数定义的随机整数。为确保集合初始化不会影响性能,将在测试设置期间创建并缓存集合初始化,但在调用 或 之前将其转换为新初始化。
请记住,我们想要测试遍历 an 的性能并创建一个数组或列表,因此,为了防止 .NET 内部优化(如使用 ),将缓存数组转换为 an 的方法将使用关键字。这与上一篇文章不同,因为我使用的是返回数组优化可枚举的方法,我想测试最坏的情况。
[SimpleJob(RuntimeMoniker.Net80, baseline: true)]
[SimpleJob(RuntimeMoniker.Net90)]
[MemoryDiagnoser]
public class ToListVsToArray
{
[Params(10, 100, 1000, 10000, 100000)]
public int Size;
private int[] _items;
[GlobalSetup]
public void Setup()
{
var random = new Random(123);
_items = Enumerable.Range(0, Size).Select(_ => random.Next()).ToArray();
}
[Benchmark]
public int[] ToArray() => CreateItemsEnumerable().ToArray();
[Benchmark]
public List<int> ToList() => CreateItemsEnumerable().ToList();
private IEnumerable<int> CreateItemsEnumerable()
{
foreach (var item in _items)
yield return item;
}
}
由于这篇文章是我上一篇文章的延续,如果我得出结论,使用比 更快、更省内存,让我们比较一下该陈述是否仍然成立。ToArrayToList
| Method | Size | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
|-------- |------- |--------------:|--------------:|--------------:|---------:|---------:|---------:|----------:|
| ToArray | 10 | 70.39 ns | 0.366 ns | 0.342 ns | 0.0134 | - | - | 112 B |
| ToList | 10 | 72.85 ns | 0.744 ns | 0.696 ns | 0.0315 | - | - | 264 B |
| ToArray | 100 | 322.65 ns | 1.816 ns | 1.610 ns | 0.0563 | - | - | 472 B |
| ToList | 100 | 368.11 ns | 4.283 ns | 4.006 ns | 0.1469 | - | - | 1232 B |
| ToArray | 1000 | 2,451.62 ns | 19.687 ns | 16.439 ns | 0.4845 | - | - | 4072 B |
| ToList | 1000 | 2,854.28 ns | 24.286 ns | 22.717 ns | 1.0109 | 0.0153 | - | 8472 B |
| ToArray | 10000 | 22,275.27 ns | 163.363 ns | 152.810 ns | 4.7607 | - | - | 40072 B |
| ToList | 10000 | 26,944.65 ns | 293.685 ns | 260.344 ns | 15.6250 | - | - | 131448 B |
| ToArray | 100000 | 328,160.90 ns | 1,874.673 ns | 1,753.570 ns | 124.5117 | 124.5117 | 124.5117 | 400156 B |
| ToList | 100000 | 410,583.73 ns | 2,298.854 ns | 2,037.874 ns | 285.6445 | 285.6445 | 285.6445 | 1049120 B |
以基线为基准,我们可以看到该方法平均速度提高了 15%,使用的内存减少了 60%。ToListToArray
请记住,这甚至比较大的集合更好,这在 .NET 8 中并非如此,尽管使用较少的内存,但它的速度会慢 4%。ToArrayToList
获胜者:.NET 9.0
由于我们还想比较 .NET 9 与 .NET 8 的性能,因此让我们在每个框架上单独分析每种方法,看看是否有任何更改。
| Runtime | Size | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio |
|----------|------- |--------------:|--------------:|--------------:|------:|--------:|---------:|---------:|---------:|----------:|------------:|
| .NET 8.0 | 10 | 107.51 ns | 1.585 ns | 1.238 ns | 1.00 | 0.00 | 0.0315 | - | - | 264 B | 1.00 |
| .NET 9.0 | 10 | 70.39 ns | 0.366 ns | 0.342 ns | 0.65 | 0.01 | 0.0134 | - | - | 112 B | 0.42 |
| .NET 8.0 | 100 | 442.33 ns | 3.788 ns | 3.543 ns | 1.00 | 0.00 | 0.1431 | - | - | 1200 B | 1.00 |
| .NET 9.0 | 100 | 322.65 ns | 1.816 ns | 1.610 ns | 0.73 | 0.01 | 0.0563 | - | - | 472 B | 0.39 |
| .NET 8.0 | 1000 | 3,186.13 ns | 31.530 ns | 29.493 ns | 1.00 | 0.00 | 1.0185 | - | - | 8544 B | 1.00 |
| .NET 9.0 | 1000 | 2,451.62 ns | 19.687 ns | 16.439 ns | 0.77 | 0.00 | 0.4845 | - | - | 4072 B | 0.48 |
| .NET 8.0 | 10000 | 30,659.83 ns | 292.167 ns | 273.293 ns | 1.00 | 0.00 | 12.6343 | - | - | 106232 B | 1.00 |
| .NET 9.0 | 10000 | 22,275.27 ns | 163.363 ns | 152.810 ns | 0.73 | 0.00 | 4.7607 | - | - | 40072 B | 0.38 |
| .NET 8.0 | 100000 | 482,397.96 ns | 1,499.949 ns | 1,403.053 ns | 1.00 | 0.00 | 249.5117 | 249.5117 | 249.5117 | 925140 B | 1.00 |
| .NET 9.0 | 100000 | 328,160.90 ns | 1,874.673 ns | 1,753.570 ns | 0.68 | 0.00 | 124.5117 | 124.5117 | 124.5117 | 400156 B | 0.43 |
使用 .NET 8 作为基线,我们可以看到该方法在 .NET 9 上平均速度提高了 30%,使用的内存减少了 55%。ToArray
获胜者:.NET 9.0
| Runtime | Size | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio |
|----------|------- |--------------:|--------------:|--------------:|------:|--------:|---------:|---------:|---------:|----------:|------------:|
| .NET 8.0 | 10 | 87.44 ns | 1.803 ns | 1.929 ns | 1.00 | 0.00 | 0.0315 | - | - | 264 B | 1.00 |
| .NET 9.0 | 10 | 72.85 ns | 0.744 ns | 0.696 ns | 0.84 | 0.02 | 0.0315 | - | - | 264 B | 1.00 |
| .NET 8.0 | 100 | 420.90 ns | 3.654 ns | 2.853 ns | 1.00 | 0.00 | 0.1469 | - | - | 1232 B | 1.00 |
| .NET 9.0 | 100 | 368.11 ns | 4.283 ns | 4.006 ns | 0.87 | 0.01 | 0.1469 | - | - | 1232 B | 1.00 |
| .NET 8.0 | 1000 | 3,448.37 ns | 67.905 ns | 78.199 ns | 1.00 | 0.00 | 1.0109 | 0.0153 | - | 8472 B | 1.00 |
| .NET 9.0 | 1000 | 2,854.28 ns | 24.286 ns | 22.717 ns | 0.82 | 0.01 | 1.0109 | 0.0153 | - | 8472 B | 1.00 |
| .NET 8.0 | 10000 | 35,650.35 ns | 707.370 ns | 1,537.764 ns | 1.00 | 0.00 | 15.6250 | - | - | 131448 B | 1.00 |
| .NET 9.0 | 10000 | 26,944.65 ns | 293.685 ns | 260.344 ns | 0.77 | 0.05 | 15.6250 | - | - | 131448 B | 1.00 |
| .NET 8.0 | 100000 | 462,317.72 ns | 1,686.365 ns | 1,577.427 ns | 1.00 | 0.00 | 285.6445 | 285.6445 | 285.6445 | 1049120 B | 1.00 |
| .NET 9.0 | 100000 | 410,583.73 ns | 2,298.854 ns | 2,037.874 ns | 0.89 | 0.01 | 285.6445 | 285.6445 | 285.6445 | 1049120 B | 1.00 |
使用 .NET 8 作为基线,我们可以看到该方法平均快 15%,同时在 .NET 9 上具有完全相同的内存占用。ToList
获胜者:.NET 9.0
在本文中,我们比较了 .NET 9 与 .NET 9 上的性能,并再次得出结论,如果需要在内存中创建一个临时集合以防止多个枚举 ,则使用在所有情况下的性能都更高,而与集合大小无关,这在 .NET 8 中并非如此。