.Net 5 性能改进

起因

在.Net Core跳过4.0,避免和先.Net Framework 4.0同名,版本号变为5.0,同时也不在叫.Net Core改为.Net 5(统一的叫法),先看看官方对.Net版本规划.
.Net 版本规划和支持
本文主要是根据https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-5/ 翻译而来.不完全翻译.顺序也有所调整.

从CPU平台看.Net 5改进

.Net 5开始使用ARM64指令集优化
在.Net 5 开始使用Arm64指令集进行性能优化,这对国产飞腾和华为鲲鹏服务器,在性能上是有很大的提升.在有就是国产龙芯处理器开始在.Net Core 3.1进行支持,不知道在.Net 5正式发布前.龙芯指令集的代码会不会合并到.Net 5代码的主干中.

从功能上看.Net 5改进

.Net 5在性能上的改进

GC

GC对性能的影响还是很大的.是因为GC回收资源的时候会挂起工作线程,只留GC线程清理资源和回收内存,造成程序有短暂的停顿.
如何提高GC性能:
  1. 减少内存分配,就能减少GC回收的次数
  2. 减少GC线程挂起的时间.让工作线程一直在执行任务(说白点就是让工作线程一直处于干活的状态)
在.Net 5 GC改进:
  1. 在Server GC中增加均衡/平衡机制(Balance),给每个GC线程一样多的工作量(理论上),每个GC线程执行的时间也是一样的.避免某个GC线程一直在工作,其他GC线程没有任务可执行.从而缩短GC线程挂起的时间. 有专门说均衡机制的文章https://devblogs.microsoft.com/dotnet/balancing-work-on-gc-threads/文章 
  2. 减少 第0代(gen0)和第1代(gen1)回收次数
  3. 减少GC扫描静态数据和减少使用并发锁
  4. 从CoreCLR(c/c++代码) 部分代码(如Array.Sort)移植到System.Private.Corelib(C#代码),这样的好处,就是代码复用(CoreCLR和Mono共用一个实现),c#代码是安全的(相对于c语言,如数组越界等),可以更好的优化C#代码.
关于GC示例1代码:
using System;
using System.Diagnostics;
using System.Threading;

class Program
{
    public static void Main()
    {
        new Thread(() =>
        {
            var a = new int[20];
            while (true) Array.Sort(a);
        }) { IsBackground = true }.Start();

        var sw = new Stopwatch();
        while (true)
        {
            sw.Restart();
            for (int i = 0; i < 10; i++)
            {
                GC.Collect();
                Thread.Sleep(15);
            }
            Console.WriteLine(sw.Elapsed.TotalSeconds);
        }
    }
}

对比.Net 3.1和5在GC回收,对比很明显,在.Net 5 GC线程回收的更快
关于GC示例2代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Running;

namespace dotnet_perf
{
    public class DoubleSorting : Sorting<double>
    {
        protected override double GetNext() => _random.Next();
    }
    public class Int32Sorting : Sorting<int>
    {
        protected override int GetNext() => _random.Next();
    }
    public class StringSorting : Sorting<string>
    {
        protected override string GetNext()
        {
            var dest = new char[_random.Next(1, 5)];
            for (int i = 0; i < dest.Length; i++) dest[i] = (char)('a' + _random.Next(26));
            return new string(dest);
        }
    }

    public abstract class Sorting<T>
    {
        protected Random _random;
        private T[] _orig, _array;

        [Params(10)]
        public int Size { get; set; }

        protected abstract T GetNext();

        [GlobalSetup]
        public void Setup()
        {
            _random = new Random(42);
            _orig = Enumerable.Range(0, Size).Select(_ => GetNext()).ToArray();
            _array = (T[])_orig.Clone();
            Array.Sort(_array);
        }

        [Benchmark]
        public void Random()
        {
            _orig.AsSpan().CopyTo(_array);
            Array.Sort(_array);
        }
    }
}

在数组排序性能提升,即使是字符串数组性能提升对.Net Core 3.1也有20%

JIT改进

JIT(即时编译器,也有人称实时编译器).作用就是C#/Vb.Net代码(编译后生成IL代码,CPU是不认识什么是IL代码的),在运行的时候,JIT生成汇编代码(或者叫机器指令),再有CPU去执行.

JIT这里有两个作用:
  1. 安全检查,说C#/VB.Net是安全的语言,第一是编译的时候,对代码进行安全检查.第二是在程序运行的时候,JIT也会进行安全检查.
  2. 生成汇编代码.
JIT对程序的性能也有很大的比重.所以要求JIT生成性能更高,代码更少的指令(通常情况下汇编指令越少,性能越高,但不是绝对的,比如使用CPU自带的指令).

C#和Java跨平台是都有中间语言的存在(.Net的IL和Java的ByteCode),这里的平台指CPU架构,CPU架构分为CISC(复杂指令集,代表为X86)和RISC(精简指令集,代表为ARM和国产龙芯),在JIT将中间语言生成对应的平台的指令.
示例1:
using System;
using BenchmarkDotNet.Attributes;

namespace dotnet_perf
{
    public class TestJit
    {
        private B[] _array = new B[42];

        [Benchmark]
        public int Ctor() => new Span<B>(_array).Length;
    }

    class A
    {
    }
    sealed class B : A
    {
    }
}

在.Net 5中JIT生成的代码更小,速度更快

汇编代码对比:

.NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT

; dotnet_perf.TestJit.Ctor()
;         public int Ctor() => new Span<B>(_array).Length;
;                              ^^^^^^^^^^^^^^^^^^^^^^^^^^
       push      rdi
       push      rsi
       sub       rsp,28
       mov       rsi,[rcx+8]
       test      rsi,rsi
       jne       short M00_L00
       xor       eax,eax
       jmp       short M00_L01
M00_L00:
       mov       rcx,rsi
       call      00007FF884C41F50
       mov       rdi,rax
       mov       rcx,7FF82531DEAA
       call      CORINFO_HELP_TYPEHANDLE_TO_RUNTIMETYPE
       cmp       rdi,rax
       jne       short M00_L02
       mov       eax,[rsi+8]
M00_L01:
       add       rsp,28
       pop       rsi
       pop       rdi
       ret
M00_L02:
       call      System.ThrowHelper.ThrowArrayTypeMismatchException()
       int       3
; Total bytes of code 66

.NET Core 5.0.0 (CoreCLR 5.0.20.47505, CoreFX 5.0.20.47505), X64 RyuJIT

; dotnet_perf.TestJit.Ctor()
;         public int Ctor() => new Span<B>(_array).Length;
;                              ^^^^^^^^^^^^^^^^^^^^^^^^^^
       mov       rax,[rcx+8]
       test      rax,rax
       jne       short M00_L00
       xor       eax,eax
       jmp       short M00_L01
M00_L00:
       mov       eax,[rax+8]
M00_L01:
       ret
; Total bytes of code 17

从上方的汇编代码对比,发现.Net 5生成的汇编代码更少,从执行时间来看,.Net 5生成的代码性能更高.

Intrinsics(内部函数,也有称内联函数,这里翻译为指令)

Intrinsics为什么这里要翻译为指令,是因为Intrinsics函数都是在指令集,如X86的AVX/SSE等.
说起这个Intrinsics就得说SIMD(Single Instruction Multiple Data,即单指令流多数据流).

代码:
using System.Numerics;
using BenchmarkDotNet.Attributes;

namespace App_Pef5
{
    [DisassemblyDiagnoser(printSource: true)]
    //[RyuJitX64Job]
    public class Intrinsics
    {
        [Benchmark]
        public void T1()
        {
            double[] op1 = new double[] { 1.0, 2.0, 3.0, 4.0 };
            double[] op2 = new double[] { 1.0, 2.0, 3.0, 4.0 };
            double[] result = new double[4];

            for (int i = 0; i < 10000; i++)
            {
                var v1 = new Vector<double>(op1, 0);
                var v2 = new Vector<double>(op2, 0);
                var v3 = Vector.Add(v1, v2);
                v3.TryCopyTo(result);
            }
        }

        [Benchmark]
        public void T2()
        {
            double[] op1 = new double[] { 1.0, 2.0, 3.0, 4.0 };
            double[] op2 = new double[] { 1.0, 2.0, 3.0, 4.0 };
            double[] result = new double[4];

            for (int j = 0; j < 10000; j++)
            {
                for (int i = 0; i < op1.Length; i++)
                {
                    result[i] = op1[i] + op2[i];
                }
            }
        }
    }
}

在.Net 5使用intrinsics函数(进行了包装,在jit调用的时候,转换为指令)

T1函数生成汇编代码:
; App_Pef5.Intrinsics.T1()
       push      rdi
       push      rsi
       sub       rsp,28
       vzeroupper
;             double[] op1 = new double[] { 1.0, 2.0, 3.0, 4.0 };
;             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       mov       rcx,offset MT_System.Double[]
       mov       edx,4
       call      CORINFO_HELP_NEWARR_1_VC
       mov       rsi,rax
       mov       rcx,14C58332BE0
       vmovdqu   xmm0,xmmword ptr [rcx]
       vmovdqu   xmmword ptr [rsi+10],xmm0
       vmovdqu   xmm0,xmmword ptr [rcx+10]
       vmovdqu   xmmword ptr [rsi+20],xmm0
;             double[] op2 = new double[] { 1.0, 2.0, 3.0, 4.0 };
;             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       mov       rcx,offset MT_System.Double[]
       mov       edx,4
       call      CORINFO_HELP_NEWARR_1_VC
       mov       rdi,rax
       mov       rcx,14C58332BE0
       vmovdqu   xmm0,xmmword ptr [rcx]
       vmovdqu   xmmword ptr [rdi+10],xmm0
       vmovdqu   xmm0,xmmword ptr [rcx+10]
       vmovdqu   xmmword ptr [rdi+20],xmm0
;             double[] result = new double[4];
;             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       mov       rcx,offset MT_System.Double[]
       mov       edx,4
       call      CORINFO_HELP_NEWARR_1_VC
;             for (int i = 0; i < 10000; i++)
;                  ^^^^^^^^^
       xor       edx,edx
;                 var v1 = new Vector<double>(op1, 0);
;                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
M00_L00:
       vmovupd   ymm0,[rsi+10]
;                 var v2 = new Vector<double>(op2, 0);
;                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       vmovupd   ymm1,[rdi+10]
       vaddpd    ymm0,ymm0,ymm1
;                 v3.TryCopyTo(result);
;                 ^^^^^^^^^^^^^^^^^^^^^
       lea       rcx,[rax+10]
       mov       r8d,4
       cmp       r8d,4
       jb        short M00_L01
       vmovupd   [rcx],ymm0
M00_L01:
       inc       edx
       cmp       edx,2710
       jl        short M00_L00
       vzeroupper
       add       rsp,28
       pop       rsi
       pop       rdi
       ret
; Total bytes of code 189

T2函数生成汇编代码:

; App_Pef5.Intrinsics.T2()
       push      rdi
       push      rsi
       sub       rsp,28
       vzeroupper
;             double[] op1 = new double[] { 1.0, 2.0, 3.0, 4.0 };
;             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       mov       rcx,offset MT_System.Double[]
       mov       edx,4
       call      CORINFO_HELP_NEWARR_1_VC
       mov       rsi,rax
       mov       rcx,212ECBD2BE0
       vmovdqu   xmm0,xmmword ptr [rcx]
       vmovdqu   xmmword ptr [rsi+10],xmm0
       vmovdqu   xmm0,xmmword ptr [rcx+10]
       vmovdqu   xmmword ptr [rsi+20],xmm0
;             double[] op2 = new double[] { 1.0, 2.0, 3.0, 4.0 };
;             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       mov       rcx,offset MT_System.Double[]
       mov       edx,4
       call      CORINFO_HELP_NEWARR_1_VC
       mov       rdi,rax
       mov       rcx,212ECBD2BE0
       vmovdqu   xmm0,xmmword ptr [rcx]
       vmovdqu   xmmword ptr [rdi+10],xmm0
       vmovdqu   xmm0,xmmword ptr [rcx+10]
       vmovdqu   xmmword ptr [rdi+20],xmm0
;             double[] result = new double[4];
;             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       mov       rcx,offset MT_System.Double[]
       mov       edx,4
       call      CORINFO_HELP_NEWARR_1_VC
;             for (int j = 0; j < 10000; j++)
;                  ^^^^^^^^^
       xor       edx,edx
;                 for (int i = 0; i < op1.Length; i++)
;                      ^^^^^^^^^
M00_L00:
       xor       ecx,ecx
;                     result[i] = op1[i] + op2[i];
;                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
M00_L01:
       movsxd    r8,ecx
       vmovsd    xmm0,qword ptr [rsi+r8*8+10]
       vaddsd    xmm0,xmm0,qword ptr [rdi+r8*8+10]
       vmovsd    qword ptr [rax+r8*8+10],xmm0
       inc       ecx
       cmp       ecx,4
       jl        short M00_L01
       inc       edx
       cmp       edx,2710
       jl        short M00_L00
       add       rsp,28
       pop       rsi
       pop       rdi
       ret
; Total bytes of code 185

使用intrinsics指令,单次并不会带来性能的提升,需要在多次使用的时候,才能带来更好的性能,因为上面的代码,是我首次使用intrinsics,后面在去了解C/C++中是如何使用的.在去整体对比性能.

Runtime Helpers(运行时中的辅助函数)

在Runtime(CLR)中GC和JIT占很大的比重,Runtime Helpers也很重要,因为JIT生成汇编代码(或着叫机器码),是需要借助运行时中的一些辅助函数,来减少生成汇编代码中的开销.
  1. 类型检查开销大的地方,通过增加缓存来提升性能.
  2. 改进泛型方法缓存,原先是通过字典进行缓存,一旦字典中没有,通过查找就比较慢了.
增加缓存后效果:
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;

namespace dotnet_perf
{
    public class RuntimeHelperCache
    {
        private List<string> _list = new List<string>();

        // IReadOnlyCollection<out T> is covariant
        [Benchmark] public bool IsIReadOnlyCollection() => IsIReadOnlyCollection(_list);
        [MethodImpl(MethodImplOptions.NoInlining)]
        private static bool IsIReadOnlyCollection(object o) => o is IReadOnlyCollection<int>;
    }
}

在Runtime中的辅助函数中增加缓存,来提升性能

改进泛型方法缓存后:

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;


namespace dotnet_perf
{
    public class RuntimeHelperCache2
    {
        [Benchmark]
        public void GenericDictionaries()
        {
            for (int i = 0; i < 14; i++)
                GenericMethod<string>(i);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static object GenericMethod<T>(int level)
        {
            switch (level)
            {
                case 0: return typeof(T);
                case 1: return typeof(List<T>);
                case 2: return typeof(List<List<T>>);
                case 3: return typeof(List<List<List<T>>>);
                case 4: return typeof(List<List<List<List<T>>>>);
                case 5: return typeof(List<List<List<List<List<T>>>>>);
                case 6: return typeof(List<List<List<List<List<List<T>>>>>>);
                case 7: return typeof(List<List<List<List<List<List<List<T>>>>>>>);
                case 8: return typeof(List<List<List<List<List<List<List<List<T>>>>>>>>);
                case 9: return typeof(List<List<List<List<List<List<List<List<List<T>>>>>>>>>);
                case 10: return typeof(List<List<List<List<List<List<List<List<List<List<T>>>>>>>>>>);
                case 11: return typeof(List<List<List<List<List<List<List<List<List<List<List<T>>>>>>>>>>>);
                case 12: return typeof(List<List<List<List<List<List<List<List<List<List<List<List<T>>>>>>>>>>>>);
                default: return typeof(List<List<List<List<List<List<List<List<List<List<List<List<List<T>>>>>>>>>>>>>);
            }
        }
    }
}

在Runtime中的辅助函数中,改进泛型方法缓存

Text Processing(文本处理)

Text Processing(文本处理),这里主要指字符/字符串的处理,及其他类型转为字符串.在开发程序中,字符串的处理是必不可少的,所以文本处理的性能会影响程序的性能(不绝对哈,这是排除网络/ I/O和数据库).
  1. char.IsWhiteSpace
  2. char.ToUpperInvariant
  3. string.IsEmptyOrWhiteSpace
  4. string.Trim
  5. string.StartsWith
  6. string.EndsWith
  7. ToString
示例:
namespace dotnet_perf
{
    [DisassemblyDiagnoser(printSource: true)]
    public class TextProcessing
    {
        [Benchmark]
        public int Trim() => " test ".AsSpan().Trim().Length;

        [Benchmark]
        public string ToString12345() => 12345.ToString();

        [Benchmark]
        public string ToString123() => ((byte)123).ToString();

    }
}

在.Net 5 String性能改进从上图看出,性能得到了改进,生成的指令也少了不少.

Regular Expressions(正则表达式)

正则表达式,这一块在.Net 5改进很大,可以放心使用了,对于正则表达式这一块的改进比较详细的博客,在这里Regex Performance Improvements in .NET 5,所以这里不过多进行翻译,后期有时间对这篇博客进行翻译.

线程异步

异步相关的相关改进,也有一篇专门的博客,后面着重学习和翻译这篇博客.博客地址:https://devblogs.microsoft.com/dotnet/async-valuetask-pooling-in-net-5/ 

集合

集合性能改进:
  1. Dictionary 和 ConcurrentDictionary 通过改进哈希算法,可以更快的获取值和判断Key是否存在.
  2. HashSet 也得到了改进,有一部分是@JeffreyZhao .Net技术大牛 提交的代码.
  3. ImmutableArray
  4. ImmutableList
  5. BitArray 在.Net 5中引入AVX2和SSE2指令,同样在ARM中也引入的内部指令.
第1到4放在一起,进行Benchmark性能测试对比:
.Net 5  集合在性能上优化
从上图可以看出,Dictionary 和 ConcurrentDictionary及HashSet性能提升都是很大的,而ImmutableArray和ImmutableList改进提升的更厉害(主要是在.Net Framework 4.8和.Net Core 3.1的时候没有进行优化).

BitArray性能测试结果:
BitArray在.Net 5通过引入cpu指令,性能提升很厉害

Linq

Linq在.Net 5改进的并不是很多,是因为在.Net Core早期的时候改进了很多.得益于一部分本机代码改为托管代码(在上方GC 第4点有介绍)和基于Span在OrderBy和SkipLast也得到性能提升.
using System;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;

namespace dotnet_perf
{
    public class LinqTest
    {
        private int[] _array;

        [GlobalSetup]
        public void Setup()
        {
            var r = new Random(42);
            _array = Enumerable.Range(0, 1_000).Select(_ => r.Next()).ToArray();
        }

        [Benchmark]
        public void Sort()
        {
            foreach (int i in _array.OrderBy(i => i)) { }
        }


        private IEnumerable<int> data = Enumerable.Range(0, 100).ToList();

        [Benchmark]
        public int SkipLast() => data.SkipLast(5).Sum();
    }
}

.Net 5 在Linq性能改进的地方

Networking(网络)


从细节上看有哪些改进

  • 更快的加载程序集,在.Net Core时,程序集被拆分的很多且很小的,加载很多很小的是会增加开销,在.Net 5中通过合并程序集,减少开销.
  • 更快的数学库(算法).
    1. 改进NaN检查.生成更小更快的代码.
    2. SSE和AMD64 (Intrinsics为内部函数) 
    3. 改进哈希值
  • 更快的加密,如RSA.
  • 更快的P/Invoke操作,Windows和Linux
  • 更快的reflection emit
  • 更快的I/O操作,
  • 更少的内存分配.
    1. 减少一些字符串内存分配
    2. 减少一些装箱操作
    3. 删除一些临时内存分配

秋风 2020-10-08