乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 即时编译JIT、超前编译AOT、本机镜像生成器NGen、Native AOT

什么是即时编译JIT

在计算机领域,即时编译(Just In Time, JIT)(也是动态编译或运行时编译)是一种执行计算机代码的方式,它涉及在程序执行期间(运行时)而不是在执行之前进行编译。这可能包括源代码翻译,但更常见的是字节码翻译成机器码,然后直接执行。实现JIT编译器的系统通常会持续分析正在执行的代码,并确定代码的哪些部分从编译或重新编译中获得的速度会超过编译该代码的开销。

image

JIT编译是两种传统的机器码翻译方法的结合,即时间先期编译(AOT)和解释,并结合了两者的一些优点和缺点。粗略地说,JIT编译结合了编译代码的速度和解释的灵活性,以及解释器的开销和编译和连接(不仅仅是解释)的额外开销。JIT编译是动态编译的一种形式,并允许自适应优化,如动态重新编译和针对微架构的速度提升。解释和JIT编译特别适用于动态编程语言,因为运行时系统可以处理迟来的数据类型并执行安全保证。

设计原理

在字节码编译的系统中,源代码被翻译成一种被称为字节码的中间表示。字节码不是任何特定计算机的机器代码,并且可以在计算机架构之间移植。然后,字节码可以被虚拟机解释,或在虚拟机上运行。JIT编译器以许多部分(或全部,很少)读取字节码,并将其动态编译为机器代码,以便程序可以更快地运行。这可以在每个文件、每个函数甚至任何任意的代码片段上进行;代码可以在即将执行时被编译(因此被称为 "即时"),然后缓存并在以后重新使用而不需要重新编译

相比之下,传统的解释型虚拟机将简单地解释字节码,通常性能低得多。有些解释器甚至可以解释源代码,而不需要先编译成字节码,其性能甚至更差。静态编译的代码或本地代码在部署之前就已经被编译了。动态编译环境是指在执行过程中可以使用编译器。使用JIT技术的一个共同目标是达到或超过静态编译的性能,同时保持字节码解释的优势。在部署之前,解析原始源代码和执行基本优化的大部分"繁重工作"通常在编译时处理:从字节码到机器码的编译比从源代码编译快得多。部署的字节码是可移植的,与本地代码不同。由于运行时对编译有控制权,就像解释的字节码一样,它可以在安全的沙盒中运行。从字节码到机器码的编译器更容易编写,因为可移植的字节码编译器已经完成了很多工作。

JIT代码通常提供比解释器好得多的性能。此外,在某些情况下,它可以提供比静态编译更好的性能,因为许多优化只有在运行时才是可行的

  • 编译可以根据目标CPU和应用程序运行的操作系统模型进行优化。例如,JIT在检测到CPU支持SSE2矢量CPU指令时可以选择这些指令。为了用静态编译器获得这种水平的优化特性,必须为每个目标平台/架构编译一个二进制文件,或者在一个二进制文件中包括代码的部分的多个版本。

  • 该系统能够收集关于程序在其所处环境中实际运行情况的统计数据,它可以重新安排和重新编译以获得最佳性能。然而,一些静态编译器也可以把配置文件信息作为输入。

  • 系统可以进行全局代码优化(如库函数的内联),而不会失去动态链接的优势,也不会出现静态编译器和链接器固有的开销。具体来说,在做全局内联替换时,静态编译过程可能需要运行时检查,并确保如果对象的实际类覆盖了内联方法,就会发生虚拟调用,而且在循环中可能需要处理数组访问的边界条件检查。在许多情况下,通过即时编译,这种处理可以从循环中移出,通常可以大大增加速度

  • 尽管静态编译的垃圾回收语言也可以做到这一点,但字节码系统可以更容易地重新安排已执行的代码,以便更好地利用缓存

由于JIT必须在运行时渲染和执行本地二进制映像,真正的机器码JIT必须有允许在运行时执行数据的平台,因此在基于哈佛架构的机器上使用这种JIT是不可能的;对于某些操作系统和虚拟机也是如此。然而,一种特殊类型的"JIT"有可能不针对物理机的CPU架构,而是针对优化的虚拟机字节码,其中对原始机器代码的限制占了上风,特别是当该字节码的虚拟机最终利用JIT来处理本地代码时。

性能表现

由于加载和编译字节码所需的时间,JIT在应用程序的初始执行中会造成轻微到明显的延迟。有时这种延迟被称为"启动时间延迟"或"预热时间"。一般来说,JIT执行的优化越多,它生成的代码就越好,但初始延迟也会增加。因此,JIT编译器必须在编译时间和它希望生成的代码的质量之间做出权衡。启动时间除了JIT编译之外,还可能包括增加的IO绑定操作:例如,Java虚拟机(JVM)的rt.jar类数据文件是40MB,JVM必须在这个上下文巨大的文件中寻找大量的数据。

Sun公司的HotSpot Java虚拟机所使用的一种可能的优化是把解释和JIT编译结合起来。应用程序代码最初是被解释的,但JVM监控哪些字节码序列经常被执行,并将它们翻译成机器代码,以便在硬件上直接执行。对于只执行几次的字节码,这可以节省编译时间并减少初始延迟;对于经常执行的字节码,JIT编译被用来在缓慢解释的初始阶段后高速运行。此外,由于一个程序大部分时间都在执行其少数的代码,因此减少的编译时间是很重要的。最后,在最初的代码解释阶段,可以在编译前收集执行统计数据,这有助于进行更好的优化。

正确的权衡会因情况不同而不同。例如,Sun公司的Java虚拟机有两种主要模式--客户端和服务器。在客户端模式下,会进行最小的编译和优化,以减少启动时间在服务器模式下,会进行大量的编译和优化,通过牺牲启动时间,使应用程序运行后的性能最大化。其他的Java即时编译器使用方法执行次数的运行时间测量,结合方法的字节码大小作为启发式的决定何时编译。还有一个使用执行次数与循环的检测相结合。一般来说,在短时运行的应用程序中准确预测哪些方法需要优化比在长时运行的应用程序中更难。

微软的Native Image Generator(Ngen)是另一种减少初始延迟的方法。Ngen将通用中间语言(Common Intermediate Language, CIL) 镜像中的字节码预编译(或"预JIT")为机器本地代码。因此,不需要在运行时进行编译。与Visual Studio 2005一起携带的.NET框架2.0在安装后立即对所有的微软库DLLs运行Ngen。Pre-jitting提供了一种改善启动时间的方法。然而,它产生的代码质量可能不如JIT的代码,原因与静态编译的代码在没有剖析指导下的优化,在极端情况下不能像JIT编译的代码那样好的原因一样:缺乏剖析数据来驱动,例如,内联缓存。

也有一些Java实现将AOT(超前)编译器与JIT编译器(Excelsior JET)或解释器(GNU Compiler for Java)相结合

什么是超前编译AOT

在计算机科学中,超前编译(Ahead-Of-Time, AOT编译)是指在程序执行前,通常在构建时,将一种(通常是)高级编程语言编译成一种(通常是)低级语言,以减少运行时需要执行的工作量的行为。

image

最常见的是,它与编译高级编程语言(如C或C++)或中间表示法(如Java字节码或.NET框架通用中间语言(CIL)代码)到本地(依赖系统的)机器码的行为有关,这样产生的二进制文件就能像标准的本地编译器一样,在本地执行。在这个特定的背景下使用时,它通常被视为及时编译(JIT)的反面

更广泛地说,AOT编译的目标语言不一定是特定的本地机器码,而是相当随意地定义。一些学术论文用这个词指的是将Java字节码编译成C语言的行为,或者指执行优化管道的时间。一个学术项目用这个词指的是将JavaScript预编译为V8(JavaScript引擎)依赖机器的优化IR和JavaScript Core依赖机器的字节码的行为一些工业语言的实现(例如Closure和Hermes JavaScript引擎)使用这个词是指将源语言预编译为机器特定的字节码的行为Angular(网络框架)使用这个词是指将其HTML模板和TypeScript转换为JavaScript

事实上,由于所有的静态编译在技术上都是提前进行的,这种特殊的措辞常常被用来强调这种预编译行为的某种性能优势。因此,将Java编译成Java字节码的行为很少被称为AOT,因为它通常是一种需求,而不是一种优化。

减少运行时间的开销

一些具有托管代码运行时间的编程语言可以被编译为中间表示,使用即时编译(JIT)。简而言之,这是在中间代码执行时将中间代码编译成机器代码,用于本地运行,这可能会降低应用程序的性能。超前编译通过在执行前而不是在执行中进行,消除了对这一步骤的需要

动态类型语言的超前编译到本地机器码或其他静态虚拟机字节码仅在有限的情况下是可能的。例如,高性能埃朗项目(HiPE)的埃朗语言的AOT编译器可以做到这一点,因为有先进的静态类型重建技术和类型猜测。

在大多数情况下,在完全AOT编译的程序和库中,有可能放弃运行时环境的有用部分,从而节省磁盘空间、内存、电池寿命和启动时间(没有JIT预热阶段)等。正因为如此,它在嵌入式或移动设备中很有用。

性能权衡

AOT编译器可以进行复杂和高级的代码优化,在大多数JIT的情况下,这将被认为是成本太高。相比之下,AOT通常不能执行JIT中可能的一些优化,如运行时配置文件指导下的优化(PGO)、伪常数传播或间接虚拟函数内联AOT必须按照目标架构进行编译,而JIT可以将代码编译成能够最好地利用它所运行的实际CPU,甚至在软件发布多年之后。

此外,JIT编译器可以通过对代码的假设,推测性地优化热代码。如果一个推测性的假设后来被证明是错误的,那么生成的代码可以被取消优化。这种操作会降低运行中的软件的性能,直到代码通过自适应优化再次被优化。AOT编译器不能做这样的假设,需要在编译时推断出尽可能多的信息。它需要借助于不太专业的代码,因为它不能知道哪些类型会通过一个方法。这样的问题可以通过配置文件引导的优化来缓解。但即使在这种情况下,生成的代码也不能像JIT编译器那样动态地适应不断变化的运行时配置文件。

什么是本机镜像生成器NGen

本机镜像生成器(Native Image Generator, NGen)是.NET框架的提前编译(AOT)服务。它允许对CLI程序集进行预编译,而不是让通用语言运行时(CLR)在运行时进行及时编译(JIT)。在某些情况下,执行速度将明显快于JIT。

本机镜像生成器为当前环境(即操作系统)生成本机二进制映像这以牺牲可移植性和磁盘空间为代价消除了JIT开销;每当NGen生成的镜像在不兼容的环境中运行时,.NET Framework都会自动恢复为使用JIT。对程序集运行NGen后,生成的本机映像将放置到本机映像缓存(NIC)中,以供所有其他CLI程序集使用。例如,这使得在安装时使用NGen处理CLI程序集成为可能,从而在以后最终用户在其系统上调用应用程序时节省处理器时间。

NGen旨在通过在运行时删除JIT编译过程来使程序集执行得更快,但这并不总能提高性能,因为某些优化只能由JIT编译器完成(例如,如果JIT编译器知道代码已经在运行)在完全信任的情况下,它可以跳过某些昂贵的安全检查)。由于这个事实,只有在对应用程序性能进行前后基准测试之后才使用NGen是有意义的

Ngen.exe为仅面向.NET Framework的程序集编译本机映像。.NET Core的等效本机映像生成器是CrossGen

使用本机镜像生成器NGen

它是随Visual Studio一起安装的,通过管理员权限打开Developer Command Prompt for VS 2022,即运行%comspec% /k "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat"

image

然后运行命令:

ngen install xxxx.exe

image

显示本机已生成镜像

ngen display

image

针对.Net Core的CrossGen

https://github.com/dotnet/runtime/blob/main/docs/workflow/building/coreclr/crossgen.md

当你使用C#编译器创建一个.NET程序集时,你的程序集只包含MSIL代码。当应用程序运行时,JIT编译器将MSIL代码翻译成本地代码,然后CPU才能执行它们。这种执行模式有一些优势。例如,你的程序集代码可以在所有支持.NET的平台和架构上移植。然而,这种可移植性是有性能代价的。你的应用程序启动得更慢,因为JIT编译器必须花费时间来翻译代码

为了帮助你的应用程序启动得更快,CoreCLR包括一个叫做CrossGen的工具,它可以预先将MSIL代码编译成本地代码。

如果你自己构建CoreCLR,CrossGen工具(Windows上的crossgen.exe,或其他平台上的crossgen)会作为构建的一部分被创建,并与其他CoreCLR二进制文件存储在同一个输出目录中。如果你使用NuGet包安装CoreCLR,你可以在NuGet包的工具文件夹中找到CrossGen。

无论你如何获得CrossGen,非常重要的一点是,它必须与其他CoreCLR二进制文件相匹配。

  • 如果你自己构建了CrossGen,你应该将它与运行时和由同一构建产生的System.Private.CoreLib.dll一起使用。不要试图将一个版本的CrossGen与另一个版本生成的二进制文件混合使用。
  • 如果你从NuGet安装CrossGen,请确保你使用的CrossGen与CoreCLR其他二进制文件的NuGet包完全相同。不要试图混合来自多个NuGet包的二进制文件。

如果你不遵守上述规则,你很可能在运行CrossGen时遇到错误。

快速JIT

对于不包含循环且不可使用预编译代码的方法,快速JIT可以更快完成编译,但不会进行优化。

启用快速JIT会缩短启动时间,但可能会生成性能下降的代码。例如,代码可能会使用更多堆栈空间、分配更多内存并以更慢的速度运行。

如何开启快速JIT

方式一runtimeconfig.json

{
   "runtimeOptions": {
      "configProperties": {
         "System.Runtime.TieredCompilation.QuickJit": true
      }
   }
}

方式二*..csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TieredCompilationQuickJit>true</TieredCompilationQuickJit>
  </PropertyGroup>

</Project>

方式三,环境变量

COMPlus_TC_QuickJit=1
# 或者
DOTNET_TC_QuickJit=1

默认情况

  • 在NET Core 2.1和2.2中,默认禁用快速JIT
  • 在NET Core 3.0及更高版本中,默认启用快速JIT

包含循环的用法

改用System.Runtime.TieredCompilation.QuickJitForLoops

分层编译(TieredCompilation)

分层编译(TieredCompilation)会会将JIT实时编译器的方法拆成两个层级:

  • 第一层级,可以更快速地生成代码(快速JIT)或者加载预编译的代码(ReadyToRun)。
  • 第二层级,在后台生成优化的代码(优化JIT)。

如何开启分层编译

方式一runtimeconfig.json

{
   "runtimeOptions": {
      "configProperties": {
         "System.Runtime.TieredCompilation": true
      }
   }
}

方式二*.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TieredCompilation>true</TieredCompilation>
  </PropertyGroup>

</Project>

方式三,环境变量

COMPlus_TieredCompilation=1
# 或者
DOTNET_TieredCompilation=1

Native AOT部署

将应用发布为本机AOT会生成一个独立应用,并且该应用已预先(AOT)编译为本机代码。本机AOT应用快速启动,使用更少的内存。应用程序的用户可以在未安装.NET运行时的计算机上运行该应用程序。

本机AOT的优势对于具有大量已部署实例(例如云基础结构和超大规模服务)的工作负载而言最为重要。目前不支持ASP.NET Core,而仅支持控制台应用。

本机AOT部署模型使用提前编译器在发布时将IL编译为本机代码。本机AOT应用在应用程序运行时不使用实时(JIT)编译器。本机AOT应用可以在不允许JIT的受限环境中运行。本机AOT应用程序面向特定的运行时环境,例如Linux x64或Windows x64,就像发布独立应用那样

.NET 本机AOT部署模型中存在一些限制,主要限制是无法生成运行时代码。.NET 7版本中的支持面向控制台类型应用程序。

.NET7支持本机AOT,但只有有限数量的库与.NET7中的本机AOT完全兼容。

.NET Runtime - Native AOT

在.Net 6之前,.Net有个叫CoreRT项目,使用该工具可以将.NET 程序集编译到Native Library,后面变迁改名为NativeAOT并转移到runtimelab

这个分支包含了CoreCLR .NET运行时的实验性分叉,为本地AOT形式因素进行了优化。超前(AOT)工具链可以将.NET应用程序编译成本地(特定架构)单文件可执行文件。它还可以产生独立的动态或静态库,可以被用其他编程语言编写的应用程序消耗。

前提条件

在Windows上安装Visual Studio 2022,包括使用C++工作负载进行桌面开发。

在Linux上,为.NET运行时所依赖的库安装clang和开发人员包。

sudo apt-get install clang zlib1g-dev

添加项目配置

在SDK样式风格的.Net Standard、.Net Core、.Net项目中,找到*.csproj项目文件。

<PropertyGroup>
    <PublishAot>true</PublishAot>
</PropertyGroup>

针对特定平台打包

dotnet publish -r <RID>

例如:

dotnet publish -r win-x64 -c Release

image

dotnet publish -r linux-arm64 -c Release

image

在Linux计算机上生成的本机AOT二进制文件仅适用于同一版本或更高版本的Linux。例如,在Ubuntu 20.04上生成的本机AOT二进制文件将在Ubuntu 20.04及更高版本上运行,但它不会在Ubuntu 18.04上运行。

应用将在发布目录中可用,并包含在其中所需运行的所有代码,包括coreclr运行时的精简版。

本机调试信息

本机AOT发布遵循本机工具链的平台约定Windows上本机工具链的默认行为是在单独的.pdb文件中生成调试信息。Linux上本机工具链的默认行为是在本机二进制文件中包含调试信息,使得本机二进制文件明显更大。

StripSymbols属性设置为true以在单独的.dbg文件中生成调试信息,并将调试信息从Linux上的本机二进制文件中移除。此属性对Windows没有影响。

<PropertyGroup>
    <StripSymbols>true</StripSymbols>
</PropertyGroup>

Native AOT部署限制

本机AOT应用程序附带一些基本限制和兼容性问题。关键限制包括:

  • 没有动态加载(例如Assembly.LoadFile)
  • 没有运行时代码生成(例如System.Reflection.Emit)
  • 没有C++/CLI
  • 没有内置的COM(仅适用于Windows)
  • 需要剪裁,其中存在限制
  • 表示编译为单个文件,该文件具有已知的不兼容性
  • 应用包括所需的运行时库(和独立应用一样,与依赖框架的应用相比,它们的大小会增加)

发布过程将分析整个项目及其依赖项,并在运行时发布的应用程序可能触及限制时发出警告。

.NET 7中本机AOT的第一个版本存在额外限制。其中包括:

  • 应面向控制台类型应用程序(而不是ASP.NET Core)。
  • 并非所有运行时库都完全注释为本机AOT兼容(即运行时库中的一些警告无法由最终开发人员操作)。
  • 有限诊断支持(调试和分析)。

面向.NET 7时支持的编译目标

平台 支持的体系结构
Windows x64、Arm64
Linux x64、Arm64

参考

posted @ 2022-08-26 20:17  TaylorShi  阅读(1236)  评论(0编辑  收藏  举报