数据API 案例 开发者 关于
掌握聚合最新动态了解行业最新趋势
API接口,开发服务,免费咨询服务
新闻动态 > 媒体报道

是什么优化让 .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 = 0; i < 100_000_000; i++)
            {
                q.Enqueue(i);
                q.Dequeue();
            }
            Console.WriteLine(sw.Elapsed);
        }
    }
}

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

000000.9392595 
000000.9390453 
000000.9455784 
000000.9508294 
000001.0107745

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

000000.5514887 
000000.5662477 
000000.5627481 
000000.5685286 
000000.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(42, 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 = 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);
        }
    }
}

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

000001.1427246
000001.1295220 
000001.1350696 
000001.1502784 
000001.1677880

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

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

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

000000.4434135 
000000.4394329 
000000.4496867 
000000.4496383 
000000.4515505

和.NET Core 2.0,得到:

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

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

000002.6485174
000002.6144919 
000002.6699958 
000002.6441047 
000002.6255135

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

000001.7700190 
000001.8324078 
000001.7552966 
000001.7518632 
000001.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中,个人计算机输出如下结果:

000006.1366044
000005.7169339 
000006.3870274 
000005.5487718 
000006.6069291

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

000001.2052460 
000001.5269184 
000001.4638793 
000001.4963922 
000001.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($"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($"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
                    
掌握聚合最新动态了解行业最新趋势
API接口,开发服务,免费咨询服务
新闻动态 > 媒体报道
是什么优化让 .NET Core 性能飙升?
发布:2017-08-15

.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);
        }
    }
}

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

000000.9392595 
000000.9390453 
000000.9455784 
000000.9508294 
000001.0107745

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

000000.5514887 
000000.5662477 
000000.5627481 
000000.5685286 
000000.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(42, 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 = 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);
        }
    }
}

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

000001.1427246
000001.1295220 
000001.1350696 
000001.1502784 
000001.1677880

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

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

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

000000.4434135 
000000.4394329 
000000.4496867 
000000.4496383 
000000.4515505

和.NET Core 2.0,得到:

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

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

000002.6485174
000002.6144919 
000002.6699958 
000002.6441047 
000002.6255135

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

000001.7700190 
000001.8324078 
000001.7552966 
000001.7518632 
000001.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中,个人计算机输出如下结果:

000006.1366044
000005.7169339 
000006.3870274 
000005.5487718 
000006.6069291

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

000001.2052460 
000001.5269184 
000001.4638793 
000001.4963922 
000001.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($"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($"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
            
电话 0512-88869195
数 据 驱 动 未 来
Data Drives The Future