一:背景

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. 洛谷P1226 【模板】快速幂

    1.快速幂模板 前置知识 一个数字n,它的二进制位数一定是log2n向下取整+1: 快速幂模板代码 这段代码实现了快速幂算法(Exponentiation by squaring),用来计算 ( an ...

  2. Apache DolphinScheduler 1.3.4升级至3.1.2版本过程中的踩坑记录

    因为在工作中需要推动Apache DolphinScheduler的升级,经过预研,从1.3.4到3.1.2有的体验了很大的提升,在性能和功能性有了很多的改善,推荐升级. 查看官方的升级文档,可知有提 ...

  3. Apache DolphinScheduler使用图关系解决核心链路告警问题,减轻任务运维负担!

    转载自程序员小陶 Apache DolphinScheduler 在使用过程中,肯定会有任务出现失败的情况,那么问题来了:调度任务的告警是需要人为配置的,在生产环境中,面对海量的任务,如何找到重要的任 ...

  4. vue-表单与v-model

    使用 v-model 后,表羊控件显示的值只依赖所绑定的数据,不再关心初始化时的 value 属性,对于 textarea></textarea> 之间插入的值,也不会生效. 使用 ...

  5. Mac版Sublime Text 4152 激活

    Sublime Text下载地址:Download - Sublime Text 破解工具"Hex Fiend"下载地址:Hex Fiend, a fast and clever ...

  6. 这才是java对象正解

    这才是 Java 对象正解 在深入讨论对象之前,让我们先明确对对象和实例的理解. 什么是对象? 对象(Object)是内存中分配的实际数据结构,它包含了数据和方法.在 Java 中,对象是类的一个实例 ...

  7. kafka消费者处理能力低引起rebalance分析

    一.背景介绍 项目上进行算法调度的需求,打算采用kafka作为消息中间件,通过将多个算法消费者加入到同一个group中并行的处理算法请求,从而达到高效处理的目的.但是算法处理的时间较长,多则几十分钟, ...

  8. Linux 磁盘分区和格式化

    分区 常用命令行工具: fdisk:适用于 MBR 分区表 gdisk:适用于 GPT 分区表 parted:适用于 MBR 和 GPT 分区表,功能更强大.它还有一个 GUI 版本,名为 gpart ...

  9. 【YashanDB知识库】EXP导致主机卡死问题

    问题现象 问题单:exp导出全库1主2备主节点执行,DMP文件30G左右系统卡死,发生主备切换 现象: exp sys/Cod-2022 file=bim20240402.dmp full=y 服务器 ...

  10. 关于 Splay 树

    前置芝士 $\LARGE {关于二叉搜索树及平衡树无聊的一大串子定义}$ 二叉搜索树(BST树) 定义 二叉搜索树是一种二叉树的树形数据结构,其定义如下: 空树是二叉搜索树. 若二叉搜索树的左子树不为 ...