ConcurrentBag <T>也有类似改进。ConcurrentBag <T>维护thread-local work-stealing队列,使得添加到的每个线程都有自己的队列。在.NET 4.7中,这些队列被实现为每个元素占据一个节点的链接列表,这意味着对该包的任何添加都会导致分配。在.NET Core 2.0中,这些队列是数组,这意味着除了增加阵列所涉及的均摊成本之外,增加的还是无需配置的。以下可以看出:
using System; using System.Diagnostics; using System.Collections.Concurrent; public class Test { Main() { while (true) { var q = new ConcurrentBag<int>() { 1, 2 }; var sw = new Stopwatch(); int gen0 = GC.CollectionCount(0), gen1 = GC.CollectionCount(1), gen2 = GC.CollectionCount(2); sw.Start(); for (int i = 0; i < 100_000_000; i++) { q.Add(i); q.TryTake(out int _); } sw.Stop(); Console.WriteLine($); } } }
在.NET 4.7中,个人计算机上产生以下输出:
Elapsed=00:00:06.5672723 Gen0=953 Gen1=0 Gen2=0 Elapsed=00:00:06.4829793 Gen0=954 Gen1=1 Gen2=0 Elapsed=00:00:06.9008532 Gen0=954 Gen1=0 Gen2=0 Elapsed=00:00:06.6485667 Gen0=953 Gen1=1 Gen2=0 Elapsed=00:00:06.4671746 Gen0=954 Gen1=1 Gen2=0
而使用.NET Core 2.0,会得到:
Elapsed=00:00:04.3377355 Gen0=0 Gen1=0 Gen2=0 Elapsed=00:00:04.2892791 Gen0=0 Gen1=0 Gen2=0 Elapsed=00:00:04.3101593 Gen0=0 Gen1=0 Gen2=0 Elapsed=00:00:04.2652497 Gen0=0 Gen1=0 Gen2=0 Elapsed=00:00:04.2808077 Gen0=0 Gen1=0 Gen2=0
吞吐量提高了约30%,并且分配和完成的垃圾收集量减少了。
LINQ
在应用程序代码中,集合通常与语言集成查询(LINQ)紧密相连,该查询已经有了更多的改进。LINQ中的许多运算符已经完全重写为.NET Core,以便减少分配的数量和大小,降低算法复杂度,并且消除不必要的工作。
例如,Enumerable.Concat方法用于创建一个单一的IEnumerable <T>,它首先产生first域可枚举的所有元素,然后再生成second域所有的元素。它在.NET 4.7中的实现是简单易懂的,下面的代码正好反映了这种行为表述:
static IEnumerable<TSource> ConcatIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second) { foreach (TSource element in first) yield return element; foreach (TSource element in second) yield return element; }
当两个序列是简单的枚举,如C#中的迭代器生成的,这种过程会执行的很好。但是如果应用程序代码具有如下代码呢?
first.Concat(second.Concat(third.Concat(fourth)));
每次我们从迭代器中退出时,则会返回到枚举器的MoveNext方法。这意味着如果你从另一个迭代器中枚举产生一个元素,则会返回两个MoveNext方法,并移动到下一个需要调用这两个MoveNext方法的元素。你调用的枚举器越多,操作所需的时间越长,特别是这些操作中的每一个都涉及多个接口调用(MoveNext和Current)。这意味着连接多个枚举会以指数方式增长,而不是呈线性增长。PR dotnet / corefx#6131修正了这个问题,在下面的例子中,区别是显而易见的:
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; public class Test { Main() { IEnumerable<int> zeroToTen = Enumerable.Range(0, 10); IEnumerable<int> result = zeroToTen; for (int i = 0; i < 10_000; i++) { result = result.Concat(zeroToTen); } var sw = Stopwatch.StartNew(); foreach (int i in result) { } Console.WriteLine(sw.Elapsed); } }
在个人计算机上,.NET 4.7需要大约4.12秒。但在.NET Core 2.0中,这只需要约0.14秒,提高了30倍。
通过消除多个运算器同时使用时的消耗,运算器也得到了大大的提升。例如下面的例子: