.NET NativeAOT 指南

随着 .NET 8 的发布,一种新的“时尚”应用模型 NativeAOT 开始在各种真实世界的应用中广泛使用。

除了对 NativeAOT 工具链的基本使用外,“NativeAOT”一词还带有原生世界的所有限制,因此您必须知道如何处理这些问题才能正确使用它。

在这篇博客中,我将讨论它们。

基本用法

使用 NativeAOT 非常简单,只需要在发布应用时使用 MSBuild 传递一个属性 PublishAot=true 即可。

通常,它可以是:

dotnet publish -c Release -r win-x64 /p:PublishAot=true

其中 win-x64 是运行时标识符,可以替换为 linux-x64osx-arm64 或其他平台。您必须指定它,因为 NativeAOT 需要为您指定的运行时标识符生成原生代码。

然后发布的应用可以在 bin/Release/<target framework>/<runtime identifier>/publish 中找到

关于编译

在讨论使用 NativeAOT 时可能遇到的各种问题的解决方案之前,我们需要稍微深入一点,看看 NativeAOT 是如何编译代码的。

我们经常听说 NativeAOT 会剪裁掉没有被使用的代码。而实际上,它并不像 IL 剪裁那样从程序集中剪裁掉不必要的代码,而是只编译代码中引用的东西。

NativeAOT 编译包括两个阶段:

  1. 扫描 IL 代码,构建整个程序视图(一个依赖图),其中包含所有需要编译的必要依赖节点。
  2. 对依赖图中的每个方法进行实际的编译,生成代码。

请注意,在编译过程中可能会出现一些“延迟”的依赖,因此上述两个阶段可能会交错出现。

这意味着,在分析过程中没有被计算为依赖的任何东西最终都不会被编译。

反射

依赖图是在编译期间静态构建的,这也意味着任何无法静态分析的东西都不会被编译。不幸的是,反射,即在不事先告诉编译器的情况下在运行时获取东西,正是编译器无法弄清楚的一件事。

NativeAOT 编译器有一些能力可以根据编译时的字面量来推断出反射调用需要什么东西。

例如:

var type = Type.GetType("Foo");
Activator.CreateInstance(type); class Foo
{
public Foo() => Console.WriteLine("Foo instantiated");
}

上面的反射目标(即 Foo)可以被编译器弄清楚,因为编译器可以看到你试图获取类型 Foo,所以类型 Foo 会被标记为一个依赖,这导致 Foo 被编译到最终的产物中。

如果你运行这个程序,它会如预期地打印 Foo instantiated

但是如果我们将代码改为如下:

var type = Type.GetType(Console.ReadLine());
Activator.CreateInstance(type); class Foo
{
public Foo() => Console.WriteLine("Foo instantiated");
}

现在让我们用 NativeAOT 构建并运行这个程序,然后输入 Foo 来创建一个 Foo 的实例。你会立刻得到一个异常:

Unhandled Exception: System.ArgumentNullException: Value cannot be null. (Parameter 'type')
at System.ArgumentNullException.Throw(String) + 0x2b
at System.ActivatorImplementation.CreateInstance(Type, Boolean) + 0xe7
...

这是因为编译器无法看到你在哪里使用了 Foo,所以它根本不会为 Foo 生成任何代码,导致这里的 typenull

此外,依赖分析是精确到单个方法的,这意味着即使一个类型被认为是一个依赖,如果该类型中的某个方法没有被使用,该方法也不会被包含在代码生成中。

虽然这可以通过将所有类型和方法添加到依赖图中来解决,这样编译器就会为它们生成代码。这就是 TrimmerRootAssembly 的作用:通过提供 TrimmerRootAssembly,NativeAOT 编译器会将你指定的程序集中的所有东西都作为根。

但是涉及泛型的情况就不是这样了。

动态泛型实例化

在 .NET 中,我们有泛型,编译器会为每个非共享的泛型类型和方法生成不同的代码。

假设我们有一个类型 Point<T>

struct Point<T>
{
public T X, Y;
}

如果我们有一段代码试图使用 Point<int>,编译器会为 Point<int> 生成专门的代码,使得 Point.XPoint.Y 都是 int。如果我们有一个 Point<float>,编译器会生成另一个专门的代码,使得 Point.XPoint.Y 都是 float

通常情况下,这不会导致任何问题,因为编译器可以静态地找出你在代码中使用的所有实例化,直到你试图使用反射来构造一个泛型类型或一个泛型方法:

var type = Type.GetType(Console.ReadLine());
var pointType = typeof(Point<>).MakeGenericType(type);

上面的代码在 NativeAOT 下不会工作,因为编译器无法推断出 Point<T> 的实例化,所以编译器既不会生成 Point<int> 的代码,也不会生成 Point<float> 的代码。

尽管编译器可以为 intfloat,甚至泛型类型定义 Point<> 生成代码,但是如果编译器没有生成 Point<int> 的实例化代码,你就无法使用 Point<int>

即使你使用 TrimmerRootAssembly 来告诉编译器将你的程序集中的所有东西都作为根,也仍然不会为像 Point<int>Point<float> 这样的实例化生成代码,因为它们需要根据类型参数来单独构造。

解决方案

既然我们已经找出了在 NativeAOT 下可能发生的潜在问题,让我们来谈谈解决方案。

在其他地方使用它

最简单的想法是,我们可以通过在代码中使用它来让编译器知道我们需要什么。

例如,对于代码

var type = Type.GetType(Console.ReadLine());
var pointType = typeof(Point<>).MakeGenericType(type);

只要我们知道我们要使用 Point<int>Point<float>,我们可以在其他地方使用它一次,然后编译器就会为它们生成代码:

// 我们使用一个永远为假的条件来确保代码不会被执行
// 因为我们只想让编译器知道依赖关系
// 注意,如果我们在这里简单地使用一个 `if (false)`
// 这个分支会被编译器完全移除,因为它是多余的
// 所以,让我们在这里使用一个不平凡但不可能的条件
if (DateTime.Now.Year < 0)
{
var list = new List<Type>();
list.Add(typeof(Point<int>));
list.Add(typeof(Point<float>));
}

DynamicDependency

我们有一个属性 DynamicDependencyAttribute 来告诉编译器一个方法依赖于另一个类型或方法。

所以我们可以利用它来告诉编译器:“如果 A 被包含在依赖图中,那么也添加 B”。

下面是一个例子:

class Foo
{
readonly Type t = typeof(Bar); [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Bar))]
public void A()
{
foreach (var prop in t.GetProperties())
{
Console.WriteLine(prop);
}
}
} class Bar
{
public int X { get; set; }
public int Y { get; set; }
}

现在只要编译器发现有任何代码路径调用了 Foo.ABar 中的所有公共属性都会被添加到依赖图中,这样我们就能够对 Bar 的每个公共属性进行动态反射调用。

这个属性还有许多重载,可以接受不同的参数来适应不同的用例,您可以在这里查看文档。

此外,现在我们知道 Foo.A 中的动态反射在剪裁和 NativeAOT 下不会造成任何问题,我们可以使用 UnconditionalSuppressMessage 来抑制警告信息,这样在构建过程中就不会再产生任何警告了。

class Foo
{
readonly Type t = typeof(Bar); [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Bar))]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2080",
Justification = "The properties of Bar have been preserved by DynamicDependency.")]
public void A()
{
foreach (var prop in t.GetProperties())
{
Console.WriteLine(prop);
}
}
}

DynamicallyAccessedMembers

有时我们试图动态地访问类型 T 的成员,其中 T 可以是一个类型参数或一个 Type 的实例:

void Foo<T>()
{
foreach (var prop in typeof(T).GetProperties())
{
Console.WriteLine(prop);
}
} class Bar
{
public int X { get; set; }
public int Y { get; set; }
}

如果我们调用 Foo<Bar>,很不幸,这在 NativeAOT 下不会工作。编译器确实看到你是用类型参数 Bar 调用 Foo 的,但在 Foo<T> 的上下文中,编译器不知道 T 是什么,而且没有其他代码直接使用 Bar 的属性,所以编译器不会为 Bar 的属性生成代码。

这里我们可以使用 DynamicallyAccessedMembers 来告诉编译器为 T 的所有公共属性生成代码:

void Foo<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>()
{
// ...
}

现在当编译器编译调用 Foo<Bar> 时,它知道 T(特别是这里的 Bar)的所有公共属性都应该被视为依赖。

这个属性也可以应用在一个 Type 上:

void Foo([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type t)
{
foreach (var prop in t.GetProperties())
{
Console.WriteLine(prop);
}
}

甚至在一个 string 上:

Foo("Bar");

void Foo([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] string s)
{
foreach (var prop in Type.GetType(s).GetProperties())
{
Console.WriteLine(prop);
}
}

所以在这里你可能会发现我们有一个替代方案,用于我们在 DynamicDependency 一节中提到的代码示例:

class Foo
{
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
readonly Type t = typeof(Bar); public void A()
{
foreach (var prop in t.GetProperties())
{
Console.WriteLine(prop);
}
}
}

顺便说一句,这也是推荐的方法。

TrimmerRootAssembly

如果你不拥有代码,但你仍然希望代码在 NativeAOT 下工作。你可以尝试使用 TrimmerRootAssembly 来告诉编译器将一个程序集中的所有类型和方法都作为依赖。但请注意,这种方法不适用于泛型实例化。

<ItemGroup>
<TrimmerRootAssembly Include="MyAssembly" />
</ItemGroup>

TrimmerRootDescriptor

对于高级用户,他们可能想要控制从一个程序集中包含什么。在这种情况下,可以指定一个 TrimmerRootDescriptor

<ItemGroup>
<TrimmerRootDescriptor Include="link.xml" />
</ItemGroup>

TrimmerRootDescriptor 文件的文档和格式可以在这里找到。

Runtime Directives

对于泛型实例化的情况,它们无法通过 TrimmerRootAssembly 或 TrimmerRootDescriptor 来解决,这里需要一个包含 runtime directives 的文件来告诉编译器需要编译的东西。

<ItemGroup>
<RdXmlFile Include="rd.xml" />
</ItemGroup>

rd.xml 中,你可以为你的泛型类型和方法指定实例化。

rd.xml 令文件的文档和格式可以在这里找到。

这种方法不推荐,但它可以解决你在使用 NativeAOT 时遇到的一些难题。请在使用 trimmer descriptor 或 runtime directives 之前,总是考虑用 DynamicallyAccessedMembersDynamicDependency 来注释你的代码,使其与剪裁/AOT 兼容。

结语

NativeAOT 是 .NET 中一个非常棒和强大的工具。有了 NativeAOT,你可以以可预测的性能构建你的应用,同时节省资源(更低的内存占用和更小的二进制大小)。

它还将 .NET 带到了不允许 JIT 编译器的平台,例如 iOS 和主机平台。此外,它还使 .NET 能够运行在嵌入式设备甚至裸机设备上(例如在 UEFI 上运行)。

在使用工具之前了解工具,这样你会节省很多时间。

.NET NativeAOT 指南的更多相关文章

  1. JavaScript权威指南 - 函数

    函数本身就是一段JavaScript代码,定义一次但可能被调用任意次.如果函数挂载在一个对象上,作为对象的一个属性,通常这种函数被称作对象的方法.用于初始化一个新创建的对象的函数被称作构造函数. 相对 ...

  2. UE4新手之编程指南

    虚幻引擎4为程序员提供了两套工具集,可共同使用来加速开发的工作流程. 新的游戏类.Slate和Canvas用户接口元素以及编辑器功能可以使用C++语言来编写,并且在使用Visual Studio 或 ...

  3. JavaScript权威指南 - 对象

    JavaScript对象可以看作是属性的无序集合,每个属性就是一个键值对,可增可删. JavaScript中的所有事物都是对象:字符串.数字.数组.日期,等等. JavaScript对象除了可以保持自 ...

  4. JavaScript权威指南 - 数组

    JavaScript数组是一种特殊类型的对象. JavaScript数组元素可以为任意类型,最大容纳232-1个元素. JavaScript数组是动态的,有新元素添加时,自动更新length属性. J ...

  5. const extern static 终极指南

    const extern static 终极指南 不管是从事哪种语言的开发工作,const extern static 这三个关键字的用法和原理都是我们必须明白的.本文将对此做出非常详细的讲解. co ...

  6. Atitit.研发管理软件公司的软资产列表指南

    Atitit.研发管理软件公司的软资产列表指南 1. Isv模型下的软资产1 2. 实现层面implet1 3. 规范spec层1 4. 法则定律等val层的总结2 1. Isv模型下的软资产 Sof ...

  7. HA 高可用软件系统保养指南

    又过了一年 618,六月是公司一年一度的大促月,一般提前一个月各系统就会减少需求和功能的开发,转而更多去关注系统可用性.稳定性和管控性等方面的非功能需求.大促前的准备工作一般叫作「备战」,可以把线上运 ...

  8. 第六代智能英特尔® 酷睿™ 处理器图形 API 开发人员指南

    欢迎查看第六代智能英特尔® 酷睿™ 处理器图形 API 开发人员指南,该处理器可为开发人员和最终用户提供领先的 CPU 和图形性能增强.各种新特性和功能以及显著提高的性能. 本指南旨在帮助软件开发人员 ...

  9. Visual Studio Code 配置指南

    Visual Studio Code (简称 VS Code)是由微软研发的一款免费.开源的跨平台文本(代码)编辑器.在我看来它是「一款完美的编辑器」. 本文是有关 VS Code 的特性介绍与配置指 ...

  10. Web API 入门指南 - 闲话安全

    Web API入门指南有些朋友回复问了些安全方面的问题,安全方面可以写的东西实在太多了,这里尽量围绕着Web API的安全性来展开,介绍一些安全的基本概念,常见安全隐患.相关的防御技巧以及Web AP ...

随机推荐

  1. Codeforces Round #623 (Div. 2) A~D题,D题multiset使用

    比赛链接:Here 1315A. Dead Pixel 签到题, 比较四个值 max(max(x, a - 1 - x) * b, a * max(y, b - 1 - y)) 1315B. Home ...

  2. Redis 缓存性能实践及总结

    一.前言 在互联网应用中,缓存成为高并发架构的关键组件.这篇博客主要介绍缓存使用的典型场景.实操案例分析.Redis使用规范及常规 Redis 监控. 二.常见缓存对比 常见的缓存方案,有本地缓存,包 ...

  3. bitcask论文翻译/笔记

    翻译 论文来源:bitcask-intro.pdf (riak.com) 背景介绍 Bitcask的起源与Riak分布式数据库的历史紧密相连.在Riak的K/V集群中,每个节点都使用了可插拔的本地存储 ...

  4. Guava缓存工具类封装和使用

    本文为博主原创,未经允许不得转载: Guava是谷歌提供的一款强大的java工具库,里面包含了很多方便且高效的工具,在项目开发中有业务场景需要保存数据到内存当中, 且只需要保存固定时间就可以,该数据只 ...

  5. C语言中的操作符:了解与实践

    ​ 欢迎大家来到贝蒂大讲堂 ​ 养成好习惯,先赞后看哦~ ​ 所属专栏:C语言学习 ​ 贝蒂的主页:Betty's blog 1. 操作符的分类 操作符又叫运算符,它在C语言中起着非常大的作用,以下是 ...

  6. 关于spring-boot-starter-parent 3.1.2和3.1.5版本的区别导致的错误

    1.问题 在学习黑马程序员SpringBoot3+Vue3全套视频教程时,手动配置springboot项目时,由于之前spring-boot-starter-parent安装的版本是3.1.5,视频要 ...

  7. Linux复习笔记

    Linux复习笔记 常识说明 目录结构 Linux以树型结构管理文件,其最上层文件夹为 / ,也就是根目录. 如图所示,图中展示了一部分文件夹的结构: 所有的文件夹都属于根目录的子文件夹. 安装好系统 ...

  8. 使用阿里canal实现mysql与Elasticsearch增量同步

    一.背景介绍 最近在做一个地理信息相关的项目,需要维护大量的地址描述数据,同时需要提供对数据检索的功能,准备采用Elasticsearch(6.7)实现.那么问题就来了,地址数据需要同时在MySQL和 ...

  9. [转帖]TiKV & TiFlash 加速复杂业务查询

    https://tidb.net/book/tidb-monthly/2022/2022-07/usercase/tikv-tiflash 背景​ 在互联网公司或传统公司的 CRM 系统中,最常用的功 ...

  10. [转帖]TiDB 热点问题处理

    TiDB 热点问题处理 本文介绍如何定位和解决读写热点问题. TiDB 作为分布式数据库,内建负载均衡机制,尽可能将业务负载均匀地分布到不同计算或存储节点上,更好地利用上整体系统资源.然而,机制不是万 ...