.NET NativeAOT 指南
.NET NativeAOT 指南
随着 .NET 8 的发布,一种新的“时尚”应用模型 NativeAOT 开始在各种真实世界的应用中广泛使用。
除了对 NativeAOT 工具链的基本使用外,“NativeAOT”一词还带有原生世界的所有限制,因此您必须知道如何处理这些问题才能正确使用它。
在这篇博客中,我将讨论它们。
基本用法
使用 NativeAOT 非常简单,只需要在发布应用时使用 MSBuild 传递一个属性 PublishAot=true 即可。
通常,它可以是:
dotnet publish -c Release -r win-x64 /p:PublishAot=true
其中 win-x64 是运行时标识符,可以替换为 linux-x64,osx-arm64 或其他平台。您必须指定它,因为 NativeAOT 需要为您指定的运行时标识符生成原生代码。
然后发布的应用可以在 bin/Release/<target framework>/<runtime identifier>/publish 中找到
关于编译
在讨论使用 NativeAOT 时可能遇到的各种问题的解决方案之前,我们需要稍微深入一点,看看 NativeAOT 是如何编译代码的。
我们经常听说 NativeAOT 会剪裁掉没有被使用的代码。而实际上,它并不像 IL 剪裁那样从程序集中剪裁掉不必要的代码,而是只编译代码中引用的东西。
NativeAOT 编译包括两个阶段:
- 扫描 IL 代码,构建整个程序视图(一个依赖图),其中包含所有需要编译的必要依赖节点。
- 对依赖图中的每个方法进行实际的编译,生成代码。
请注意,在编译过程中可能会出现一些“延迟”的依赖,因此上述两个阶段可能会交错出现。
这意味着,在分析过程中没有被计算为依赖的任何东西最终都不会被编译。
反射
依赖图是在编译期间静态构建的,这也意味着任何无法静态分析的东西都不会被编译。不幸的是,反射,即在不事先告诉编译器的情况下在运行时获取东西,正是编译器无法弄清楚的一件事。
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 生成任何代码,导致这里的 type 为 null。
此外,依赖分析是精确到单个方法的,这意味着即使一个类型被认为是一个依赖,如果该类型中的某个方法没有被使用,该方法也不会被包含在代码生成中。
虽然这可以通过将所有类型和方法添加到依赖图中来解决,这样编译器就会为它们生成代码。这就是 TrimmerRootAssembly 的作用:通过提供 TrimmerRootAssembly,NativeAOT 编译器会将你指定的程序集中的所有东西都作为根。
但是涉及泛型的情况就不是这样了。
动态泛型实例化
在 .NET 中,我们有泛型,编译器会为每个非共享的泛型类型和方法生成不同的代码。
假设我们有一个类型 Point<T>:
struct Point<T>
{
public T X, Y;
}
如果我们有一段代码试图使用 Point<int>,编译器会为 Point<int> 生成专门的代码,使得 Point.X 和 Point.Y 都是 int。如果我们有一个 Point<float>,编译器会生成另一个专门的代码,使得 Point.X 和 Point.Y 都是 float。
通常情况下,这不会导致任何问题,因为编译器可以静态地找出你在代码中使用的所有实例化,直到你试图使用反射来构造一个泛型类型或一个泛型方法:
var type = Type.GetType(Console.ReadLine());
var pointType = typeof(Point<>).MakeGenericType(type);
上面的代码在 NativeAOT 下不会工作,因为编译器无法推断出 Point<T> 的实例化,所以编译器既不会生成 Point<int> 的代码,也不会生成 Point<float> 的代码。
尽管编译器可以为 int,float,甚至泛型类型定义 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.A,Bar 中的所有公共属性都会被添加到依赖图中,这样我们就能够对 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 之前,总是考虑用 DynamicallyAccessedMembers 和 DynamicDependency 来注释你的代码,使其与剪裁/AOT 兼容。
结语
NativeAOT 是 .NET 中一个非常棒和强大的工具。有了 NativeAOT,你可以以可预测的性能构建你的应用,同时节省资源(更低的内存占用和更小的二进制大小)。
它还将 .NET 带到了不允许 JIT 编译器的平台,例如 iOS 和主机平台。此外,它还使 .NET 能够运行在嵌入式设备甚至裸机设备上(例如在 UEFI 上运行)。
在使用工具之前了解工具,这样你会节省很多时间。
.NET NativeAOT 指南的更多相关文章
- JavaScript权威指南 - 函数
函数本身就是一段JavaScript代码,定义一次但可能被调用任意次.如果函数挂载在一个对象上,作为对象的一个属性,通常这种函数被称作对象的方法.用于初始化一个新创建的对象的函数被称作构造函数. 相对 ...
- UE4新手之编程指南
虚幻引擎4为程序员提供了两套工具集,可共同使用来加速开发的工作流程. 新的游戏类.Slate和Canvas用户接口元素以及编辑器功能可以使用C++语言来编写,并且在使用Visual Studio 或 ...
- JavaScript权威指南 - 对象
JavaScript对象可以看作是属性的无序集合,每个属性就是一个键值对,可增可删. JavaScript中的所有事物都是对象:字符串.数字.数组.日期,等等. JavaScript对象除了可以保持自 ...
- JavaScript权威指南 - 数组
JavaScript数组是一种特殊类型的对象. JavaScript数组元素可以为任意类型,最大容纳232-1个元素. JavaScript数组是动态的,有新元素添加时,自动更新length属性. J ...
- const extern static 终极指南
const extern static 终极指南 不管是从事哪种语言的开发工作,const extern static 这三个关键字的用法和原理都是我们必须明白的.本文将对此做出非常详细的讲解. co ...
- Atitit.研发管理软件公司的软资产列表指南
Atitit.研发管理软件公司的软资产列表指南 1. Isv模型下的软资产1 2. 实现层面implet1 3. 规范spec层1 4. 法则定律等val层的总结2 1. Isv模型下的软资产 Sof ...
- HA 高可用软件系统保养指南
又过了一年 618,六月是公司一年一度的大促月,一般提前一个月各系统就会减少需求和功能的开发,转而更多去关注系统可用性.稳定性和管控性等方面的非功能需求.大促前的准备工作一般叫作「备战」,可以把线上运 ...
- 第六代智能英特尔® 酷睿™ 处理器图形 API 开发人员指南
欢迎查看第六代智能英特尔® 酷睿™ 处理器图形 API 开发人员指南,该处理器可为开发人员和最终用户提供领先的 CPU 和图形性能增强.各种新特性和功能以及显著提高的性能. 本指南旨在帮助软件开发人员 ...
- Visual Studio Code 配置指南
Visual Studio Code (简称 VS Code)是由微软研发的一款免费.开源的跨平台文本(代码)编辑器.在我看来它是「一款完美的编辑器」. 本文是有关 VS Code 的特性介绍与配置指 ...
- Web API 入门指南 - 闲话安全
Web API入门指南有些朋友回复问了些安全方面的问题,安全方面可以写的东西实在太多了,这里尽量围绕着Web API的安全性来展开,介绍一些安全的基本概念,常见安全隐患.相关的防御技巧以及Web AP ...
随机推荐
- AtCoder Beginner Contest 218 A~D
比赛链接:Here A - Weather Forecas 水题,判断 \(s[n - 1] = o\) 的话输出 YES B - qwerty 题意:给出 \((1,2,...,26)\) 的某个全 ...
- AtCoder Beginner Contest 195 Editorial
AtCoder Beginner Contest 195 Editorial Problem A - Health M Death(opens new window) 只要检查 \(H\equiv 0 ...
- js滑动验证
https://gitee.com/anji-plus/captcha AjCaptcha验证码: https://blog.csdn.net/zbchina2004/article/details/ ...
- CSS Sticky Footer 几种实现方式
项目里,有个需求,登录页,信息,需要使用到sticky footer布局,刚好,巩固下这个技术: 核心代码: 播客: https://www.jb51.net/css/676798.html 视频:h ...
- Java虚拟机——内存区域及内存溢出异常
一.Java内存区域 1.概述 对于java程序员来说,在虚拟机的自动内存管理机制的帮助下,不需要为每一个new操作去写delete/free代码,而且不容易出现内存泄漏和内存溢出问题.但是把内存控制 ...
- docker目录迁移流程
概述 在安装测试最新版本的HOMER7的过程中,docker作为基础工具碰到一些问题,针对问题进行总结. docker的默认工作目录在/var目录,而在我们的环境中,/var目录空间预留不足,随着do ...
- 大四上 | 计算机综合课设(OS)· 答辩经验帖
课设代码 repo 被问了如下问题: 我们的 OS 中是否有 idle 进程. 背景:如果所有进程都被 kill 掉了,那么 os 就会陷入死循环.即使再发生需要响应的事情,比如希望再创建个进程 或者 ...
- spring--FactoryBean和BeanFactory有关系吗
BeanFactory 和 FactoryBean 是 Spring 框架中的两个不同的概念,两者是雷锋和雷峰塔的关系,就是没有任何关系,它们在 Spring 的依赖注入和 bean 创建过程中扮演不 ...
- Blazor开发小游戏?趁热打铁上!!!
大家好,我是沙漠尽头的狼. 网站使用Blazor重构上线一天了,用Blazor开发是真便捷,空闲时间查查gpt和github,又上线一个 正则表达式在线验证工具 和几个在线小游戏,比如 井字棋游戏.扫 ...
- 01-module/分频器/激励写法
1.module module有出入接口,输出接口 module有时钟和复位 // input clock; rest_n; // n表示低电平复位 //output o_data; module m ...