在360做网站和百度做网站的区别,wordpress 盗链,建设部网站79号文件,h5海报模板前言TypedocConverter 是我先前因帮助维护 monaco-editor-uwp 但苦于 monaco editor 的 API 实在太多#xff0c;手写 C# 的类型绑定十分不划算而发起的一个项目。这个工具可以将 typedoc 根据 TypeScript 生成的 JSON 文件直接生成对应的 C# 类型绑定代码#xff0c;并提供完… 前言TypedocConverter 是我先前因帮助维护 monaco-editor-uwp 但苦于 monaco editor 的 API 实在太多手写 C# 的类型绑定十分不划算而发起的一个项目。这个工具可以将 typedoc 根据 TypeScript 生成的 JSON 文件直接生成对应的 C# 类型绑定代码并提供完整的 JSON 序列化支持因此使用这个工具可以大大降低移植 TypeScript 库到 .NET 上的困难。至于为什么是从 typedoc 而不是从 TypeScript 直接 parse其实只是因为太懒了不想写 TypeScript 的 parserTypedocConverter 使用 F# 编写虽然使用 .NET 5 可以做到程序集裁剪后使用单文件自托管发布但是我一直在想如果能使用 AOT 技术将整个程序编译为 native binary 那就好了这样的话用户在使用的时候将不需要运行 .NET 的运行时也不需要 JIT而是直接运行机器代码。工具除了功能性之外最重要的就是用户体验这样做将大大提升程序的启动速度虽然原本已经够快了但是我想将 100ms 的启动时间缩短到不到 1ms使得用户使用该工具时不需要任何的等待。AOT 方案调研.NET 一直以来都有一个叫做 CoreRT 的项目使用该工具可以将 .NET 程序集编译到 native binary然而这个项目自从 2018 年官方就没有再积极维护。但是由于社区的强烈呼声以及某个微软的合作伙伴的项目需要 AOT 技术并表示如果没有这项技术将不再使用 .NET于是这个项目原地复活以 NativeAOT 的名字转移到了 runtimelab 并作为 .NET 6 的 P0最高 优先级实验性工作项即提供带支持的官方 preview而不再是原来的万年 alpha目前支持 win-x64、linux-x64 和 osx-x64对于 ARM64 、移动平台和浏览器WebAssembly的支持在计划当中。借着这个契机我决定使用该方案将项目编译为原生镜像。NativeAOT 原理.NET 的 NativeAOT 的思路其实很简单首先需要一个 AOT 友好的、用于 NativeAOT 的核心库 System.Private.CoreLib实现提供类型和实现查找、类型解析等方法扫描程序集记录用到的类型和方法调用 RyuJIT 接口生成类型的元数据为所有的方法生成代码最终产生出 obj 二进制文件调用链接器MSVC 或 clang将产生的 obj 与 GC 和系统库等链接成为最终的可执行文件现阶段 NativeAOT 基本已经完成剩余的部分工作则是一些修补和完善以及对新版本 .NET 的跟进目前还没有跟进 C# 8 之后牵扯到运行时修改的特性如默认接口方法实现和模块初始化器等等。可能你会问这和 .NET Native 技术有何不同不同之处在于 .NET Native 使用 UTC 编译器MSVC 后端进行代码生成而 NativeAOT 使用 RyuJIT 进行代码生成。关于 .NET NativeAOT 完整的使用文档可以参考using-native-aot。针对 NativeAOT 改造项目NativeAOT 使用非常简单只需要修改 csproj 项目文件即可PropertyGroupIlcOptimizationPreferenceSpeed/IlcOptimizationPreferenceIlcFoldIdenticalMethodBodiestrue/IlcFoldIdenticalMethodBodies
/PropertyGroup
ItemGroupPackageReference IncludeMicrosoft.DotNet.ILCompiler Version6.0.0-* /
/ItemGroupIlcOptimizationPreference 指定 Speed 表示以最大性能为目标生成代码如果指定 Size 则表示以最小程序为目标生成代码。IlcFoldIdenticalMethodBodies 参数则可以将相同的方法体合并有助于减小体积。最后则是新的 Microsoft.DotNet.ILCompiler这是 NativeAOT 编译器本体通过 wildcard 指定 6.0.0-* 版本这样每次编译都会获取最新的版本。由于 Microsoft.DotNet.ILCompiler 来自实验仓库的 artifacts而没有发布在官方的 nuget 源需要新建 nuget.config 额外将实验仓库的 artifacts 作为源引入?xml version1.0 encodingutf-8?
configurationpackageSourcesadd keydotnet-experimental valuehttps://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json //packageSources
/configuration如此一来便大功告成了这就开始编译dotnet publish -c Release -r win-x64伴随而来的是大量的警告AOT analysis warning IL9700: Microsoft.FSharp.Reflection.FSharpType.MakeFunctionType(Type,Type): Calling System.Type.MakeGenericType(Type[]) which has RequiresDynamicCodeAttribute can break functionality when compiled fully ahead of time. The native code for this instantiation might not be available at runtime.
AOT analysis warning IL9700: Microsoft.FSharp.Reflection.FSharpValue.MakeFunction(Type,FSharpFunc2Object,Object): Calling System.Type.MakeGenericType(Type[]) which has RequiresDynamicCodeAttribute can break functionality when compiled fully ahead of time. The native code for this instantiation might not be available at runtime.
...观察警告可以发现这是分析器报出来的理由很简单NativeAOT 是不支持运行时动态代码生成的但是 MakeGenericType 在需要在运行时产生类型因此可能不受支持。为什么说是可能呢因为 NativeAOT 条件下不支持运行时产生新的类型但是对于已经生成代码的类型则是完全支持的。由于项目没有用到 System.Reflection.Emit 在运行时动态织入 IL也没有用到 Assembly.LoadFile 等动态加载程序集更没有用到 C/CLI 和 COM因此是 NativeAOT 兼容的。编译速度尚可只等待了半分钟。编译完成后产生了一个 29mb 的 exe体积还不够优秀但是先运行看看 ./TypedocConverter
[Error] No input file
Typedoc Converter Arguments: --inputfile [file]: input file
--namespace [namespace]: specify namespace for generated code
--splitfiles [true|false]: whether to split code to different files
--outputdir [path]: used for place code files when splitfiles is true
--outputfile [path]: used for place code file when splitfiles is false
--number-type [int/decimal/double...]: config for number type mapping
--promise-type [CLR/WinRT]: config for promise type mapping, CLR for Task and WinRT for IAsyncAction/IAsyncOperation
--any-type [object/dynamic...]: config for any type mapping
--array-type [Array/IEnumerable/List...]: config for array type mapping
--nrt-disabled [true|false]: whether to disable Nullable Reference Types
--use-system-json [true|false]: whether to use System.Text.Json instead of Newtonsoft.Json一瞬间就运行了起来完全感受不到启动时间体感小于 1ms这个体验太爽了。可是正当我高兴的时候使用一个实际的 JSON 文件对功能进行测试却报错了Unhandled Exception: EETypeRva:0x013EC198(System.Reflection.MissingRuntimeArtifactException): MakeGenericMethod() cannot create this generic method instantiation because no code was generated for it: Microsoft.FSharp.Collections.ListModule.OfSeqSystem.Int32(System.Collections.Generic.IEnumerableSystem.Int32).at Internal.Reflection.Core.Execution.ExecutionEnvironment.GetMethodInvoker(RuntimeTypeInfo, QMethodDefinition, RuntimeTypeInfo[], MemberInfo) 0x144at System.Reflection.Runtime.MethodInfos.NativeFormat.NativeFormatMethodCommon.GetUncachedMethodInvoker(RuntimeTypeInfo[], MemberInfo) 0x50at System.Reflection.Runtime.MethodInfos.RuntimeMethodInfo.get_MethodInvoker() 0xa1at System.Reflection.Runtime.MethodInfos.RuntimeNamedMethodInfo1.MakeGenericMethod(Type[]) 0x104...可以看到方法 Microsoft.FSharp.Collections.ListModule.OfSeqSystem.Int32(System.Collections.Generic.IEnumerableSystem.Int32 缺失了。这是因为 NativeAOT 编译器并没有通过代码路径分析出该类型因此没有为该类型生成代码导致运行时尝试创建该类型时由于找不到实现代码而出错。因此需要通过 Runtime Directives 指示编译器生成指定类型和方法的代码方法是创建一个 rd.xml 并引入项目 ItemGroupRdXmlFile Includerd.xml //ItemGroup然后在 rd.xml 中编写需要编译器额外生成的类型和方法。经过一番试错之后我写出了如下的代码DirectivesApplicationAssembly NameFSharp.Core DynamicRequired AllType NameMicrosoft.FSharp.Collections.ListModule DynamicRequired AllMethod NameOfSeq DynamicRequiredGenericArgument NameSystem.Int32,System.Private.CoreLib //Method/TypeType NameMicrosoft.FSharp.Core.PrintfImplSpecializations3[[System.Object,System.Private.CoreLib],[System.Object,System.Private.CoreLib],[System.Object,System.Private.CoreLib]] DynamicRequired AllMethod NameCaptureFinal1 DynamicRequiredGenericArgument NameSystem.Object,System.Private.CoreLib //MethodMethod NameCaptureFinal2 DynamicRequiredGenericArgument NameSystem.Object,System.Private.CoreLib /GenericArgument NameSystem.Object,System.Private.CoreLib //MethodMethod NameCaptureFinal3 DynamicRequiredGenericArgument NameSystem.Object,System.Private.CoreLib /GenericArgument NameSystem.Object,System.Private.CoreLib /GenericArgument NameSystem.Object,System.Private.CoreLib //MethodMethod NameOneStepWithArg DynamicRequiredGenericArgument NameSystem.Object,System.Private.CoreLib //MethodMethod NameCapture1 DynamicRequiredGenericArgument NameSystem.Object,System.Private.CoreLib /GenericArgument NameSystem.Object,System.Private.CoreLib //MethodMethod NameCapture2 DynamicRequiredGenericArgument NameSystem.Object,System.Private.CoreLib /GenericArgument NameSystem.Object,System.Private.CoreLib /GenericArgument NameSystem.Object,System.Private.CoreLib //MethodMethod NameCapture3 DynamicRequiredGenericArgument NameSystem.Object,System.Private.CoreLib /GenericArgument NameSystem.Object,System.Private.CoreLib /GenericArgument NameSystem.Object,System.Private.CoreLib /GenericArgument NameSystem.Object,System.Private.CoreLib //Method/Type /AssemblyAssembly NameSystem.Linq DynamicRequired AllType NameSystem.Linq.Enumerable DynamicRequired AllMethod NameToArray DynamicRequiredGenericArgument NameSystem.Int32,System.Private.CoreLib //Method/Type/Assembly/Application
/Directives稍微对上面的东西进行一下解释Name 用于指定类型, 前后分别是类型的完整名称和类型来自的程序集名称.NET 中的各种基础类型都来源于 System.Private.CoreLib 或 mscorlib。详细的格式说明可以参考 rd-xml-format。在 .NET 中编译器会为所有的值类型的泛型参数特化一份实现而所有的引用类型参数共享一份实现。这么做其实原因显而易见因为引用类型背后只是一个指针罢了。因此根据这个特点所有的引用类型都无需指定实际的类型参数统一指定一个 System.Object 就好了而对于值类型作为类型参数则需要指出生成什么类型的代码。经过上面一番折腾之后重新编译运行这次所有的功能均正常了启动速度飞快运行时性能也非常棒并且纯静态链接无需安装任何运行时就能运行体验几乎和 C 编写出来的程序一样。程序体积优化上面一系列操作之后虽然启动和运行速度很快但是生成的程序大小有 30 mb还是有些大那么接下来在不牺牲运行时代码性能的情况下针对程序体积进行优化。首先指定 TrimMode为 Link这可以使 NativeAOT 采用更加激进的程序集剪裁方案将代码路径中没有被引用的代码以方法为粒度删掉另外想到自己的程序不需要国际化支持因此可以删除掉没有用的多语言支持及其资源文件。PropertyGroupTrimModeLink/TrimModeInvariantGlobalizationtrue/InvariantGlobalization
/PropertyGroup重新进行编译这个时候产生的 exe 大小只有 27mb 了运行测试Unhandled Exception: Newtonsoft.Json.JsonSerializationException: Unable to find a constructor to use for type DefinitionsReflection. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute. Path id, line 2, position 6.at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateNewObject(JsonReader, JsonObjectContract, JsonProperty, JsonProperty, String, Boolean) 0x1d1at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader, Type, JsonContract, JsonProperty, JsonContainerContract, JsonProperty, Object) 0x2ccat Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader, Type, JsonContract, JsonProperty, JsonContainerContract, JsonProperty, Object) 0xa4at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader, Type, Boolean) 0x26eat Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader, Type) 0xf8at Newtonsoft.Json.JsonConvert.DeserializeObject(String, Type, JsonSerializerSettings) 0x93at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String, JsonSerializerSettings) 0x2bat Program.main$cont84(JsonSerializerSettings, Definitions.Config, Unit) 0x31at TypedocConverter!BaseAddress0x83a0ca根据报错信息我们知道是 JSON 反序列化过程出了问题问题在于 DefinitionsReflection 类型被裁剪掉了。由于我知道我自己的程序内进行 JSON 反序列化的目标类型都是来自于我自己的程序集本身因此不必使用 rd.xml 那么麻烦只需要告诉编译器不要裁剪我自己的程序集中的类型即可这对于泛型类实例无效因为泛型类型实现是需要特化的ItemGroupTrimmerRootAssembly IncludeTypedocConverter /
/ItemGroup接下来重新编译运行这次没问题了。最终程序的大小是 27mb相比 30mb 并没有小太多不过这也正常毕竟前面写的 rd.xml 中由于偷懒通过 DynamicRequire All 保留了 F# 核心库中的所有类型。如果我去掉 DynamicRequire All 的话最终编译出 22mb 的二进制文件但是需要更多的精力调研有哪些类型需要写进 rd.xml。通过 zip 压缩之后只剩下 11mb这个体积我觉得已经不错了。当然要注意的是Windows 下调试符号文件默认作为单独的 pdb 文件提供而在 *inx 下调试符号是直接内嵌到程序二进制数据中的因此在非 Windows 平台下需要使用 strip 命令将符号裁剪掉否则你将得到一个非常大的二进制程序文件。strip ./TypedocConverter想要看看最终效果的可以去此处下载含 Native 名称的 Release 文件体验github.com/hez2010/Type。到这里有人可能好奇 NativeAOT 最小能做到多小经过实验禁用反射并取消 root 所有程序集后的 hello world 项目可以做到不到 1mb 的体积。已知问题和限制.NET NativeAOT 预计会在 .NET 6 将会为尝鲜者提供带支持的预览其实已经足够稳定现阶段有一些比较影响使用的已知问题我将在这里列出。由于缺少实现而不支持主要是 C# 8 之后的需要运行时改变的特性但是短期内会被解决的问题不支持含泛型方法的默认接口方法实现不支持协变返回try-catch 语句中不支持 catch (T)即将泛型参数作为 catch 的异常类型不支持模块初始化器短期内不会被解决的问题不支持 COM不支持 C/CLI受限于运行时无 JIT 而无法实现的运行时动态生成代码如System.Reflection.Emit运行时动态加载程序集如Assembly.LoadFile无限泛型递归调用有人可能不理解什么叫做无限泛型递归调用我通过代码解释一下假如你编写了如下代码public void FooT()
{if (bar){FooUT();}
}
那么会导致编译器 Stack Overflow。原因是因为代码中将 UT 类型代入了 T如果是不改变泛型嵌套层数调用的话比如将 U 带入 T只需要通过 rd.xml 指定一下用到的类型即可解决但是对于前后嵌套层数不一致的情况编译器在编译时并不知道你到底会展开多少层代码NativeAOT 编译器需要在编译时展开所有的泛型并为涉及到的所有的方法和类型生成代码于是会无限的生成用于 T、UT、UUT... 的代码最终导致无法完成编译。而为什么有 JIT 的情况下不存在问题呢是因为可以根据 bar 这个条件在运行时按需产生类型和生成代码。我曾经为 ReactvieX 和 Entity Framework Core 修复过类似的问题如果想要了解详情的话可以参考Fix infinite recursive generics in CatchSchedulerFix infinite recursive genericsGUI 解决方案由于短期内不支持 COM 和 C/CLI意味着 WPF 目前无法经过 NativeAOT 编译为本机程序但是好在 WPF 的跨平台基于 Skia 自绘实现版本 Avalonia 完全不需要 COM也不包含我上述列出的已知问题因此今天就已经能够使用它开发跨平台的 UI 程序。由于 0.10.0 版本做了大量优化并引入了编译时绑定性能有极大的提升并且所有动画都以 60fps 呈现还自带一套 Fluent Design 的主题库体验非常舒适。我经过尝试之后将自己的可视化通用旅行商问题解算器应用使用 NativeAOT 编译后得到了一个 40mb 大小的应用程序无需运行时可以瞬间启动且运行时内存占用不到 20mb什么才是小而美战术后仰。左侧是一个包含接近 70 万个节点的折线图可以 60 fps 的体验其实可以更高但对于桌面 GUI 应用来说 60 fps 渲染是一个默认的设定随意滑动、缩放和跟踪点完全不带一点卡顿某 WebGL 实现的 echart 这时候早已经停止了思考。Web 解决方案自然ASP.NET Core 是支持 NativeAOT 的MVC 中的 View 暂时除外而 Entity Framework Core 由于使用了含泛型的默认接口方法实现暂时不支持 NativeAOT随着 NativeAOT 编译器和库的更新会解决。至于重度依赖运行时织入 IL 的 Dapper可能永远也不会支持 NativeAOT毕竟熊掌和鱼不可兼得。当然通过 Source Generator 将动态生成代码转为静态生成代码不失为一种解决方案。不过对于 ASP.NET Core有一点需要注意该框架通过反射程序集加载 Controller因此代码路径中没有直接引用 Controller 类型的代码编译时所有的 Controller 都会被剪裁掉导致访问所有的 API 都是 404。这一个问题同样也是通过编写 rd.xml 告知编译器保留类型来解决。我将自己的一个没有使用 ORM只是使用 Microsoft.Data.Sqlite 的用于人员管理的 Web 服务经过 NativeAOT 编译得到了一个 30mb 的程序运行后瞬间就能提供服务内存占用只需要 20mb且首次请求只需要 0.7ms体验非常的棒。这意味着在云原生环境下尤其是扩容时新建节点中的应用可以在极短时间内一秒都不到启动并投入使用而不是都启动不久了还在等健康检查的响应。预热是什么不存在的总结和展望毫无疑问NativeAOT 将能极大的改善 .NET 程序的启动速度和运行性能并自带反破解属性真正做到 C# 的编写效率C 的运行效率。在 .NET 5 的今天这套工具链其实发展状况已经较为成熟了想用的话已经可以提前体验国外其实已经有使用这套工具链上线生产项目的例子了。.NET NativeAOT 目前还在不断探索各种可能性其中一个我认为比较有趣的是在 NativeAOT 编译中先将 IL 借助 RyuJIT 编译到 LLVM IR这个过程会对代码进行 IL 特有模式相关的优化然后将 LLVM IR 编译到原生二进制程序这个过程将会通过 LLVM 进行进一步的优化使得编译后的体积更小、运行时性能更强。先前的之前编译到 LLVM IR 的实验 LLILC 的问题在于直接 target 到 LLVM IR 导致 RyuJIT 针对 IL 特定模式的优化缺失。而新的实验当中RyuJIT 作为“中端”做好针对 IL 特定模式的优化后再送到 LLVM避免了该不足之处。未来 .NET NativeAOT 技术同样会被带到移动平台和浏览器WebAssembly上对于这套技术以后的发展我也会长期关注和跟进。最后希望 .NET 平台越来越好。