起因
在.Net Core跳过4.0,避免和先.Net Framework 4.0同名,版本号变为5.0,同时也不在叫.Net Core改为.Net 5(统一的叫法),先看看官方对.Net版本规划.
本文主要是根据https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-5/ 翻译而来.不完全翻译.顺序也有所调整.
从CPU平台看.Net 5改进
在.Net 5 开始使用Arm64指令集进行性能优化,这对国产飞腾和华为鲲鹏服务器,在性能上是有很大的提升.在有就是国产龙芯处理器开始在.Net Core 3.1进行支持,不知道在.Net 5正式发布前.龙芯指令集的代码会不会合并到.Net 5代码的主干中.(编者朱:这个可能性是没有了,.NET 6是很有可能的)
从功能上看.Net 5改进
GC
GC对性能的影响还是很大的.是因为GC回收资源的时候会挂起工作线程,只留GC线程清理资源和回收内存,造成程序有短暂的停顿.
如何提高GC性能:
减少内存分配,就能减少GC回收的次数
减少GC线程挂起的时间.让工作线程一直在执行任务(说白点就是让工作线程一直处于干活的状态)
在.Net 5 GC改进:
在Server GC中增加均衡/平衡机制(Balance),给每个GC线程一样多的工作量(理论上),每个GC线程执行的时间也是一样的.避免某个GC线程一直在工作,其他GC线程没有任务可执行.从而缩短GC线程挂起的时间. 有专门说均衡机制的文章https://devblogs.microsoft.com/dotnet/balancing-work-on-gc-threads/文章
减少 第0代(gen0)和第1代(gen1)回收次数
减少GC扫描静态数据和减少使用并发锁
从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);
}
}
}
关于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);
}
}
}
JIT改进
JIT(即时编译器,也有人称实时编译器).作用就是C#/Vb.Net代码(编译后生成IL代码,CPU是不认识什么是IL代码的),在运行的时候,JIT生成汇编代码(或者叫机器指令),再有CPU去执行.
JIT这里有两个作用:
安全检查,说C#/VB.Net是安全的语言,第一是编译的时候,对代码进行安全检查.第二是在程序运行的时候,JIT也会进行安全检查.
生成汇编代码.
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 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];
}
}
}
}
}
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++中是如何使用的.在去整体对比性能.
从细节上看有哪些改进
更快的加载程序集,在.Net Core时,程序集被拆分的很多且很小的,加载很多很小的是会增加开销,在.Net 5中通过合并程序集,减少开销.
更快的数学库(算法).
改进NaN检查.生成更小更快的代码.
SSE和AMD64 (Intrinsics为内部函数)
改进哈希值
更快的加密,如RSA.
更快的P/Invoke操作,Windows和Linux
更快的reflection emit
更快的I/O操作,
更少的内存分配.
减少一些字符串内存分配
减少一些装箱操作
删除一些临时内存分配