本帖最后由 断天涯大虾 于 2017-7-17 13:46 编辑
.NET Core(开放源代码,跨平台,x-copy可部署等)有许多令人兴奋的方面,其中最值得称赞的就是其性能了。 感谢所有社区开发人员对.NET Core做出的贡献,其中的许多改进也将在接下来的几个版本中引入.NET Framework。 本文主要介绍.NET Core中的一些性能改进,特别是.NET Core 2.0中的,重点介绍各个核心库的一些示例。
集合集合是任何应用程序的基石,同时.NET库中也有大量集合。.NET库中的一些改进是为了消除开销,例如简化操作以便更好的实现内联,减少指令数量等。例如,下面的这个使用Q<T>的例子: - using System;
- using System.Diagnostics;
- using System.Collections.Generic;
- public class Test
- {
- public static void Main()
- {
- while (true)
- {
- var q = new Queue<int>();
- var sw = Stopwatch.StartNew();
- for (int i = 0; i < 100_000_000; i++)
- {
- q.Enqueue(i);
- q.Dequeue();
- }
- Console.WriteLine(sw.Elapsed);
- }
- }
- }
复制代码- 00:00:00.9392595
- 00:00:00.9390453
- 00:00:00.9455784
- 00:00:00.9508294
- 00:00:01.0107745
复制代码而使用.NET Core 2.0则会产生如下输出: - 00:00:00.9392595
- 00:00:00.9390453
- 00:00:00.9455784
- 00:00:00.9508294
- 00:00:01.0107745
复制代码由于这是挂钟时间所节省的,较小的值计算的更快,这也表明吞吐量增加了约2倍!
在其他情况下,通过更改操作算法的复杂性,可以更快地进行操作。编写软件时,最初编写的一个简单实现,虽然是正确的,但是这样实现往往不能表现出最佳的性能,直到特定的场景出现时,才考虑如何提高性能。例如,SortedSet <T>的ctor最初以相对简单的方式编写,由于使用O(N ^ 2)算法来处理重复项,因此不能很好地处理复杂性。该算法在PRnetnet / corefx#1955中的.NET Core中得到修复。以下简短的程序说明了修复的区别: - using System;
- using System.Diagnostics;
- using System.Collections.Generic;
- using System.Linq;
- public class Test
- {
- public static void Main()
- {
- var sw = Stopwatch.StartNew();
- var ss = new SortedSet<int>(Enumerable.Repeat(42, 400_000));
- Console.WriteLine(sw.Elapsed);
- }
- }
复制代码在个人电脑的.NET Framework上,这段代码需要大约7.7秒执行完成。在.NET Core 2.0上,减少到大约0.013s(改进改变了算法的复杂性,集合越大,节省的时间越多)。 - public class Test
- {
- static int s_result;
- public static void Main()
- {
- while (true)
- {
- var s = new SortedSet<int>();
- for (int n = 0; n < 100_000; n++)
- {
- s.Add(n);
- }
- var sw = Stopwatch.StartNew();
- for (int i = 0; i < 10_000_000; i++)
- {
- s_result = s.Min;
- }
- Console.WriteLine(sw.Elapsed);
- }
- }
- }
复制代码- 00:00:01.1427246
- 00:00:01.1295220
- 00:00:01.1350696
- 00:00:01.1502784
- 00:00:01.1677880
复制代码而在.NET Core 2.0中,我们得到如下结果: - 00:00:00.0861391
- 00:00:00.0861183
- 00:00:00.0866616
- 00:00:00.0848434
- 00:00:00.0860198
复制代码显示出相当大的时间下降和吞吐量的增加。 - using System;
- using System.Diagnostics;
- using System.Collections.Generic;
- public class Test
- {
- public static void Main()
- {
- while (true)
- {
- var l = new List<int>();
- var sw = Stopwatch.StartNew();
- for (int i = 0; i < 100_000_000; i++)
- {
- l.Add(i);
- l.RemoveAt(0);
- }
- Console.WriteLine(sw.Elapsed);
- }
- }
- }
复制代码在.NET 4.7中,会得到的结果如下: - 00:00:00.4434135
- 00:00:00.4394329
- 00:00:00.4496867
- 00:00:00.4496383
- 00:00:00.4515505
复制代码和.NET Core 2.0,得到: - 00:00:00.3213094
- 00:00:00.3211772
- 00:00:00.3179631
- 00:00:00.3198449
- 00:00:00.3164009
复制代码可以肯定的是,在0.3秒内可以实现1亿次这样的添加并从列表中删除的操作,这表明操作开始并不慢。但是,通过执行一个应用程序,列表通常会添加到很多,同时也节省了总时间消耗。
- using System;
- using System.Diagnostics;
- using System.Collections.Concurrent;
- public class Test
- {
- public static void Main()
- {
- while (true)
- {
- var q = new ConcurrentQueue<int>();
- var sw = Stopwatch.StartNew();
- for (int i = 0; i < 100_000_000; i++)
- {
- q.Enqueue(i);
- q.TryDequeue(out int _);
- }
- Console.WriteLine(sw.Elapsed);
- }
- }
- }
复制代码在个人电脑上,.NET 4.7产生的输出如下: - 00:00:02.6485174
- 00:00:02.6144919
- 00:00:02.6699958
- 00:00:02.6441047
- 00:00:02.6255135
复制代码- 00:00:01.7700190
- 00:00:01.8324078
- 00:00:01.7552966
- 00:00:01.7518632
- 00:00:01.7560811
复制代码这表明当将.NET Core 2.0切换到30%时,ConcurrentQueue <T>的吞吐量没有任何并发​​性提高。但是实施中的变化提高了序列化的吞吐量,甚至更多地减少了使用队列的生产和消耗之间的同步,这可能对吞吐量有更明显的影响。请考虑以下代码: - using System;
- using System.Diagnostics;
- using System.Collections.Concurrent;
- using System.Threading.Tasks;
- public class Test
- {
- public static void Main()
- {
- while (true)
- {
- const int Items = 100_000_000;
- var q = new ConcurrentQueue<int>();
- var sw = Stopwatch.StartNew();
- Task consumer = Task.Run(() =>
- {
- int total = 0;
- while (total < Items) if (q.TryDequeue(out int _)) total++;
- });
- for (int i = 0; i < Items; i++) q.Enqueue(i);
- consumer.Wait();
- Console.WriteLine(sw.Elapsed);
- }
- }
- }
复制代码在.NET 4.7中,个人计算机输出如下结果: - 00:00:06.1366044
- 00:00:05.7169339
- 00:00:06.3870274
- 00:00:05.5487718
- 00:00:06.6069291
复制代码而使用.NET Core 2.0,会得到以下结果: - 00:00:01.2052460
- 00:00:01.5269184
- 00:00:01.4638793
- 00:00:01.4963922
- 00:00:01.4927520
复制代码这是一个3.5倍的吞吐量的增长。不但CPU效率提高了, 而且内存分配也大大减少。下面的例子主要观察GC集合的数量,而不是挂钟时间: - using System;
- using System.Diagnostics;
- using System.Collections.Concurrent;
- public class Test
- {
- public static void Main()
- {
- while (true)
- {
- var q = new ConcurrentQueue<int>();
- int gen0 = GC.CollectionCount(0), gen1 = GC.CollectionCount(1), gen2 = GC.CollectionCount(2);
- for (int i = 0; i < 100_000_000; i++)
- {
- q.Enqueue(i);
- q.TryDequeue(out int _);
- }
- Console.WriteLine([ DISCUZ_CODE_16 ]quot;Gen0={GC.CollectionCount(0) - gen0} Gen1={GC.CollectionCount(1) - gen1} Gen2={GC.CollectionCount(2) - gen2}");
- }
- }
- }
复制代码在.NET 4.7中,得到以下输出: - Gen0 = 162 Gen1 = 80 Gen2 = 0
- Gen0 = 162 Gen1 = 81 Gen2 = 0
- Gen0 = 162 Gen1 = 81 Gen2 = 0
- Gen0 = 162 Gen1 = 81 Gen2 = 0
- Gen0 = 162 Gen1 = 81 Gen2 = 0
复制代码而使用.NET Core 2.0,会得到如下输出: - Gen0 = 0 Gen1 = 0 Gen2 = 0
- Gen0 = 0 Gen1 = 0 Gen2 = 0
- Gen0 = 0 Gen1 = 0 Gen2 = 0
- Gen0 = 0 Gen1 = 0 Gen2 = 0
- Gen0 = 0 Gen1 = 0 Gen2 = 0
复制代码.NET 4.7中的实现使用了固定大小的数组链表,一旦固定数量的元素被添加到每个数组中,就会被丢弃, 这有助于简化实现,但也会导致生成大量垃圾。在.NET Core 2.0中,新的实现仍然使用链接在一起的链接列表,但是随着新的片段的添加,这些片段的大小会增加,更重要的是使用循环缓冲区,只有在前一个片段完全结束时,新片段才会增加。这种分配的减少可能对应用程序的整体性能产生相当大的影响。
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
- {
- public static void 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([ DISCUZ_CODE_19 ]quot;Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0} Gen1={GC.CollectionCount(1) - gen1} Gen2={GC.CollectionCount(2) - gen2}");
- }
- }
- }
复制代码在.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,以便减少分配的数量和大小,降低算法复杂度,并且消除不必要的工作。
- 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)));
复制代码- using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.Linq;
- public class Test
- {
- public static void 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倍。 通过消除多个运算器同时使用时的消耗,运算器也得到了大大的提升。例如下面的例子: - using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.Linq;
- public class Test
- {
- public static void Main()
- {
- IEnumerable<int> tenMillionToZero = Enumerable.Range(0, 10_000_000).Reverse();
- while (true)
- {
- var sw = Stopwatch.StartNew();
- int fifth = tenMillionToZero.OrderBy(i => i).Skip(4).First();
- Console.WriteLine(sw.Elapsed);
- }
- }
- }
复制代码在这里,我们创建一个可以从10,000,000下降到0的数字,然后再等待一会来排序它们上升,跳过排序结果中的前4个元素,并抓住第五个。在个人计算机上的NET 4.7中得到如下输出: - 00:00:01.3879042
- 00:00:01.3438509
- 00:00:01.4141820
- 00:00:01.4248908
- 00:00:01.3548279
复制代码而使用.NET Core 2.0,会得到如下输出: - 00:00:00.1776617
- 00:00:00.1787467
- 00:00:00.1754809
- 00:00:00.1765863
- 00:00:00.1735489
复制代码这是一个巨大的改进(〜8x),避免了大部分的开销。
- using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.Linq;
- public class Test
- {
- public static void Main()
- {
- IEnumerable<int> tenMillionToZero = Enumerable.Range(0, 10_000_000).Reverse();
- while (true)
- {
- var sw = Stopwatch.StartNew();
- int fifth = tenMillionToZero.OrderBy(i => i).Skip(4).First();
- Console.WriteLine(sw.Elapsed);
- }
- }
- }
复制代码在.NET 4.7中,会得到如下结果: - 00:00:00.1308687
- 00:00:00.1228546
- 00:00:00.1268445
- 00:00:00.1247647
- 00:00:00.1503511
复制代码而在.NET Core 2.0中,得到如下结果: - 00:00:00.0386857
- 00:00:00.0337234
- 00:00:00.0346344
- 00:00:00.0345419
- 00:00:00.0355355
复制代码显示吞吐量增加约4倍。 - using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.Linq;
- public class Test
- {
- public static void Main()
- {
- IEnumerable<int> zeroToTenMillion = Enumerable.Range(0, 10_000_000).ToArray();
- while (true)
- {
- var sw = Stopwatch.StartNew();
- zeroToTenMillion.Select(i => i).ToList();
- Console.WriteLine(sw.Elapsed);
- }
- }
- }
复制代码在.NET 4.7中,会得到如下的结果: - Elapsed=00:00:01.0548794 Gen0=2 Gen1=2 Gen2=2
- Elapsed=00:00:01.1147146 Gen0=2 Gen1=2 Gen2=2
- Elapsed=00:00:01.0709146 Gen0=2 Gen1=2 Gen2=2
- Elapsed=00:00:01.0706030 Gen0=2 Gen1=2 Gen2=2
- Elapsed=00:00:01.0620943 Gen0=2 Gen1=2 Gen2=2
复制代码
而.NET Core 2.0的结果如下: - Elapsed=00:00:00.1716550 Gen0=1 Gen1=1 Gen2=1
- Elapsed=00:00:00.1720829 Gen0=1 Gen1=1 Gen2=1
- Elapsed=00:00:00.1717145 Gen0=1 Gen1=1 Gen2=1
- Elapsed=00:00:00.1713335 Gen0=1 Gen1=1 Gen2=1
- Elapsed=00:00:00.1705285 Gen0=1 Gen1=1 Gen2=1
复制代码这个例子中提高了6倍,但是垃圾收集却只有一半。 LINQ有一百多个运算器,本文只提到了几个,其它的很多也都有所改进。
压缩前面所展示的集合和LINQ的例子都是处理内存中的数据,当然还有许多其他形式的数据处理,包括大量CPU计算和逻辑判断,这些运算也在得到提升。
- using System;
- using System.IO;
- using System.IO.Compression;
- using System.Diagnostics;
- public class Test
- {
- public static void Main()
- {
- // Create some fairly compressible data
- byte[] raw = new byte[100 * 1024 * 1024];
- for (int i = 0; i < raw.Length; i++) raw[i] = (byte)i;
- var sw = Stopwatch.StartNew();
- // Compress it
- var compressed = new MemoryStream();
- using (DeflateStream ds = new DeflateStream(compressed, CompressionMode.Compress, true))
- {
- ds.Write(raw, 0, raw.Length);
- }
- compressed.Position = 0;
- // Decompress it
- var decompressed = new MemoryStream();
- using (DeflateStream ds = new DeflateStream(compressed, CompressionMode.Decompress))
- {
- ds.CopyTo(decompressed);
- }
- decompressed.Position = 0;
- Console.WriteLine(sw.Elapsed);
- }
- }
复制代码在.NET 4.7中,这一个压缩/解压缩操作,会得到如下结果: 00:00:00.7977190
而使用.NET Core 2.0,会得到如下结果: 00:00:00.1926701
加密.NET应用程序中另一个常见的计算源是使用加密操作,在这方面.NET Core也有改进。例如,在.NET 4.7中,SHA256.Create返回在管理代码中实现的SHA256类型,而管理代码可以运行得非常快,但是对于运算量非常大的计算,这仍然难以与原始吞吐量和编译器优化竞争。相反,对于.NET Core 2.0,SHA256.Create返回基于底层操作系统的实现,例如在Windows上使用CNG或在Unix上使用OpenSSL。从下面这个简单的例子可以看出,它散列着一个100MB的字节数组: - using System;
- using System.Diagnostics;
- using System.Security.Cryptography;
- public class Test
- {
- public static void Main()
- {
- byte[] raw = new byte[100 * 1024 * 1024];
- for (int i = 0; i < raw.Length; i++) raw[i] = (byte)i;
- using (var sha = SHA256.Create())
- {
- var sw = Stopwatch.StartNew();
- sha.ComputeHash(raw);
- Console.WriteLine(sw.Elapsed);
- }
- }
- }
复制代码在.NET 4.7中,会得到: 00:00:00.7576808
而使用.NET Core 2.0,会得到: 00:00:00.4032290
零代码更改的一个很好提升。
数学运算- using System;
- using System.Diagnostics;
- using System.Numerics;
- public class Test
- {
- public static void Main()
- {
- var rand = new Random(42);
- BigInteger a = Create(rand, 8192);
- BigInteger b = Create(rand, 8192);
- BigInteger c = Create(rand, 8192);
- var sw = Stopwatch.StartNew();
- BigInteger.ModPow(a, b, c);
- Console.WriteLine(sw.Elapsed);
- }
- private static BigInteger Create(Random rand, int bits)
- {
- var value = new byte[(bits + 7) / 8 + 1];
- rand.NextBytes(value);
- value[value.Length - 1] = 0;
- return new BigInteger(value);
- }
- }
复制代码在.NET 4.7中,会得到以下输出结果: 00:00:05.6024158
.NET Core 2.0上的相同代码会得到输出结果如下: 00:00:01.2707089
这是开发人员只关注.NET的某个特定领域的一个很好的例子,开发人员使得这种改进更好的满足了自己的需求,同时也满足了可能会用到这方面功能的其他开发人员的需求。 一些核心的整型类型的数学运算也得到了改进。例如: - using System;
- using System.Diagnostics;
- public class Test
- {
- private static long a = 99, b = 10, div, rem;
- public static void Main()
- {
- var sw = Stopwatch.StartNew();
- for (int i = 0; i < 100_000_000; i++)
- {
- div = Math.DivRem(a, b, out rem);
- }
- Console.WriteLine(sw.Elapsed);
- }
- }
复制代码 00:00:01.4143100
并在.NET Core 2.0上得到如下结果: 00:00:00.7469733
吞吐量提高约2倍。
序列化二进制序列化是.NET的另一个领域。BinaryFormatter最初并不是.NET Core中的一个组件,但是它包含在.NET Core 2.0中。该组件在性能方面有比较巧妙的修复。例如,PR dotnet / corefx#17949是一种单行修复,可以增加允许增长的最大大小的特定数组,但是这一变化可能对吞吐量产生重大影响,通过O(N)算法比以前的O(N ^ 2)算法要话费更长的操作时间。以下代码示例,明显的展示了这一点: - using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.IO;
- using System.Runtime.Serialization.Formatters.Binary;
- class Test
- {
- static void Main()
- {
- var books = new List<Book>();
- for (int i = 0; i < 1_000_000; i++)
- {
- string id = i.ToString();
- books.Add(new Book { Name = id, Id = id });
- }
- var formatter = new BinaryFormatter();
- var mem = new MemoryStream();
- formatter.Serialize(mem, books);
- mem.Position = 0;
- var sw = Stopwatch.StartNew();
- formatter.Deserialize(mem);
- sw.Stop();
- Console.WriteLine(sw.Elapsed.TotalSeconds);
- }
- [Serializable]
- private class Book
- {
- public string Name;
- public string Id;
- }
- }
复制代码在.NET 4.7中,代码输出如下结果: 76.677144
而在.NET Core 2.0中,会输出如下结果: 6.4044694
在这种情况下显示出了12倍的吞吐量提高。换句话说,它能够更有效地处理巨大的序列化输入。
文字处理.NET应用程序中另一种很常见的计算形式就是处理文本,文字处理在堆栈的各个层次上都有大量的改进。 - using System;
- using System.Diagnostics;
- using System.Text.RegularExpressions;
- public class Test
- {
- public static void Main()
- {
- var sw = new Stopwatch();
- int gen0 = GC.CollectionCount(0);
- sw.Start();
- for (int i = 0; i < 10_000_000; i++)
- {
- Regex.IsMatch("555-867-5309", @"^\d{3}-\d{3}-\d{4}[ DISCUZ_CODE_39 ]quot;);
- }
- Console.WriteLine([ DISCUZ_CODE_39 ]quot;Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0}");
- }
- }
复制代码在个人计算机上,.NET 4.7会得到的如下结果: - Elapsed=00:00:05.4367262 Gen0=820 Gen1=0 Gen2=0
复制代码而使用.NET Core 2.0会得到如下结果: - Elapsed=00:00:04.0231373 Gen0=248
复制代码
文本处理的另一个例子是各种形式的编码和解码,例如通过WebUtility.UrlDecode进行URL解码。在这种解码方法中,通常情况下输入不需要任何解码,但是如果输入经过了解码器,则输入仍然可以通过。感谢来自hughbe的 PR dotnet / corefx#7671,这种情况已经被优化了。例如下面这段程序: - using System;
- using System.Diagnostics;
- using System.Net;
- public class Test
- {
- public static void Main()
- {
- var sw = new Stopwatch();
- int gen0 = GC.CollectionCount(0);
- sw.Start();
- for (int i = 0; i < 10_000_000; i++)
- {
- WebUtility.UrlDecode("abcdefghijklmnopqrstuvwxyz");
- }
- Console.WriteLine([ DISCUZ_CODE_42 ]quot;Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0}");
- }
- }
复制代码在.NET 4.7中,会得到以下输出: - Elapsed=00:00:01.6742583 Gen0=648
复制代码而在.NET Core 2.0中,输出如下: - Elapsed=00:00:01.2255288 Gen0=133
复制代码- using System;
- using System.Diagnostics;
- using System.Linq;
- using System.Text;
- public class Test
- {
- public static void Main()
- {
- string s = new string(Enumerable.Range(0, 1024).Select(i => (char)('a' + i)).ToArray());
- while (true)
- {
- var sw = Stopwatch.StartNew();
- for (int i = 0; i < 1_000_000; i++)
- {
- byte[] data = Encoding.UTF8.GetBytes(s);
- }
- Console.WriteLine(sw.Elapsed);
- }
- }
- }
复制代码在.NET 4.7中得到以下输出,如: - 00:00:02.4028829
- 00:00:02.3743152
- 00:00:02.3401392
- 00:00:02.4024785
- 00:00:02.3550876
复制代码而.NET Core 2.0等到如下输出: - 00:00:01.6133550
- 00:00:01.5915718
- 00:00:01.5759625
- 00:00:01.6070851
- 00:00:01.6070767
复制代码- using System;
- using System.Diagnostics;
- public class Test
- {
- public static void Main()
- {
- while (true)
- {
- var sw = new Stopwatch();
- int gen0 = GC.CollectionCount(0);
- sw.Start();
- for (int i = 0; i < 2_000_000; i++)
- {
- Enum.Parse(typeof(Colors), "Red, Orange, Yellow, Green, Blue");
- }
- Console.WriteLine([ DISCUZ_CODE_48 ]quot;Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0}");
- }
- }
- [Flags]
- private enum Colors
- {
- Red = 0x1,
- Orange = 0x2,
- Yellow = 0x4,
- Green = 0x8,
- Blue = 0x10
- }
- }
复制代码在.NET 4.7中,会得到的以下结果: - Elapsed=00:00:00.9529354 Gen0=293
- Elapsed=00:00:00.9422960 Gen0=294
- Elapsed=00:00:00.9419024 Gen0=294
- Elapsed=00:00:00.9417014 Gen0=294
- Elapsed=00:00:00.9514724 Gen0=293
复制代码在.NET Core 2.0上,会得到以下结果: - Elapsed=00:00:00.6448327 Gen0=11
- Elapsed=00:00:00.6438907 Gen0=11
- Elapsed=00:00:00.6285656 Gen0=12
- Elapsed=00:00:00.6286561 Gen0=11
- Elapsed=00:00:00.6294286 Gen0=12
复制代码不但吞吐量提高了约33%,而且分配和相关垃圾收集也减少了约25倍。
当然,在.NET应用程序中需要进行大量的自定义文本处理,除了使用像Regex / Encoding这样的内置类型和Parse和ToString这样的内置操作之外,文本操作通常都是直接构建在字符串之上,并且大量的改进已经引入到了操作on String之上。
- using System;
- using System.Diagnostics;
- public class Test
- {
- public static void Main()
- {
- var dt = DateTime.Now;
- while (true)
- {
- var sw = new Stopwatch();
- int gen0 = GC.CollectionCount(0);
- sw.Start();
- for (int i = 0; i < 2_000_000; i++)
- {
- dt.ToString("o");
- dt.ToString("r");
- }
- Console.WriteLine([ DISCUZ_CODE_51 ]quot;Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0}");
- }
- }
- }
复制代码在.NET 4.7上,会得到如下结果: - 00:00:05.9718129
- 00:00:05.9199793
- 00:00:06.0203108
- 00:00:05.9458049
- 00:00:05.9622262
复制代码而在.NET Core 2.0中,会得到如下结果: - 00:00:03.1283763
- 00:00:03.0925150
- 00:00:02.9778923
- 00:00:03.0782851
复制代码吞吐量提高约2倍。 - using System;
- using System.Diagnostics;
- using System.Linq;
- public class Test
- {
- public static void Main()
- {
- string s = string.Concat(Enumerable.Repeat("a", 100)) + "b";
- while (true)
- {
- var sw = Stopwatch.StartNew();
- for (int i = 0; i < 100_000_000; i++)
- {
- s.IndexOf('b');
- }
- Console.WriteLine(sw.Elapsed);
- }
- }
- }
复制代码在.NET 4.7上会得到如下结果: - 00:00:01.3097317
- 00:00:01.3072381
- 00:00:01.3045015
- 00:00:01.3068244
- 00:00:01.3210207
复制代码.NET Core 2.0会得到如下结果: - 00:00:00.6239002
- 00:00:00.6150021
- 00:00:00.6147173
- 00:00:00.6129136
- 00:00:00.6099822
复制代码对String的改进,也让我们看到对于其它方面进行更多改进的可能性,这是非常有趣的。
文件系统到目前为止,本文一直专注于内存中操纵数据的各种改进。但是.NET Core的许多更改都是​​关于I / O的。 下面从文件开始介绍。这是一个从文件中异步读取所有数据并将其写入另一个文件的示例: - using System;
- using System.Diagnostics;
- using System.IO;
- using System.Threading.Tasks;
- class Test
- {
- static void Main() => MainAsync().GetAwaiter().GetResult();
- static async Task MainAsync()
- {
- string inputPath = Path.GetTempFileName(), outputPath = Path.GetTempFileName();
- byte[] data = new byte[50_000_000];
- new Random().NextBytes(data);
- File.WriteAllBytes(inputPath, data);
- 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; i++)
- {
- using (var input = new FileStream(inputPath, FileMode.Open, FileAccess.Read, FileShare.Read, 0x1000, useAsync: true))
- using (var output = new FileStream(outputPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x1000, useAsync: true))
- {
- await input.CopyToAsync(output);
- }
- }
- Console.WriteLine([ DISCUZ_CODE_57 ]quot;Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0} Gen1={GC.CollectionCount(1) - gen1} Gen2={GC.CollectionCount(2) - gen2}");
- }
- }
复制代码- Elapsed=00:00:09.4070345 Gen0=14 Gen1=7 Gen2=1
复制代码.NET Core 2.0会得到如下结果: - Elapsed=00:00:06.4286604 Gen0=4 Gen1=1 Gen2=1
复制代码
网络网络是值得关注的部分,这部分也将取得很大的改进。目前正在付出很大的努力来优化和调整低等级的网络堆栈,以便高效地构建更高级别的组件。 - using System;
- using System.Diagnostics;
- using System.Net;
- using System.Net.Sockets;
- using System.Threading;
- using System.Threading.Tasks;
- class Test
- {
- static void Main()
- {
- using (Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
- using (Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
- {
- listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
- listener.Listen(1);
- Task connectTask = Task.Run(() => client.Connect(listener.LocalEndPoint));
- using (Socket server = listener.Accept())
- {
- connectTask.Wait();
- using (var clientAre = new AutoResetEvent(false))
- using (var clientSaea = new SocketAsyncEventArgs())
- using (var serverAre = new AutoResetEvent(false))
- using (var serverSaea = new SocketAsyncEventArgs())
- {
- byte[] sendBuffer = new byte[1000];
- clientSaea.SetBuffer(sendBuffer, 0, sendBuffer.Length);
- clientSaea.Completed += delegate { clientAre.Set(); };
- byte[] receiveBuffer = new byte[1000];
- serverSaea.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
- serverSaea.Completed += delegate { serverAre.Set(); };
- var sw = new Stopwatch();
- int gen0 = GC.CollectionCount(0), gen1 = GC.CollectionCount(1), gen2 = GC.CollectionCount(2);
- sw.Start();
- for (int i = 0; i < 1_000_000; i++)
- {
- if (client.SendAsync(clientSaea)) clientAre.WaitOne();
- if (clientSaea.SocketError != SocketError.Success) throw new SocketException((int)clientSaea.SocketError);
- if (server.ReceiveAsync(serverSaea)) serverAre.WaitOne();
- if (serverSaea.SocketError != SocketError.Success) throw new SocketException((int)clientSaea.SocketError);
- }
- Console.WriteLine([ DISCUZ_CODE_60 ]quot;Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0} Gen1={GC.CollectionCount(1) - gen1} Gen2={GC.CollectionCount(2) - gen2}");
- }
- }
- }
- }
- }
复制代码该程序创建两个连接的socket,然后向socket写入1000次,并且在案例中使用异步方法接收,但绝大多数操作将同步完成。在.NET 4.7中会得到如下结果:
在.NET Core 2.0中,大多数操作能够同步完成,得到如下结果: Elapsed=00:00:05.6197060 Gen0=0 Gen1=0 Gen2=0
- using System;
- using System.Diagnostics;
- using System.Net;
- using System.Net.Sockets;
- using System.Threading;
- using System.Threading.Tasks;
- class Test
- {
- static void Main()
- {
- using (Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
- using (Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
- {
- listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
- listener.Listen(1);
- Task connectTask = Task.Run(() => client.Connect(listener.LocalEndPoint));
- using (Socket server = listener.Accept())
- {
- connectTask.Wait();
- using (var clientAre = new AutoResetEvent(false))
- using (var clientSaea = new SocketAsyncEventArgs())
- using (var serverAre = new AutoResetEvent(false))
- using (var serverSaea = new SocketAsyncEventArgs())
- {
- byte[] sendBuffer = new byte[1000];
- clientSaea.SetBuffer(sendBuffer, 0, sendBuffer.Length);
- clientSaea.Completed += delegate { clientAre.Set(); };
- byte[] receiveBuffer = new byte[1000];
- serverSaea.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
- serverSaea.Completed += delegate { serverAre.Set(); };
- var sw = new Stopwatch();
- int gen0 = GC.CollectionCount(0), gen1 = GC.CollectionCount(1), gen2 = GC.CollectionCount(2);
- sw.Start();
- for (int i = 0; i < 1_000_000; i++)
- {
- if (client.SendAsync(clientSaea)) clientAre.WaitOne();
- if (clientSaea.SocketError != SocketError.Success) throw new SocketException((int)clientSaea.SocketError);
- if (server.ReceiveAsync(serverSaea)) serverAre.WaitOne();
- if (serverSaea.SocketError != SocketError.Success) throw new SocketException((int)clientSaea.SocketError);
- }
- Console.WriteLine([ DISCUZ_CODE_61 ]quot;Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0} Gen1={GC.CollectionCount(1) - gen1} Gen2={GC.CollectionCount(2) - gen2}");
- }
- }
- }
- }
- }
复制代码- Elapsed = 00:00:24.7827947 Gen0 = 220 Gen1 = 3 Gen2 = 0
复制代码而在.NET Core 2.0中,时间减少了5倍,垃圾回收有效地减少到零: - Elapsed=00:00:05.6456073 Gen0=74 Gen1=0 Gen2=0
复制代码- using System;
- using System.Diagnostics;
- using System.IO;
- using System.Net;
- using System.Net.Sockets;
- using System.Threading;
- using System.Threading.Tasks;
- class Test
- {
- static void Main() => MainAsync().GetAwaiter().GetResult();
- static async Task MainAsync()
- {
- using (Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
- using (Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
- {
- listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
- listener.Listen(1);
- Task connectTask = Task.Run(() => client.Connect(listener.LocalEndPoint));
- using (Socket server = listener.Accept())
- {
- await connectTask;
- using (var serverStream = new NetworkStream(server))
- using (var clientStream = new NetworkStream(client))
- {
- Task serverCopyAll = serverStream.CopyToAsync(Stream.Null);
- byte[] data = new byte[1024];
- new Random().NextBytes(data);
- var sw = new Stopwatch();
- int gen0 = GC.CollectionCount(0), gen1 = GC.CollectionCount(1), gen2 = GC.CollectionCount(2);
- sw.Start();
- for (int i = 0; i < 1_000_000; i++)
- {
- await clientStream.WriteAsync(data, 0, data.Length);
- }
- client.Shutdown(SocketShutdown.Send);
- serverCopyAll.Wait();
- sw.Stop();
- Console.WriteLine([ DISCUZ_CODE_64 ]quot;Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0} Gen1={GC.CollectionCount(1) - gen1} Gen2={GC.CollectionCount(2) - gen2}");
- }
- }
- }
- }
- }
复制代码在.NET 4.7中,会得到如下结果: - Elapsed=00:00:21.1171962 Gen0=470 Gen1=3 Gen2=1
复制代码- Elapsed=00:00:05.6456073 Gen0=74 Gen1=0 Gen2=0
复制代码85%的垃圾收集已被删除!
并发对于并发和并行性相关的原始化和基础部分,也得到了许多改进。 - using System;
- using System.Diagnostics;
- using System.Threading;
- class Test
- {
- static void Main()
- {
- while (true)
- {
- int remaining = 20_000_000;
- var mres = new ManualResetEventSlim();
- WaitCallback wc = null;
- wc = delegate
- {
- if (Interlocked.Decrement(ref remaining) <= 0) mres.Set();
- else ThreadPool.QueueUserWorkItem(wc);
- };
- var sw = new Stopwatch();
- int gen0 = GC.CollectionCount(0), gen1 = GC.CollectionCount(1), gen2 = GC.CollectionCount(2);
- sw.Start();
- for (int i = 0; i < Environment.ProcessorCount; i++) ThreadPool.QueueUserWorkItem(wc);
- mres.Wait();
- Console.WriteLine([ DISCUZ_CODE_67 ]quot;Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0} Gen1={GC.CollectionCount(1) - gen1} Gen2={GC.CollectionCount(2) - gen2}");
- }
- }
- }
复制代码在.NET 4.7中,会等到如下结果: - Elapsed=00:00:03.6263995 Gen0=225 Gen1=51 Gen2=16
- Elapsed=00:00:03.6304345 Gen0=231 Gen1=62 Gen2=17
- Elapsed=00:00:03.6142323 Gen0=225 Gen1=53 Gen2=16
- Elapsed=00:00:03.6565384 Gen0=232 Gen1=62 Gen2=16
- Elapsed=00:00:03.5999892 Gen0=228 Gen1=62 Gen2=17
复制代码而在.NET Core 2.0中,会得到如下结果: - Elapsed=00:00:02.1797508 Gen0=153 Gen1=0 Gen2=0
- Elapsed=00:00:02.1188833 Gen0=154 Gen1=0 Gen2=0
- Elapsed=00:00:02.1000003 Gen0=153 Gen1=0 Gen2=0
- Elapsed=00:00:02.1024852 Gen0=153 Gen1=0 Gen2=0
- Elapsed=00:00:02.1044461 Gen0=154 Gen1=1 Gen2=0
复制代码这是一个巨大的吞吐量的改善,并且这样一个核心组件的垃圾量也将大幅减少。 - using System;
- using System.Diagnostics;
- using System.Threading;
- class Test
- {
- static void Main()
- {
- while (true)
- {
- bool taken = false;
- var sl = new SpinLock(false);
- sl.Enter(ref taken);
- var sw = Stopwatch.StartNew();
- for (int i = 0; i < 100_000_000; i++)
- {
- taken = false;
- sl.TryEnter(0, ref taken);
- }
- Console.WriteLine(sw.Elapsed);
- }
- }
- }
复制代码在.NET 4.7中,会得到如下结果: - 00:00:02.3276463
- 00:00:02.3174042
- 00:00:02.3022212
- 00:00:02.3015542
- 00:00:02.2974777
复制代码而在.NET Core 2.0中,会得到如下结果: - 00:00:00.3915327
- 00:00:00.3953084
- 00:00:00.3875121
- 00:00:00.3980009
- 00:00:00.3886977
复制代码吞吐量的这种差异可能会对运行这种锁的热路径产生很大的影响。 - using System;
- using System.Diagnostics;
- class Test
- {
- static int s_result;
- static void Main()
- {
- while (true)
- {
- var lazy = new Lazy<int>(() => 42);
- s_result = lazy.Value;
- var sw = Stopwatch.StartNew();
- for (int i = 0; i < 1_000_000_000; i++)
- {
- s_result = lazy.Value;
- }
- Console.WriteLine(sw.Elapsed);
- }
- }
- }
复制代码在.NET 4.7中,会得到的结果如下: - 00:00:02.6769712
- 00:00:02.6789140
- 00:00:02.6535493
- 00:00:02.6911146
- 00:00:02.7253927
复制代码而在.NET Core 2.0中,会得到的结果如下: - 00:00:00.5278348
- 00:00:00.5594950
- 00:00:00.5458245
- 00:00:00.5381743
- 00:00:00.5502970
复制代码吞吐量增加约5倍。
下一步是什么本文只涉及了部分.NET Core的性能改进。在dotnet / corefx和dotnet / coreclr repos 中的pull请求中搜索“perf”或“performance”,你会发现接近一千个合并的PR改进。其中一些是比较大的同时也很有影响力的改进,而另一些则主要减少了库和运行时的消耗,这些变化一起起作用,保证了能够在.NET Core上更快的运行应用程序。展望未来,性能将成为关注的重点,无论是以性能改进为目标的API还是现有库的性能的改进。
欢迎大家深入了解.NET Core代码库,以便找到影响自己的应用程序和库的瓶颈,并提交PR来修复它们。如果你的问题得到修复,也请将修复程序分享给所有需要的人。 |