本文参考文章:https://blogs.msdn.microsoft.com/dotnet/2017/06/07/performance-improvements-in-net-core/

转载请注明出自:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。

.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 = ; i < 100_000_000; i++)
{
q.Enqueue(i);
q.Dequeue();
}
Console.WriteLine(sw.Elapsed);
}
}
}

PR dotnet/corefx #2515移除了这些操作中相对复杂的模数运算,在个人计算机,以上代码在.NET 4.7上产生如下输出:

::00.9392595
::00.9390453
::00.9455784
::00.9508294
::01.0107745

而使用.NET Core 2.0则会产生如下输出:

::00.5514887
::00.5662477
::00.5627481
::00.5685286
::00.5262378

由于这是挂钟时间所节省的,较小的值计算的更快,这也表明吞吐量增加了约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(, 400_000));
Console.WriteLine(sw.Elapsed);
}
}

在个人电脑的.NET Framework上,这段代码需要大约7.7秒执行完成。在.NET Core 2.0上,减少到大约0.013s(改进改变了算法的复杂性,集合越大,节省的时间越多)。

或者在SortedSet <T>上考虑这个例子:

public class Test
{
static int s_result; public static void Main()
{
while (true)
{
var s = new SortedSet<int>();
for (int n = ; n < 100_000; n++)
{
s.Add(n);
} var sw = Stopwatch.StartNew();
for (int i = ; i < 10_000_000; i++)
{
s_result = s.Min;
}
Console.WriteLine(sw.Elapsed);
}
}
}

.NET 4.7中MinMax的实现遍布SortedSet <T>的整个树,但是只需要找到最小或最大值即可,因为实现可以只遍历相关的节点。PR dotnet / corefx#11968修复了.NET Core实现。在.NET 4.7中,此示例生成如下结果:

::01.1427246
::01.1295220
::01.1350696
::01.1502784
::01.1677880

而在.NET Core 2.0中,我们得到如下结果:

::00.0861391
::00.0861183
::00.0866616
::00.0848434
::00.0860198

显示出相当大的时间下降和吞吐量的增加。

即使像List <T>这样的主工作核心也有改进的空间。考虑下面的例子:

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 = ; i < 100_000_000; i++)
{
l.Add(i);
l.RemoveAt();
}
Console.WriteLine(sw.Elapsed);
}
}
}

在.NET 4.7中,会得到的结果如下:

::00.4434135
::00.4394329
::00.4496867
::00.4496383
::00.4515505

和.NET Core 2.0,得到:

::00.3213094
::00.3211772
::00.3179631
::00.3198449
::00.3164009

可以肯定的是,在0.3秒内可以实现1亿次这样的添加并从列表中删除的操作,这表明操作开始并不慢。但是,通过执行一个应用程序,列表通常会添加到很多,同时也节省了总时间消耗。

这些类型的集合改进扩展不仅仅是System.Collections.Generic命名空间; System.Collections.Concurrent也有很多改进。事实上,.NET Core 2.0上的ConcurrentQueue <T>ConcurrentBag <T>完全重写了。下面看看一个基本的例子,使用ConcurrentQueue <T>但没有任何并发,例子中使用ConcurrentQueue <T>代替了Queue<T>

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 = ; i < 100_000_000; i++)
{
q.Enqueue(i);
q.TryDequeue(out int _);
}
Console.WriteLine(sw.Elapsed);
}
}
}

在个人电脑上,.NET 4.7产生的输出如下:

::02.6485174
::02.6144919
::02.6699958
::02.6441047
::02.6255135

显然,.NET 4.7上的ConcurrentQueue <T>示例比.NET 4.7中的Queue <T>版本慢,因为ConcurrentQueue <T>需要采用同步来确保是否安全使用。但是,更有趣的比较是当在.NET Core 2.0上运行相同的代码时会发生什么:

::01.7700190
::01.8324078
::01.7552966
::01.7518632
::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 = ;
while (total < Items) if (q.TryDequeue(out int _)) total++;
});
for (int i = ; i < Items; i++) q.Enqueue(i);
consumer.Wait(); Console.WriteLine(sw.Elapsed);
}
}
}

在.NET 4.7中,个人计算机输出如下结果:

::06.1366044
::05.7169339
::06.3870274
::05.5487718
::06.6069291

而使用.NET Core 2.0,会得到以下结果:

::01.2052460
::01.5269184
::01.4638793
::01.4963922
::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(), gen1 = GC.CollectionCount(), gen2 = GC.CollectionCount();
for (int i = ; i < 100_000_000; i++)
{
q.Enqueue(i);
q.TryDequeue(out int _);
}
Console.WriteLine($"Gen0={GC.CollectionCount(0) - gen0} Gen1={GC.CollectionCount(1) - gen1} Gen2={GC.CollectionCount(2) - gen2}");
}
}
}

在.NET 4.7中,得到以下输出:

Gen0 =  Gen1 =  Gen2 =
Gen0 = Gen1 = Gen2 =
Gen0 = Gen1 = Gen2 =
Gen0 = Gen1 = Gen2 =
Gen0 = Gen1 = Gen2 =

而使用.NET Core 2.0,会得到如下输出:

Gen0 =  Gen1 =  Gen2 =
Gen0 = Gen1 = Gen2 =
Gen0 = Gen1 = Gen2 =
Gen0 = Gen1 = Gen2 =
Gen0 = Gen1 = Gen2 =

.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>() { , };
var sw = new Stopwatch(); int gen0 = GC.CollectionCount(), gen1 = GC.CollectionCount(), gen2 = GC.CollectionCount();
sw.Start(); for (int i = ; i < 100_000_000; i++)
{
q.Add(i);
q.TryTake(out int _);
} sw.Stop();
Console.WriteLine($"Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0} Gen1={GC.CollectionCount(1) - gen1} Gen2={GC.CollectionCount(2) - gen2}");
}
}
}

在.NET 4.7中,个人计算机上产生以下输出:

Elapsed=::06.5672723 Gen0= Gen1= Gen2=
Elapsed=::06.4829793 Gen0= Gen1= Gen2=
Elapsed=::06.9008532 Gen0= Gen1= Gen2=
Elapsed=::06.6485667 Gen0= Gen1= Gen2=
Elapsed=::06.4671746 Gen0= Gen1= Gen2=

而使用.NET Core 2.0,会得到:

Elapsed=::04.3377355 Gen0= Gen1= Gen2=
Elapsed=::04.2892791 Gen0= Gen1= Gen2=
Elapsed=::04.3101593 Gen0= Gen1= Gen2=
Elapsed=::04.2652497 Gen0= Gen1= Gen2=
Elapsed=::04.2808077 Gen0= Gen1= Gen2=

吞吐量提高了约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方法的元素。你调用的枚举器越多,操作所需的时间越长,特别是这些操作中的每一个都涉及多个接口调用(MoveNextCurrent)。这意味着连接多个枚举会以指数方式增长,而不是呈线性增长。PR dotnet / corefx#6131修正了这个问题,在下面的例子中,区别是显而易见的:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
public class Test
{
public static void Main()
{
IEnumerable<int> zeroToTen = Enumerable.Range(, );
IEnumerable<int> result = zeroToTen;
for (int i = ; 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(, 10_000_000).Reverse();
while (true)
{
var sw = Stopwatch.StartNew();
int fifth = tenMillionToZero.OrderBy(i => i).Skip().First();
Console.WriteLine(sw.Elapsed);
}
}
}

在这里,我们创建一个可以从10,000,000下降到0的数字,然后再等待一会来排序它们上升,跳过排序结果中的前4个元素,并抓住第五个。在个人计算机上的NET 4.7中得到如下输出:

::01.3879042
::01.3438509
::01.4141820
::01.4248908
::01.3548279

而使用.NET Core 2.0,会得到如下输出:

::00.1776617
::00.1787467
::00.1754809
::00.1765863
::00.1735489

这是一个巨大的改进(〜8x),避免了大部分的开销。

类似地,来自justinvp的 PR dotnet / corefx#3429对常用的ToList方法添加了优化,为已知长度的源,提供了优化的路径,并且通过像Select这样的操作器来管理。在以下简单测试中,这种影响是显而易见的:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
public class Test
{
public static void Main()
{
IEnumerable<int> tenMillionToZero = Enumerable.Range(, 10_000_000).Reverse();
while (true)
{
var sw = Stopwatch.StartNew();
int fifth = tenMillionToZero.OrderBy(i => i).Skip().First();
Console.WriteLine(sw.Elapsed);
}
}
}

在.NET 4.7中,会得到如下结果:

::00.1308687
::00.1228546
::00.1268445
::00.1247647
::00.1503511

而在.NET Core 2.0中,得到如下结果:

::00.0386857
::00.0337234
::00.0346344
::00.0345419
::00.0355355

显示吞吐量增加约4倍。

在其他情况下,性能优势来自于简化实施,以避免开销,例如减少分配,避免委托分配,避免接口调用,最小化字段读取和写入,避免拷贝等。例如,jamesqo为PR dotnet / corefx#11208做出的贡献,大大地减少了Enumerable.ToArray涉及的开销。请看下面的例子:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
public class Test
{
public static void Main()
{
IEnumerable<int> zeroToTenMillion = Enumerable.Range(, 10_000_000).ToArray();
while (true)
{
var sw = Stopwatch.StartNew();
zeroToTenMillion.Select(i => i).ToList();
Console.WriteLine(sw.Elapsed);
}
}
}

在.NET 4.7中,会得到如下的结果:

Elapsed=::01.0548794 Gen0= Gen1= Gen2=
Elapsed=::01.1147146 Gen0= Gen1= Gen2=
Elapsed=::01.0709146 Gen0= Gen1= Gen2=
Elapsed=::01.0706030 Gen0= Gen1= Gen2=
Elapsed=::01.0620943 Gen0= Gen1= Gen2=

而.NET Core 2.0的结果如下:

Elapsed=::00.1716550 Gen0= Gen1= Gen2=
Elapsed=::00.1720829 Gen0= Gen1= Gen2=
Elapsed=::00.1717145 Gen0= Gen1= Gen2=
Elapsed=::00.1713335 Gen0= Gen1= Gen2=
Elapsed=::00.1705285 Gen0= Gen1= Gen2=

这个例子中提高了6倍,但是垃圾收集却只有一半。

LINQ有一百多个运算器,本文只提到了几个,其它的很多也都有所改进。

压缩

前面所展示的集合和LINQ的例子都是处理内存中的数据,当然还有许多其他形式的数据处理,包括大量CPU计算和逻辑判断,这些运算也在得到提升。

一个关键的例子是压缩,例如使用DeflateStream,性能方面也有一些重大的性能改进。例如,在.NET 4.7中,zlib(本地压缩库)用于压缩数据,但是相对未优化的托管实现了用于解压缩的数据; PR dotnet / corefx#2906添加了.NET Core支持,以便使用zlib进行解压缩。来自bjjones的 PR dotnet / corefx#5674使用英特尔生产的zlib这个更优化的版本。这些结合产生了非常棒的效果。下面的例子,创建一个大量的数据:

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[ * * ];
for (int i = ; 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, , raw.Length);
}
compressed.Position = ; // Decompress it
var decompressed = new MemoryStream();
using (DeflateStream ds = new DeflateStream(compressed, CompressionMode.Decompress))
{
ds.CopyTo(decompressed);
}
decompressed.Position = ; Console.WriteLine(sw.Elapsed);
}
}

在.NET 4.7中,这一个压缩/解压缩操作,会得到如下结果:

::00.7977190

而使用.NET Core 2.0,会得到如下结果:

::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[ * * ];
for (int i = ; 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.7576808

而使用.NET Core 2.0,会得到:

::00.4032290

零代码更改的一个很好提升。

数学运算

数学运算也是一个很大的计算量,特别是处理大量数据时。通过像dotnet / corefx#2182这样的PR ,axelheerBigInteger的各种操作做了一些实质的改进。请考虑以下示例:

using System;
using System.Diagnostics;
using System.Numerics; public class Test
{
public static void Main()
{
var rand = new Random();
BigInteger a = Create(rand, );
BigInteger b = Create(rand, );
BigInteger c = Create(rand, ); 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 + ) / + ];
rand.NextBytes(value);
value[value.Length - ] = ;
return new BigInteger(value);
}
}

在.NET 4.7中,会得到以下输出结果:

::05.6024158

.NET Core 2.0上的相同代码会得到输出结果如下:

::01.2707089

这是开发人员只关注.NET的某个特定领域的一个很好的例子,开发人员使得这种改进更好的满足了自己的需求,同时也满足了可能会用到这方面功能的其他开发人员的需求。

一些核心的整型类型的数学运算也得到了改进。例如:

using System;
using System.Diagnostics;
public class Test
{
private static long a = , b = , div, rem; public static void Main()
{
var sw = Stopwatch.StartNew();
for (int i = ; i < 100_000_000; i++)
{
div = Math.DivRem(a, b, out rem);
}
Console.WriteLine(sw.Elapsed);
}
}

PR dotnet / coreclr#8125用更快的实现取代了DivRem,在.NET 4.7中会得到的如下结果:

::01.4143100

并在.NET Core 2.0上得到如下结果:

::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 = ; 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 = ; 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应用程序中另一种很常见的计算形式就是处理文本,文字处理在堆栈的各个层次上都有大量的改进。

对于正则表达式,通常用于验证和解析输入文本中的数据。以下是使用Regex.IsMatch重复匹配电话号码的示例:

using System;
using System.Diagnostics;
using System.Text.RegularExpressions;
public class Test
{
public static void Main()
{
var sw = new Stopwatch();
int gen0 = GC.CollectionCount();
sw.Start(); for (int i = ; i < 10_000_000; i++)
{
Regex.IsMatch("555-867-5309", @"^\d{3}-\d{3}-\d{4}$");
} Console.WriteLine($"Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0}");
}
}

在个人计算机上,.NET 4.7会得到的如下结果:

Elapsed=::05.4367262 Gen0= Gen1= Gen2=

而使用.NET Core 2.0会得到如下结果:

Elapsed=::04.0231373 Gen0=

由于PR dotnet / corefx#231的变化很小,这些修改有助于缓存一部分数据,因此吞吐量提高了25%,分配/垃圾收集减少了70%。

文本处理的另一个例子是各种形式的编码和解码,例如通过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();
sw.Start(); for (int i = ; i < 10_000_000; i++)
{
WebUtility.UrlDecode("abcdefghijklmnopqrstuvwxyz");
} Console.WriteLine($"Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0}");
}
}

在.NET 4.7中,会得到以下输出:

Elapsed=::01.6742583 Gen0=

而在.NET Core 2.0中,输出如下:

Elapsed=::01.2255288 Gen0=

其他形式的编码和解码也得到了改进。例如,dotnet / coreclr#10124优化了使用一些内置Encoding -derived类型的循环。例如下面的示例:

using System;
using System.Diagnostics;
using System.Linq;
using System.Text;
public class Test
{
public static void Main()
{
string s = new string(Enumerable.Range(, ).Select(i => (char)('a' + i)).ToArray());
while (true)
{
var sw = Stopwatch.StartNew();
for (int i = ; i < 1_000_000; i++)
{
byte[] data = Encoding.UTF8.GetBytes(s);
}
Console.WriteLine(sw.Elapsed);
}
}
}

在.NET 4.7中得到以下输出,如:

::02.4028829
::02.3743152
::02.3401392
::02.4024785
::02.3550876

而.NET Core 2.0等到如下输出:

::01.6133550
::01.5915718
::01.5759625
::01.6070851
::01.6070767

这些改进也适用于字符串和其它类型之间转换,例如.NET中生成Parse和ToString方法。使用枚举来表示各种状态是相当普遍的,例如使用Enum.Parse将字符串解析为相应的枚举。PR dotnet / coreclr#2933改善了这一点。请查看以下的代码:

using System;
using System.Diagnostics;
public class Test
{
public static void Main()
{
while (true)
{
var sw = new Stopwatch();
int gen0 = GC.CollectionCount();
sw.Start(); for (int i = ; i < 2_000_000; i++)
{
Enum.Parse(typeof(Colors), "Red, Orange, Yellow, Green, Blue");
} Console.WriteLine($"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.9529354 Gen0=
Elapsed=::00.9422960 Gen0=
Elapsed=::00.9419024 Gen0=
Elapsed=::00.9417014 Gen0=
Elapsed=::00.9514724 Gen0=

在.NET Core 2.0上,会得到以下结果:

Elapsed=::00.6448327 Gen0=
Elapsed=::00.6438907 Gen0=
Elapsed=::00.6285656 Gen0=
Elapsed=::00.6286561 Gen0=
Elapsed=::00.6294286 Gen0=

不但吞吐量提高了约33%,而且分配和相关垃圾收集也减少了约25倍。

当然,在.NET应用程序中需要进行大量的自定义文本处理,除了使用像Regex / Encoding这样的内置类型和Parse和ToString这样的内置操作之外,文本操作通常都是直接构建在字符串之上,并且大量的改进已经引入到了操作on String之上。

例如,String.IndexOf很擅长于查找字符串中的字符。IndexOfbnetyersmyth的dotnet / coreclr#5327中得到改进,他们为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();
sw.Start(); for (int i = ; i < 2_000_000; i++)
{
dt.ToString("o");
dt.ToString("r");
} Console.WriteLine($"Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0}");
}
}
}

在.NET 4.7上,会得到如下结果:

::05.9718129
::05.9199793
::06.0203108
::05.9458049
::05.9622262

而在.NET Core 2.0中,会得到如下结果:

::03.1283763
::03.0925150
::02.9778923
::03.0782851

吞吐量提高约2倍。

下面是比较字符串部分。这是一个使用String.StartsWith和序数比较的例子:

using System;
using System.Diagnostics;
using System.Linq;
public class Test
{
public static void Main()
{
string s = string.Concat(Enumerable.Repeat("a", )) + "b";
while (true)
{
var sw = Stopwatch.StartNew();
for (int i = ; i < 100_000_000; i++)
{
s.IndexOf('b');
}
Console.WriteLine(sw.Elapsed);
}
}
}

在.NET 4.7上会得到如下结果:

::01.3097317
::01.3072381
::01.3045015
::01.3068244
::01.3210207

.NET Core 2.0会得到如下结果:

::00.6239002
::00.6150021
::00.6147173
::00.6129136
::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(), gen1 = GC.CollectionCount(), gen2 = GC.CollectionCount();
sw.Start(); for (int i = ; i < ; 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($"Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0} Gen1={GC.CollectionCount(1) - gen1} Gen2={GC.CollectionCount(2) - gen2}");
}
}

FileStream中的开销也在进一步减少,例如DOTNET / corefx#11569增加了一个专门的CopyToAsync实现,dotnet/ corefx#2929也改进了异步写入的处理,.NET 4.7会得到如下结果:

Elapsed=::09.4070345 Gen0= Gen1= Gen2=

.NET Core 2.0会得到如下结果:

Elapsed=::06.4286604 Gen0= Gen1= Gen2=

网络

网络是值得关注的部分,这部分也将取得很大的改进。目前正在付出很大的努力来优化和调整低等级的网络堆栈,以便高效地构建更高级别的组件。

这种改变带来的一个很大的影响是PR dotnet / corefx#15141SocketAsyncEventArgsSocket上大量异步操作的核心,它支持同步完成模型,因此异步操作实际完成了同步操作,这样避免了异步操作的分配消耗。但是,.NET 4.7中的同步操作运算是失败的, PR修复了上述的实现问题,允许在socket上进行所有异步操作的同步完成。这样的提升在以下代码中变现的非常明显:

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, ));
listener.Listen(); 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[];
clientSaea.SetBuffer(sendBuffer, , sendBuffer.Length);
clientSaea.Completed += delegate { clientAre.Set(); }; byte[] receiveBuffer = new byte[];
serverSaea.SetBuffer(receiveBuffer, , receiveBuffer.Length);
serverSaea.Completed += delegate { serverAre.Set(); }; var sw = new Stopwatch();
int gen0 = GC.CollectionCount(), gen1 = GC.CollectionCount(), gen2 = GC.CollectionCount();
sw.Start(); for (int i = ; 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($"Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0} Gen1={GC.CollectionCount(1) - gen1} Gen2={GC.CollectionCount(2) - gen2}");
}
}
}
}
}

该程序创建两个连接的socket,然后向socket写入1000次,并且在案例中使用异步方法接收,但绝大多数操作将同步完成。在.NET 4.7中会得到如下结果:

Elapsed=::20.5272910 Gen0= Gen1= Gen2=

在.NET Core 2.0中,大多数操作能够同步完成,得到如下结果:

Elapsed=::05.6197060 Gen0= Gen1= Gen2=

不仅仅是直接使用socket来实现组件的这种改进,而且还通过更高级别的组件来间接使用socket,其他PR的结果是更高级别组件(如NetworkStream)的额外性能提升。例如,PR dotnet / corefx#16502在SocketAsyncEventArgs上重新实现了基于Socket的SendAsync和ReceiveAsync操作,并且允许它们在NetworkStream中使用Read / WriteAsync和PR dotnet / corefx#12664添加了一个专门的CopyToAsync重写,以便更有效地从NetworkStream读取数据并将其复制到其他流中。这些变化对NetworkStream吞吐量和分配有非常大的影响。看看下面这个例子:

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, ));
listener.Listen(); 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[];
new Random().NextBytes(data); var sw = new Stopwatch();
int gen0 = GC.CollectionCount(), gen1 = GC.CollectionCount(), gen2 = GC.CollectionCount();
sw.Start(); for (int i = ; i < 1_000_000; i++)
{
await clientStream.WriteAsync(data, , data.Length);
}
client.Shutdown(SocketShutdown.Send);
serverCopyAll.Wait();
sw.Stop(); Console.WriteLine($"Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0} Gen1={GC.CollectionCount(1) - gen1} Gen2={GC.CollectionCount(2) - gen2}");
}
}
}
}
}

与之前的Socket一样,下面我们创建两个连接的socket,然后把它们包含在NetworkStream中。在其中一个流中,我们将1K数据写入一百万次,而另一个流则通过CopyToAsync操作读出所有数据。在.NET 4.7中,会得到如下输出:

Elapsed = ::24.7827947 Gen0 =  Gen1 =  Gen2 = 

而在.NET Core 2.0中,时间减少了5倍,垃圾回收有效地减少到零:

Elapsed=::05.6456073 Gen0= Gen1= Gen2=

其它网络相关组件也将得到进一步优化。例如SslStream通常将围绕在NetworkStream中,以便向连接中添加SSL。下面的示例将看到这种影响,这个示例将在NetworkStream之上添加SslStream的用法:

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) <= ) mres.Set();
else ThreadPool.QueueUserWorkItem(wc);
}; var sw = new Stopwatch();
int gen0 = GC.CollectionCount(), gen1 = GC.CollectionCount(), gen2 = GC.CollectionCount();
sw.Start(); for (int i = ; i < Environment.ProcessorCount; i++) ThreadPool.QueueUserWorkItem(wc);
mres.Wait(); Console.WriteLine($"Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0} Gen1={GC.CollectionCount(1) - gen1} Gen2={GC.CollectionCount(2) - gen2}");
}
}
}

在.NET 4.7中,会得到如下结果:

Elapsed=::21.1171962 Gen0= Gen1= Gen2=

.NET Core 2.0包含了诸如dotnet / corefx#12935dotnet / corefx#13274等PR的改进,这两者都将大大减少了使用SslStream所涉及的分配。在.NET Core 2.0上运行相同的代码时,会得到如下结果:

Elapsed=::05.6456073 Gen0= Gen1= Gen2=

85%的垃圾收集已被删除!

并发

对于并发和并行性相关的原始化和基础部分,也得到了许多改进。

这里的一个关键点是ThreadPool,它是执行许多.NET应用程序的核心。例如,PR dotnet / coreclr#3157减少了QueueUserWorkItem中涉及的某些对象的大小,PR dotnet / coreclr#9234使用了ConcurrentQueue <T>重写来替换ThreadPool的全局队列,其中会用到较少的同步和分配。从以下的示例中,会看到最终结果:

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) <= ) mres.Set();
else ThreadPool.QueueUserWorkItem(wc);
}; var sw = new Stopwatch();
int gen0 = GC.CollectionCount(), gen1 = GC.CollectionCount(), gen2 = GC.CollectionCount();
sw.Start(); for (int i = ; i < Environment.ProcessorCount; i++) ThreadPool.QueueUserWorkItem(wc);
mres.Wait(); Console.WriteLine($"Elapsed={sw.Elapsed} Gen0={GC.CollectionCount(0) - gen0} Gen1={GC.CollectionCount(1) - gen1} Gen2={GC.CollectionCount(2) - gen2}");
}
}
}

在.NET 4.7中,会等到如下结果:

Elapsed=::03.6263995 Gen0= Gen1= Gen2=
Elapsed=::03.6304345 Gen0= Gen1= Gen2=
Elapsed=::03.6142323 Gen0= Gen1= Gen2=
Elapsed=::03.6565384 Gen0= Gen1= Gen2=
Elapsed=::03.5999892 Gen0= Gen1= Gen2=

而在.NET Core 2.0中,会得到如下结果:

Elapsed=::02.1797508 Gen0= Gen1= Gen2=
Elapsed=::02.1188833 Gen0= Gen1= Gen2=
Elapsed=::02.1000003 Gen0= Gen1= Gen2=
Elapsed=::02.1024852 Gen0= Gen1= Gen2=
Elapsed=::02.1044461 Gen0= Gen1= Gen2=

这是一个巨大的吞吐量的改善,并且这样一个核心组件的垃圾量也将大幅减少。

同步原语也在.NET Core中得到提升。例如,低级并发代码通常使用SpinLock来尝试避免分配锁定对象或最小化竞争锁所花费的时间。PR dotnet / coreclr#6952改进了失败的快速路径,以下测试会得到显而易见的结果:

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 = ; i < 100_000_000; i++)
{
taken = false;
sl.TryEnter(, ref taken);
}
Console.WriteLine(sw.Elapsed);
}
}
}

在.NET 4.7中,会得到如下结果:

::02.3276463
::02.3174042
::02.3022212
::02.3015542
::02.2974777

而在.NET Core 2.0中,会得到如下结果:

::00.3915327
::00.3953084
::00.3875121
::00.3980009
::00.3886977

吞吐量的这种差异可能会对运行这种锁的热路径产生很大的影响。

这只是众多例子中的一个。另一个例子围绕着Lazy<T>,它被PR dotnet / coreclr#8963manofstick重写,以便提高访问初始化过的Lazy <T>的效率。这样的提升效果从下面的示例中清晰可见:

using System;
using System.Diagnostics;
class Test
{
static int s_result; static void Main()
{
while (true)
{
var lazy = new Lazy<int>(() => );
s_result = lazy.Value; var sw = Stopwatch.StartNew();
for (int i = ; i < 1_000_000_000; i++)
{
s_result = lazy.Value;
}
Console.WriteLine(sw.Elapsed);
}
}
}

在.NET 4.7中,会得到的结果如下:

::02.6769712
::02.6789140
::02.6535493
::02.6911146
::02.7253927

而在.NET Core 2.0中,会得到的结果如下:

::00.5278348
::00.5594950
::00.5458245
::00.5381743
::00.5502970

吞吐量增加约5倍。

下一步是什么

本文只涉及了部分.NET Core的性能改进。在dotnet / corefxdotnet / coreclr repos 中的pull请求中搜索“perf”或“performance”,你会发现接近一千个合并的PR改进。其中一些是比较大的同时也很有影响力的改进,而另一些则主要减少了库和运行时的消耗,这些变化一起起作用,保证了能够在.NET Core上更快的运行应用程序。展望未来,性能将成为关注的重点,无论是以性能改进为目标的API还是现有库的性能的改进。

欢迎大家深入了解.NET Core代码库,以便找到影响自己的应用程序和库的瓶颈,并提交PR来修复它们。如果你的问题得到修复,也请将修复程序分享给所有需要的人。

相关阅读:

Visual Studio 2017正式版发布全纪录

从Visual Studio看微软20年技术变迁

Visual Studio 20周年,我和VS不得不说的故事

是什么优化让 .NET Core 性能飙升?的更多相关文章

  1. 是什么优化让 .NET Core 性能飙升?(转)

    欢迎大家持续关注葡萄城控件技术团队博客,更多更好的原创文章尽在这里~~ .NET Core(开放源代码,跨平台,x-copy可部署等)有许多令人兴奋的方面,其中最值得称赞的就是其性能了. 感谢所有社区 ...

  2. ASP.NET Core 性能优化最佳实践

    本文提供了 ASP.NET Core 的性能最佳实践指南. 译文原文地址:https://docs.microsoft.com/en-us/aspnet/core/performance/perfor ...

  3. 通过/proc/sys/net/ipv4/优化Linux下网络性能

    通过/proc/sys/net/ipv4/优化Linux下网络性能 /proc/sys/net/ipv4/优化1)      /proc/sys/net/ipv4/ip_forward该文件表示是否打 ...

  4. NVIDIA深度学习Tensor Core性能解析(下)

    NVIDIA深度学习Tensor Core性能解析(下) DeepBench推理测试之RNN和Sparse GEMM DeepBench的最后一项推理测试是RNN和Sparse GEMM,虽然测试中可 ...

  5. NVIDIA深度学习Tensor Core性能解析(上)

    NVIDIA深度学习Tensor Core性能解析(上) 本篇将通过多项测试来考验Volta架构,利用各种深度学习框架来了解Tensor Core的性能. 很多时候,深度学习这样的新领域会让人难以理解 ...

  6. 优化Web中的性能

    优化Web中的性能 简介 web的优化就是一场阻止http请求最终访问到数据库的战争. 优化的方式就是加缓存,在各个节点加缓存. web请求的流程及节点 熟悉流程及节点,才能定位性能的问题.而且优化的 ...

  7. 不修改代码就能优化ASP.NET网站性能的一些方法

    阅读目录 开始 配置OutputCache 启用内容过期 解决资源文件升级问题 启用压缩 删除无用的HttpModule 其它优化选项 本文将介绍一些方法用于优化ASP.NET网站性能,这些方法都是不 ...

  8. 优化Angular应用的性能

    MVVM框架的性能,其实就取决于几个因素: 监控的个数 数据变更检测与绑定的方式 索引的性能 数据的大小 数据的结构 我们要优化Angular项目的性能,也需要从这几个方面入手. 1. 减少监控值的个 ...

  9. 【阿里云产品公测】利用PTS服务优化网站数据库读写性能

    [阿里云产品公测]利用PTS服务优化网站数据库读写性能 作者:阿里云用户千鸟 写这个帖子主要也是因为在用PTS测试网站的时候,手动访问网站进入报错页面,主要原因是数据库连接对象存在问题,导致并发多的时 ...

随机推荐

  1. haproxy-代码阅读-内存管理

    haproxy内存池概述 内存池按照类型分类,每个类型的内存池都有一个名字,用链表记录空闲的内存块,每个内存块大小相等,并按照16字节对齐. haporxy用pool_head 结构记录内存池 str ...

  2. 可视化之Berkeley Earth

    去年冬天雾霾严重的那几天,写了两篇关于空气质量的文章,<可视化之PM2.5>和<谈谈我对雾霾的认识>.坦白说,环境问题是一个无法逃避又无能为力的话题.最近因为工作中有一些数据可 ...

  3. vuex所有核心概念完整解析State Getters Mutations Actions

    vuex是解决vue组件和组件件相互通信而存在的,vue理解起来稍微复杂但一旦看懂择即为好用 安装: npm install --save vuex 引入 import Vuex from 'vuex ...

  4. Hybrid App开发之jQuery基础

    前言: 前面学习了JavaScript/Html/Css的基础知识,今天学习一下常用js框架jQuery的使用进行快速的开发. JQuery的基本功能: 方位和操作DOM元素 控制页面样式 对页面事件 ...

  5. scrapy架构初探

    scrapy架构初探 引言 Python即时网络爬虫启动的目标是一起把互联网变成大数据库.单纯的开放源代码并不是开源的全部,开源的核心是"开放的思想",聚合最好的想法.技术.人员, ...

  6. Java IO学习笔记二

    Java IO学习笔记二 流的概念 在程序中所有的数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成. 程序中的输入输 ...

  7. 关于MATLAB处理大数据坐标文件201762

    经过头脑风暴法想出了很多特征,目前经过筛选已经提交了两次数据,数据提交结果不尽如人意,但是收获很大. 接下来继续提取特征,特征数达到27时筛选出20条特征,并找出最佳搭配

  8. 神奇的 conic-gradient 圆锥渐变

    感谢 LeaVerou 大神,让我们可以提前使用上这么美妙的属性. conic-gradient 是个什么?说到 conic-gradient ,就不得不提的它的另外两个兄弟: linear-grad ...

  9. 根据list得到list中的最大值最小值

    List ll = new ArrayList(); ll.add(new BigDecimal(1)); ll.add(new BigDecimal(4.99)); ll.add(new BigDe ...

  10. Entity Framework入门教程:什么是Entity Framework

    Entity Framework简介 Entity Framework是微软提供的一个O/RM(对象关系映射)框架.它基于ADO.NET,为开发人员提供了一种自动化的机制来访问和存储数据库中的数据. ...