一:背景

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. 基于druid和spring的动态数据库以及读写分离 转

    spring与druid可以实现动态数据源,夸库查询,读写分离等功能.现在说一下配置: 1.需要配置多个spring数据源 spring-data.xml <!-- 动态数据源 --> & ...

  2. Linux驱动|cdev_init、cdev_alloc区别

    这两个函数是字符设备初始化相关的内核函数. 要想了解这两个函数,必须要知道字符设备的架构,以及字符设备创建的流程. 关于字符设备可以参考下面这篇文章 <手把手教Linux驱动3-之字符设备架构详 ...

  3. Docker Windows 下的绑定挂载

    在 Windows 环境下进行绑定挂载时,需要注意路径的写法,需要使用 Windows 风格 (C:\xxx\xxx) 的路径,而不是 Cygwin (/c/xxx/xxx) 风格的路径.这一点在使用 ...

  4. HttpContext.SignInAsync 失效(表面解决了问题,未深入到.net core 源码去找问题,记录一下,等有时间翻一下.net core 源码试试能不能找到根本原因)

    今天在弄 identityServer4 项目的时候,发现好好的登录竟然没用了. 各种跟踪后发现是 HttpContext.SignInAsync 这个方法不写cookies了 原本经过这个方法后,会 ...

  5. 线性dp:LeetCode516 .最长回文子序列

    LeetCode516 .最长回文子序列 题目叙述: 力扣题目链接(opens new window) 给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度. 子序列定义为:不改变剩余字 ...

  6. SQL查折线图数据

    在做后台管理系统的时候,有很大概率是要与各种图表数据打交道, 那么如何通过SQL查出基本图表的数据呢,以折线图为例, X轴为日期,Y轴为统计数据,那么我们通过SQL如何查询出连续日期下的统计数据呢, ...

  7. SpringBoot 引入第三方 jar

    SpringBoot 引入第三方 jar 项目结构 -BCJS |--lib |--hsm-talos-1.0.1.jar |--src |--pom.xml step1 : 配置第三方 jar 为依 ...

  8. windows系统使用UnblockNeteaseMusic解锁网易云音乐灰色歌曲

    使用UnblockNeteaseMusic解锁网易云音乐灰色歌曲 一.问题 用网易云音乐听歌曲,有的曲目听不了,如下: 二.解决步骤 参照:https://github.com/nondanee/Un ...

  9. VMware安装CentOS7及远程登录详细教程

    写在前面 主要使用软件: VMware Workstation Pro17 Navicat Premium17 Xshell7 Xftp7 1.在虚拟机安装CentOS7 访问阿里云镜像站 ,选择标记 ...

  10. 深度学习模型训练的过程理解(训练集、验证集、测试集、batch、iteration、epoch、单步预测、多步预测、kernels、学习率)

    呜呜呜呜,感谢大佬学弟给我讲干货. 本来是讨论项目的,后面就跑偏讲论文模型了. 解答了我一直以来的疑问: 数据放模型里训练的过程. 假设我们有一个数据集26304条数据,假设设置模型读入1000条,如 ...