深度探秘.NET 5.0

今年11月10号 .NET 5.0 如约而至。这是.NET All in one后的第一个版本,虽然不是LTS(Long term support)版本,但是是生产环境可用的。

有微软的背书,微软从.NET Preview 1就开始在自己的网站上运行.NET 5, (Bing.com、dot.net已升级并运行了数个月),同时早期的.NET Core版本可以直接升级到.NET 5. 所以大家可以放心使用的。

接下来,我们探秘.NET 5.0这次带来了哪些新的特性。

一、.NET 5.0 的一些亮点(Highlights)

1. 通过线上(生产环境)测试(battle-tested) : .NET5.0 通过在Bing.com和dot.net 托管运行数个月,全面通过了线上验证,这证明这个版本是生产可用的

2. 性能大幅提升:GC、JIT、正则表达式、多线程和异步处理、集合、LINQ、网络访问、JSON序列化、gRPC等等,了解详细可以访问

3. C# 9和F# 9 的语音提升:例如C#9的顶级程序和记录,而F#5提供了交互式编程,并提高了.NET上的功能性编程的性能。

4. .NET库增强了Json序列化,正则表达式和HTTP(HTTP 1.1,HTTP / 2)的性能。这一点在第二条中已经有所涉及。

5. P95 的延迟有所减少,得益于GC、分层编译和其他组件的一些改进

6.更好、更灵活的应用部署选项:ClickOnce客户端应用程序发布,单文件应用程序,减小的容器映像大小以及添加的Server Core容器映像。

7.平台支持的范围进一步扩展Windows Arm64WebAssembly

 二、再看统一平台的愿景

   2019年5月6号,微软发布了.NET 5.0 统一平台的愿景:将来只会有一个.NET,您将可以使用它来定位Windows,Linux,macOS,iOS,Android,tvOS,watchOS和WebAssembly等。

   

   实现这一愿景的第一步是整合.NET仓库,即:整合关键的.NET代码库, 其中包括很大一部分的Mono。为.NET运行库和库提供一个存储库是在各处交付相同产品的前提。Blazor是利用代码合并和.NET统一的最佳示例。Blazor WebAssembly的运行时和库现在是从合并的dotnet /运行时仓库中构建的。这意味着服务器上的Blazor WebAssembly和Blazor使用与完全相同的代码List<T>。

   代码整合后,.NET Framework怎么办?

   .NET Framework仍然是受支持的Microsoft产品,并且每个新版本的Windows都将继续支持.NET Framework。去年,微软宣布已停止向.NET Framework添加新功能,并已向.NET Core添加.NET Framework API。

   这就意味着,.NET Framework已经停更了,版面目前停留在.NET Framework 4.8. 

   这也是没办法的事情,统一后的.NET, 从.NET5.0开始迭代了。这次.NET 5.0的Release列表也能发现这个情况:

   

    在上述情况下,目前是将.NET Framework升级到.NET Core的最佳时机了。如果比较在意LTS版本,也可以等到明年.NET 6统一升级。对于此,微软的建议是:

    对于.NET Framework客户端开发人员,.NET 5.0支持Windows窗体和WPF。

    对于.NET Framework服务器开发人员, 如果采用ASP.NET Core才能使用.NET 5.0。

    对于Web Forms开发人员,Blazor通过高效且更加现代的实现方式提供类似的开发人员体验。

    对于WCF服务器和Workflow用户可以查看支持这些框架的社区项目

    以上,对于统一后的.NET 5.0, 广大.NET Developers 可以放心、开心地去拥抱这次升级和统一,这代表了.NET的未来。

 三、深入看一下编程语言层面的提升(C# 9 和 F# 5)

   C#9和F#5是.NET 5.0版本的一部分,被包含在.NET 5.0 SDK中。接下来详细看一下C# 9 的一些语言新特性(F# 5用的比较少,不再做详细介绍):

  1. Top-level programs 顶级程序

   大家会问这是什么?这是在顶级编写程序的一种更简单的方式:一个更简单的 Program.cs 文件。

   我们知道,原先在Program类中,必须有Main函数,这是程序的一个EntryPoint入口。

using System;

namespace NET5Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

   .NET 5引入Top-level programs 后,我们不需要写Main函数了。可以直接这么写:

System.Console.WriteLine("Hello World!");

 大家会有疑问,真的没有Main函数了吗?其实这是个语法糖,我们通过IL Spy看一下反编译后的代码:

 

  2. 逻辑模式和属性模式匹配(Logical and property patterns)

  我们可以使用not or and 实现更强的更灵活的逻辑模式匹配:

  先看一个逻辑匹配的Demo:

var input = Console.ReadKey();
if (input.KeyChar is 'Y' or 'y')
{
    Console.WriteLine("You choosed yes!");
}

  再看一个Switch的Demo:

int score = 90;
switch (score)
{
    case 0:
        Console.WriteLine("0分.");
        break;
    case > 0 and <= 60:
        Console.WriteLine("合格.");
        break;
    case > 60 and <= 80:
        Console.WriteLine("优秀.");
        break;
    case > 80 and <= 100:
        Console.WriteLine("卓越.");
        break;
}

 属性模式匹配:通过两个{},实现对对象属性的模式匹配。

Type type = Type.GetType("System.String");
if (type is not null and { FullName: "System.String" })
{
    Console.WriteLine("It's type is System.String.");
}

  3. record类型

  record是一个新增的引用类型,与class很像,那么大家会问?为什么增加一个record类型呢?它的使用场景是什么呢?

  答案:为了方便比较数据是否一致。我们写个代码示意一下:

  假设我们有个User类,包含ID、Name、Gender、Tel几个属性,如果我们要对比2个User对象是否相等,我们可能需要逐个属性对比,或重写Equals、GetHashCode方法。

  那么如果我们用record类型呢?

record User(int Id, string Name, int Gender, string Tel);

 做个对象对比的Demo:


var userA = new User(1, “小米”, 1, “123456789”);
var userB = new User(1, “小米”, 1, “123456789”);

if (userA == userB)
{
    Console.WriteLine("这是一个用户.");
}

 总结一下:record类型让开发省去了重写相等比较的业务逻辑,同时简化了类型定义和初始化

 4. 可空注解的提升

  目前.NET library 类库,已经全面设置了是否可空注解。其实这个特性其实在C# 8.0已经引入:C#8.0 引入了“可为空引用类型”和“不可为空引用类型”,使你能够对引用类型变量的属性作出重要声明 :

  #nullable enable
  class A{ }

  即.NET 5.0的类库中已经全面更新了这个注解,方便开发时进行查看。

  同时,这次引入新的成员是否为空的注解:MemberNotNull 和 MemberNotNullWhen,例如以下的代码:

class UserManager
{
    User user = new User(1, "小米", 1, "123456789");

    [MemberNotNull(nameof(user))]
    public string GetUserName(string id)
    {
        return user.Name;
    }
}

  编译器会智能提示:CS8602警告

  

 四、工具类的新变化

  .NET 5.0 改进了Windows窗体设计器,更改了目标框架适用于.NET 5.0及更高版本的方式,更改了WinRT的支持方式,以及其他的一些改进。

  1. Windows窗体设计器:winform设计器

   Windows Forms设计器(用于.NET Core 3.1和.NET 5.0)已经在Visual Studio 16.8中进行了更新,现在支持所有Windows Forms控件。它还支持WinForms控件的Telerik UI。设计器包括您期望的所有设计器功能,包括:拖放,选择,移动和调整大小,剪切/复制/粘贴/删除控件,与属性窗口集成,事件生成等。数据绑定和对更广泛的第三方控件的支持即将推出。

   

   2. .NET 5.0目标框架

   新增一个Console类型工程后,选择目标框架是.NET 5.0, 其Project文件内容是这样的:

   

   新增一个Windows窗体应用工程后,选择目标框架是.NET 5.0, 其Project文件内容是这样的:

   

   Windows桌面API(包括Windows窗体,WPF和WinRT)仅在定位时可用net5.0-windows。同时也可以指定操作系统版本,例如net5.0-windows7或net5.0-windows10.0.17763.0(对于Windows October 2018 Update)。

   如果要使用WinRT API,则需要定位Windows 10版本。

   总结一下:

  • net5.0 是.NET 5.0的新目标框架绰号,Target Framework Moniker(TFM)。
  • net5.0结合并替换netcoreapp和netstandard TFM。
  • net5.0支持.NET Framework兼容模式
  • net5.0-windows 将用于公开Windows特定功能,包括Windows窗体,WPF和WinRT API。
  • .NET 6.0将使用相同的方法,并带有net6.0和将添加net6.0-ios和net6.0-android。
  • 特定于操作系统的TFM可以包含操作系统版本号,例如net6.0-ios14。
  • 可移植的API(如ASP.NET Core)可与一起使用net5.0。带有的Xamarin形式也是如此net6.0。

  3. WinRT Interop的重大改进

   在以Windows API为目标这一主题上,微软已经移至一个新模型,以作为.NET 5.0的一部分来支持WinRT API。这包括调用API(在任一方向上; CLR <==> WinRT),两个类型系统之间的数据封送处理以及打算在类型系统或ABI边界上统一对待的类型的统一(即“投影类型” ”,IEnumerable<T>并且IIterable<T>是示例)。

   从.NET 5.0开始,原有的WinRT互操作体系已被移除。这是一个巨大的变化。这意味着使用WinRT和.NET Core 3.x的应用程序和库需要重新开发对接,并且不能按原样在.NET 5.0上运行。

   使用WinRT API的库将需要多目标来管理.NET Core 3.1和.NET 5.0之间的这种差异。

   未来,.NET 将依靠Windows中的WinRT团队提供的新CsWinRT工具。它生成基于C#的WinRT互操作程序集,可以通过NuGet交付该程序集。Windows团队正是针对Windows中的WinRT API所做的。希望将WinRT(在Windows上)用作互操作系统的任何人都可以使用该工具,以将本机API公开给.NET或将.NET API公开给本机代码。

   关于CsWinRT工具,已经发布了1.0版本,具体可以参考链接://blogs.windows.com/windowsdeveloper/2020/11/10/announcing-c-winrt-version-1-0-with-the-net-5-ga-release/

   4. .NET Native Export/ .NET 本地导出

    即本机二进制文件启用导出功能。

   .NET 开发团队的Aaron Robinson一直在从事.NET Native Exports项目,该项目为将.NET组件作为本机库发布提供了更完整的体验。

   .NET Native导出项目能够实现:

  • 公开自定义的本地出口。
  • 不需要像COM这样的高级互操作技术。
  • 跨平台工作

   类似的实现技术,还有:

   5. 事件管道

   事件管道是在.NET Core 2.2中添加的新子系统和API,可以在任何操作系统上执行性能和其他诊断调查。

   在.NET 5.0中,事件管道已得到扩展,以使事件探查器能够写入事件管道事件。

   对于以前依靠ETW(在Windows上)监视应用程序行为和性能的分析探查器,来说是一个很好的方案和选择。

   这里不做详细展开了。

   6. 转储调试,Dump分析调试

   调试托管代码需要了解托管对象和构造。数据访问组件(DAC)是运行时执行引擎的子集,该引擎具有这些构造的知识,并且可以在没有运行时的情况下访问这些托管对象。

   现在,可以使用WinDBG或Windows在Windows上分析在Linux上收集的.NET Core进程转储dotnet dump analyze。

   本次发布还增加了对从macOS上运行的.NET进程捕获ELF转储的支持。由于ELF不是lldbmacOS上的本机可执行文件(像这样的本地调试器将无法与这些转储一起使用)文件格式,因此我们将其设为启用功能。

   要在macOS上支持转储收集,请设置环境变量COMPlus_DbgEnableElfDumpOnMacOS=1。可以使用来分析产生的转储dotnet dump analyze。

   7. 打印环境信息

   随着.NET扩展了对新操作系统和芯片体系结构的支持,有时需要一种打印环境信息的方法。.NET 5.0 创建了一个简单的.NET工具来执行此操作,称为dotnet-runtimeinfo。可以使用以下命令安装和运行该工具:

dotnet tool install -g dotnet-runtimeinfo
dotnet-runtimeinfo

    

   五、运行时和类库的提升

    1. RyuJIT的代码质量提升

    2. GC垃圾回收

  • Card mark stealing – dotnet/coreclr #25986  Server GC (on different threads) can now work-steal while marking gen0/1 objects held live by older generation objects. This means that ephemeral GC pauses are shorter for scenarios where some GC threads took much longer to mark than others.  ServerGC 中标记阶段的耗时更短了
  • Introducing Pinned Object Heap – dotnet/runtime #32283 — Adds the Pinned Object Heap (POH). This new heap (a peer to the Large Object Heap (LOH)) will allow the GC to manage pinned objects separately, and as a result avoid the negative effects of pinned objects on the generational heaps. 新增固定对象堆(POH)。此新堆(与大对象堆(LOH)对等)将允许GC单独管理固定对象,从而避免固定对象对堆的负面影响。
  • Allow allocating large object from free list while background sweeping SOH — Enabled LOH allocations using the free list while BGC is sweeping SOH. Previously this was only using end of segment space on LOH. This allowed for better heap usage.允许在后台扫描SOH时从空闲列表中分配大对象
  • Background GC suspension fixes – dotnet/coreclr #27729 — Suspension fixes to reduce time for both BGC and user threads to be suspended. This reduces the total time it takes to suspend managed threads before a GC can happen. dotnet/coreclr #27578 also contributes to the same outcome. 挂起修复程序可减少BGC和用户线程挂起的时间。这样可以减少发生GC之前挂起托管线程所需的总时间。
  • Fix named cgroup handling in docker — Added support to read limits from named cgroups. Previously we only read from the global one. 修复了docker中命名cgroup处理的问题—添加了对从命名cgroups读取限制的支持
  • Optimize vectorized sorting – dotnet/runtime #37159 — vectorized mark list sorting in GC which reduces the ephemeral GC pause time (also dotnet/runtime #40613). GC中的矢量化标记列表排序,减少了短暂的GC暂停时间
  • Generational aware analysis – dotnet/runtime #40322 — generational aware analysis that allows you to determine what old generation objects hold on to younger generation objects thus making them survive and contribute to ephemeral GC pause time.GC代感知分析,能够确定哪些旧世代对象保留在年轻代对象上,从而使它们得以生存并有助于短暂的GC暂停时间。
  • Optimize decommitting GC heap memory pages – dotnet/runtime #35896 — optimized decommit, much better decommit logic and for Server GC took decommit completely out of the “stop the world” phase which reduced blocking GC pause time.优化了取消授权,更好的取消授权逻辑,对于Server GC,完全取消了“停止一切”阶段的授权,从而减少了阻塞GC的暂停时间

     整体总结一下,Server GC延迟更低了,CPU消耗更少、性能更好了。

     3. Windows Arm64的支持

     .NET应用程序现在可以在Windows Arm64上本机运行。在.NET Core 3.0中添加的对Linux Arm64的支持(对glibc和musl的支持)。使用.NET 5.0,可以在Windows Arm64设备(例如Surface Pro X)上开发和运行应用程序。也可以通过x86仿真在Windows Arm64上运行.NET Core和.NET Framework应用程序。但是本机运行Arm64具有更好的性能。

     同时,.NET 5.0 SDK当前在Windows Arm64上不包含Windows桌面组件-Windows窗体和WPF。Windows Arm64上支持SDK,控制台和ASP.NET Core应用程序,但Windows桌面组件不支持。

     4. Arm64性能优化

     .NET 5.0 中主要针对Arm64平台做了以下优化:

  • 调整Arm64的JIT优化(示例
  • 启用并利用Arm64硬件内部函数(示例)。
  • 调整Arm64库中对性能至关重要的算法(示例)。

     更多详细信息,请参见在.NET 5.0中提高Arm64性能

      5. P95 +延迟改进

      Stack Overflow的一位工程师Nick Craver最近分享了他们升级.NET Core后,对延迟的改进:

      问题页面的展现时间中值从大约21毫秒(由于GC而有所增加)降至约15毫秒。

      第95个百分位数从〜40ms下降到〜30ms(相同测量)。第99位从〜60ms降至〜45ms。

      .NET项目组的解读是这样的:固定对象一直是GC性能的长期挑战,因为它们会加速(或导致)内存碎片。.NET 5.0为固定对象添加了新的GC堆。该固定对象堆是基于这样的假设(以空间换时间),他们的存在会导致不相称的性能挑战极少数固定的对象。将固定的对象(尤其是由.NET库作为实现细节创建的对象)移动到唯一的区域是有意义的,而垃圾回收代的GC堆几乎没有或没有固定的对象,因此具有更高的性能。

       6. 分层编译性能改进

       关于分层编译,大家可以参考这个连接://devblogs.microsoft.com/dotnet/tiered-compilation-preview-in-net-core-2-1

       在.NET 5.0中对分层编译进行了两项重大改进。下面这2段有点复杂,也比较晦涩

      分层编译的主要机制是调用计数。一旦某个方法被调用了n次,运行时就会要求JIT以更高的质量重新编译该方法。从最早的性能分析中,发现采用计数机制太慢,但是没有找到解决该问题的直接方法。.NET 5.0中改进了分层JIT编译所使用的调用计数机制,以平滑启动期间的性能。在过去的发行版中,已经发现在进程生命周期的前10到15秒钟内,性能会发生不可预测的变化(主要是针对Web服务器)。目前应该已经解决了。

     另一个性能挑战是对具有循环的方法使用分层编译。根本的问题是,您可以使用带有循环多次的循环的冷方法(仅调用一次或几次; $ lt; n)。我们称这种病理情况为“冷方法”。热循环”。可以想象Main应用程序的方法会发生这种情况。结果,默认情况下,我们禁用了带循环方法的分层编译。相反,使应用程序可以选择使用带循环的分层编译。在某些情况下看到了个位数的高性能改进后,PowerShell就是选择执行此操作的应用程序。

     为了更好地解决循环问题,.NET 实现了栈上替换(OSR)。这类似于Java虚拟机具有的同名功能。OSR允许在方法执行过程中重新编译当前正在运行的方法执行的代码,而这些方法是“堆栈上”活动的。该功能目前处于试验和选择启用状态,并且仅在x64上可用。

     要使用OSR,必须启用多个功能。目前.NET 5.0中没有启用OSR,这个功能尚未决定在生产环境中是否启用,所以这个技术点,了解即可。

      7. JSON序列化 System.Text.Json

      .NET 5.0 对System.Text.Json进行了显着改进,以提高性能和可靠性,同时API尽可能地和Newtonsoft.Json类似。它还包括对将JSON对象反序列化对record类型的支持。

      同时微软提供了System.Text.Json替换Newtonsoft.Json的迁移指南。该指南详细阐明了这两个API之间的关系。

      如何从 Newtonsoft.Json 迁移到 System.Text.Json

      JsonSerializer.NET 5.0中的性能显着提高。Stephen Toub在.NET 5中的性能改进中介绍了一些JsonSerializer改进

  六、应用程序部署

    应用程序开发完成后,根据实际的需要,可能会部署到Web服务器,云服务或客户端计算机,或者使用Azure DevOps或GitHub Actions之类的服务进行CI/CD。

    .NET 5.0专注于改善单个文件应用程序,减小docker多阶段构建的容器大小,并为使用.NET Core部署ClickOnce应用程序提供更好的支持。

    1. 容器

    与容器的交互协作非常重要。这个版本中添加了OpenTelemetry支持,可以从应用程序中捕获分布式跟踪和指标。dotnet-monitor是一个新工具,可以作为从.NET进程访问诊断信息的主要工具。特别是,我们已经开始构建dotnet-monitor的容器变体,您可以将其用作应用程序sidecar。同时,.NET项目组正在构建dotnet / tye,以提高微服务开发人员在开发和部署到Kubernetes环境中的效率。

   .NET运行时现在支持cgroup v2,这个API预计将在2020年以后成为与容器相关的重要API。Docker当前使用cgroup v1(.NET已支持)。相比之下,cgroup v2比cgroup v1更简单,更有效且更安全。.NET 5.0将在cgroup v2环境中正常工作。

   除了Nano Server,微软还将发布Windows Server Core映像,努力减小Windows Server Core映像的大小。

    更小的体积、更低的成本、更快的启动性能。.NET 5.0中将SDK映像重新建立在ASP.NET映像之上,而不是buildpack-deps,这样可以显着减小在多阶段构建方案中提取的聚合映像的大小。

 

   

    2. 单文件应用    

    单个文件应用程序作为单个文件发布和部署。该应用程序及其依赖项都包含在该文件中。当应用程序运行时,依赖项直接从该文件加载到内存中(不影响性能)。

    在.NET 5.0中,单个文件应用程序主要集中在Linux上。它们可以是框架相关的,也可以是独立的。依赖于全局安装的.NET运行时,依赖于框架的单个文件应用程序可能很小。自包含的单文件应用程序较大(由于带有运行时),但不需要作为安装前步骤就安装.NET运行时,因此可以正常工作。通常,依赖框架对开发和企业环境有利,而对于ISV,独立包含通常是更好的选择。

    .NET Core 3.1制作了一个单文件应用程序版本。它将二进制文件打包到一个文件中以进行部署,然后将这些文件解压缩到一个临时目录中以加载并执行它们。在某些情况下,这种方法会更好,但是希望为5.0构建的解决方案将是首选,并且会受到欢迎。  

    可以使用以下命令来生成单文件应用程序:

框架相关的单文件应用程序:
dotnet publish -r linux-x64 --self-contained false /p:PublishSingleFile=true
自包含的单文件应用程序:
dotnet publish -r linux-x64 --self-contained true /p:PublishSingleFile=true

  3. ClickOnce

    ClickOnce一直是流行的.NET部署选项,历史也比较悠久了。.NET Core 3.1和.NET 5.0 Windows应用程序现在支持它。   

    

     以上是.NET 5.0 发布后的技术梳理和整理,.NET 5.0作为.NET技术栈上近几年一个重量级的里程碑,是All in one,统一平台的第一个版本。现在有微软的背书,微软从.NET Preview 1就开始在自己的网站上运行.NET 5, (Bing.com、dot.net已升级并运行了数个月),同时早期的.NET Core版本可以直接升级到.NET 5. 所以大家可以放心使用的。我们也会逐渐将部分应用迁移升级到.NET 5.0. 以上是本次的一个分享。

    参考链接:

   //devblogs.microsoft.com/dotnet/announcing-net-5-0?WT.mc_id=DT-MVP-5003918

   //devblogs.microsoft.com/dotnet/performance-improvements-in-net-5/#json?WT.mc_id=DT-MVP-5003918

   //blogs.windows.com/windowsdeveloper/2020/11/10/announcing-c-winrt-version-1-0-with-the-net-5-ga-release?WT.mc_id=DT-MVP-5003918

 

周国庆

2020/11/15