一:背景

1. 讲故事

上一篇我们聊过AOT编程中可能会遇到的三大件问题,而这三大件问题又是考验你对AOT中节点图的理解,它是一切的原点,接下来我就画几张图以个人的角度来解读下吧,不一定对。

二:理解节点依赖图

1. 对节点的理解

按照官方的说法,构建依赖节点和GC的标记算法一样,都是采用深度优先,每一个节点都是一种类型,比如:

  1. MethodCodeNode 表示方法节点
  2. EETypeNode 表示 MethodTable 类型节点

同时节点的层级关系比较深,比如这样的链路, MethodCodeNode -> ObjectNode -> SortableDependencyNode -> DependencyNodeCore<DependencyContextType> -> DependencyNode -> IDependencyNode

对了,最核心的节点依赖图算法来自于方法 DependencyAnalyzer.ComputeMarkedNodes(), 简化后如下:


public override void ComputeMarkedNodes()
{
do
{
// Run mark stack algorithm as much as possible
using (PerfEventSource.StartStopEvents.DependencyAnalysisEvents())
{
ProcessMarkStack();
} // Compute all dependencies which were not ready during the ProcessMarkStack step
_deferredStaticDependencies.TryGetValue(_currentDependencyPhase, out var deferredDependenciesInCurrentPhase); if (deferredDependenciesInCurrentPhase != null)
{
ComputeDependencies(deferredDependenciesInCurrentPhase);
foreach (DependencyNodeCore<DependencyContextType> node in deferredDependenciesInCurrentPhase)
{
Debug.Assert(node.StaticDependenciesAreComputed);
GetStaticDependenciesImpl(node);
} deferredDependenciesInCurrentPhase.Clear();
} if (_markStack.Count == 0)
{
// Time to move to next deferred dependency phase. // 1. Remove old deferred dependency list(if it exists)
if (deferredDependenciesInCurrentPhase != null)
{
_deferredStaticDependencies.Remove(_currentDependencyPhase);
} // 2. Increment current dependency phase
_currentDependencyPhase++; // 3. Notify that new dependency phase has been entered
ComputingDependencyPhaseChange?.Invoke(_currentDependencyPhase);
}
} while ((_markStack.Count != 0) || (_deferredStaticDependencies.Count != 0)); }

在遍历的过程中,它是先用 ProcessMarkStack() 处理所有的静态节点,在处理完后再处理那些在上一阶段产生的新节点或者在上一阶段还没预备好的节点,这里叫 延迟节点,这个说起来有点懵,举个例子: A 是必达节点,C 只有在 B 进入依赖图时才进去,否则不进入,所以这叫条件依赖。最后我再配一张图,大家可以观赏下:

再往下编我就编不下去了,写一个小例子直观的感受下吧。

2. 一个小例子

代码非常简单,大家可以看看这段代码构建的依赖图可能是个什么样子?


internal class Program
{
static int Main(string[] args)
{
Animal animal = new Bird();
animal.Sound();
return animal is Dog ? 1 : 0;
}
} public abstract class Animal
{
public virtual void Fly() { } public abstract void Sound();
} public class Bird : Animal
{
public override void Sound() { } public override void Fly() { }
} public class Dog : Animal
{
public override void Sound() { }
}

就不吊着大家了,最后的依赖图大概是这个样子。

上图稍微解释一下:

  • 矩形: 方法体
  • 椭圆: 类
  • 虚线矩形: 虚方法
  • 点状椭圆形: 未构造的类
  • 虚线边: 条件依赖关系

从图中可以看到,起点是在 Program::Main 函数上,这里要稍微提醒一下,这是逻辑上的托管入口,在 ilc 层面真正的入口是非托管函数 {[Example_21_1]<Module>.StartupCodeMain(int32,native int)} 上,大家可以对 DependencyAnalyzerBase<DependencyContextType>.AddRoot 上下一个断点即可,截图如下:

眼尖得朋友可能会有一个疑问,这个 Bird.Fly() 在依赖图中被移走了是能够说得通得,但有没有什么证据让我眼见为实一下呢?

3. 如何观察节点移走了

aot在调试支持上做了很多的努力,比如通过 IlcGenerateMapFile 就可以让你看到每一个依赖图的节点类型,在 csproj 上配置如下:


<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
<IlcGenerateMapFile>true</IlcGenerateMapFile>
</PropertyGroup>
</Project>

接下来打开生成好的 obj\Debug\net8.0\win-x64\native\Example_21_1.map.xml 文件,搜索对应的 Bird__SoundBird__Fly 方法。

对了,上面的 MethodCode 节点我稍微解释一下,完整的如下:


<MethodCode Name="Example_21_1_Example_21_1_Bird__Sound" Length="16" Hash="5e2f1c14edcffc6459b012c27e0e8410215a90cfa5dda68376042264d59e6252" />

刚才也说了 MethodCode 是一个方法节点,Name 不用说了,Length 是方法的汇编代码长度,Hash是对字节码的hash表示,这个在源码上的 XmlObjectDumper.DumpObjectNode 上能够找到答案的。

4. 未构造类型解读

这个指的是上面的 return animal is Dog ? 1 : 0; 这句话,我个人觉得AOT团队这一块没做好,为什么呢?因为 Animal is Dog 底层调用的是 CastHelpers.IsInstanceOfClass 方法,而这个方法底层只需要保存 MethodTable.ParentMethodTable 信息就行了,截图如下:

但遗憾的是AOT居然把 Example_21_1.Dog.Sound() 也追加到依赖图,这就完全没有必要了。

退一万步说生成就生成吧,但恶心的是又不给生成 Dog::Dog 构造函数,这就导致这个 Dog 无法实例化,造成 Dog.Sound 成了一个孤岛函数,无语了,在 csproj 上配置 <IlcGenerateMapFile>true</IlcGenerateMapFile> 节点可以更直观的观察到。

三:总结

节点依赖图的生成是一个比较复杂的过程,目前.NET8 中的 AOT Compiler 还是有很大的优化空间,比如:

  1. 基于上下文的依赖推测。
  2. 未构造类型的推测。
  3. 还不知道的一些未知...

期待后续的 .NET9, .NET10 有更大的提升吧。

AOT漫谈专题(第七篇): 聊一聊给C#打造的节点依赖图的更多相关文章

  1. PerfView专题 (第七篇):如何洞察触发 GC 的 C# 代码?

    一:背景 上一篇我们聊到了如何用 PerfView 洞察 GC 的变化,但总感觉还缺了点什么? 对,就是要跟踪到底是什么代码触发了 GC,这对我们分析由于 GC 导致的 CPU 爆高有非常大的参考价值 ...

  2. 无线安全专题_攻击篇--MAC泛洪攻击

    上一篇讲解了无线安全专题_攻击篇--干扰通信,没在首页待多长时间就被拿下了,看来之后不能只是讲解攻击实战,还要进行技术原理和防御方法的讲解.本篇讲解的是局域网内的MAC泛洪攻击,这种攻击方式主要目的是 ...

  3. 跟我学SpringCloud | 第七篇:Spring Cloud Config 配置中心高可用和refresh

    SpringCloud系列教程 | 第七篇:Spring Cloud Config 配置中心高可用和refresh Springboot: 2.1.6.RELEASE SpringCloud: Gre ...

  4. 解剖SQLSERVER 第七篇 OrcaMDF 特性概述(译)

    解剖SQLSERVER 第七篇  OrcaMDF 特性概述(译) http://improve.dk/orcamdf-feature-recap/ 时间过得真快,这已经过了大概四个月了自从我最初介绍我 ...

  5. 第七篇 :微信公众平台开发实战Java版之如何获取微信用户基本信息

    在关注者与公众号产生消息交互后,公众号可获得关注者的OpenID(加密后的微信号,每个用户对每个公众号的OpenID是唯一的.对于不同公众号,同一用户的openid不同). 公众号可通过本接口来根据O ...

  6. 第七篇 Replication:合并复制-订阅

    本篇文章是SQL Server Replication系列的第七篇,详细内容请参考原文. 订阅服务器就是复制发布项目的所有变更将传送到的服务器.每一个发布需要至少一个订阅,但是一个发布可以有多个订阅. ...

  7. 第七篇 Integration Services:中级工作流管理

    本篇文章是Integration Services系列的第七篇,详细内容请参考原文. 简介在上一篇文章,我们创建了一个新的SSIS包,学习了SSIS中的脚本任务和优先约束,并检查包的MaxConcur ...

  8. 第七篇 SQL Server安全跨数据库所有权链接

    本篇文章是SQL Server安全系列的第七篇,详细内容请参考原文. Relational databases are used in an amazing variety of applicatio ...

  9. 第七篇 SQL Server代理作业活动监视器

    本篇文章是SQL Server代理系列的第七篇,详细内容请参考原文 在这一系列的上一篇,你创建并配置SQL Server代理作业.每个作业有一个或多个步骤,可能包含大量的工作流.在这篇文章中,将查看作 ...

  10. 用仿ActionScript的语法来编写html5——第七篇,自定义按钮

    第七篇,自定义按钮这次弄个简单点的,自定义按钮.其实,有了前面所定义的LSprite,LBitmap等类,定义按钮就很方便了.下面是添加按钮的代码, function gameInit(event){ ...

随机推荐

  1. PEP 703作者给出的一种no-GIL的实现——python3.9的nogil版本

    PEP 703的内容是什么,意义又是什么呢? 可以说python的官方接受的no-GIL提议的PEP就是PEP 703给出的,如果GIL帧的从python中移除那么可以说对整个python生态圈将有着 ...

  2. 安装RabbitMQ遇到的一些坑

    Ubantu18.0正确安装RabbitMQ 1.安装erlang 因为RabbitMQ需要erlang语言的支持,所以我们需要先安装erlang. sudo apt-get install erla ...

  3. 关于REACT范式的一些思考

    关于REACT范式的一些思考 REACT范式经过近一年的探索,让我们在很多领域有了非常广泛的应用,它确实提升了很多之前无法解决的问题,比如大模型虽然在语言理解和交互式决策方面在任务中表现出令人印象深刻 ...

  4. .net5调用WebService简单事例

    1. 创建 .net5控制台项目: dotnet new console -o WebServiceConsole 2. 添加全局工具 dotnet tool install --global dot ...

  5. Java设计模式之单例模式 通俗易懂 超详细 【内含案例】

    单例模式 推荐 Java 常见面试题 什么是单例模式 ? 确保程序中一个类只能被实例化一次,实现这种功能就叫单例模式 单例模式的好处是什么 ? 方便控制对象 节省资源减少浪费 怎么实现单例模式 ? 构 ...

  6. 首次在WebAPI中写单元测试

    xUnit 这次我使用的是xUnit测试框架,而不是VS自带的MSTest框架.在添加新建项目时选择xUnit测试项目就行了. 目前只体验到了一个差别,即xUnit可以使用特性向测试方法传参,而不用在 ...

  7. bazel简介(二)——从makefile向bazel转变(使用genrule)

    0x01 背景 上篇中已经介绍了bazel的基本工作原理和相关的概念.这篇将继续介绍下,现有的makefile构建工程如何切换到bazel构建系统. bazel提供了丰富的扩展方式,当然也支持从目前的 ...

  8. Linux 常见编辑器

    命令行编辑器 Vim Linux 上最出名的编辑器当属 Vim 了.Vim 由 Vi 发展而来,Vim 的名字意指 Vi IMproved,表示 Vi 的升级版.Vim 对于新手来说使用比较复杂,不过 ...

  9. failed to copy: httpReadSeeker: failed open: unexpected status code xxx 403

    ack上pull镜像的时候,报的错 非运行脚本的问题,由负责ack相关设定的人员调整即可

  10. HTML – HTML Tags & Semantic HTML 语义化 HTML

    前言 HTML tag 有 100 多个, 有些是功能形的, 非用不可, 有些是为了语义化对 screen reader 友好 (给眼睛有残缺的人也可以获取清晰的网站信息). 语义化是很重要的, 有些 ...