ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

01-11-运行时-Mono与CoreCLR-运行时谱系-代码生成与GC边界

01-11-运行时-Mono与CoreCLR-运行时谱系-代码生成与GC边界 Mono 与 CoreCLR运行时谱系、代码生成和 GC 边界系列C# 与常用数据结构源码剖析 · 运行时底层剖析阅读时间约 70 分钟前置知识CLI 元数据、JIT/AOT、追踪式 GC、泛型版本口径本文讨论的是运行时家族不把“Mono”视为单一且永远不变的实现。源码细节应绑定仓库、提交或发布版本复核。一、先拆掉一个错误前提“Mono”不是一个固定答案开发者常问“这段DictionaryTKey, TValue在 Mono 上为什么比 CoreCLR 慢”这个问题缺少至少四项信息是哪一支 Mono、哪个版本、哪个目标平台、采用 JIT 还是 AOT。缺少这些边界任何关于内联、泛型共享和 GC 的结论都可能答非所问。今天至少要区分以下几类实现名称本文中的含义典型执行方式不能直接推出的结论上游 Mono历史上的mono/mono项目及其发布物JIT、解释器、全 AOT 或混合方式不能代表某一版 Unity 内置的 fork.NET 中的 Monodotnet/runtime仓库src/mono下持续演进的运行时WebAssembly、移动平台等场景可使用 AOT、解释器或 JIT不能用十年前 Mono 的能力表概括Unity MonoUnity 集成、修改并随编辑器版本发布的 Mono 分支编辑器和部分 Player 配置使用 Mono平台支持取决于 Unity 版本不能假设与最新上游 Mono 同步CoreCLRdotnet/runtime中另一套运行时实现通常由 RyuJIT 执行也支持 ReadyToRun 等预编译机制不能把桌面 .NET 的观察直接套到 UnityIL2CPPUnity 的 C# IL 到 C 的 AOT 工具链及配套运行时构建期生成 C再由平台 C 编译器产出本机代码它不是一种 Mono 运行模式也不是 CoreCLR下图只表示谱系与构建路径的高层关系不表示这些实现共享私有布局、JIT 管线或 GC。尤其要注意IL2CPP 是从托管程序集进入 C 工具链的独立分支不是从 Mono “升级”而来。读图时应先选定一条路径再继续追问精确版本、代码生成模式和 GC 配置只说“这是 C#”或“这是 Mono”仍不足以定位真正执行代码的后端。还有一个容易忽略的维度基础类库和运行时不是同一层。ListT、DictionaryTKey,TValue的托管实现可能来自不同代际的类库而对象布局、方法调用、机器码生成与 GC 又由运行时和构建后端决定。“都叫 C#”只能保证语言层面大体相同不能保证内部布局、优化结果或停顿行为相同。因此本文不是一场简单的胜负评选。它建立一套分析框架先确定实现谱系再查看元数据如何物化为运行时类型随后分析代码生成、泛型、GC 和互操作最后把差异映射到常用数据结构。二、历史坐标相同标准不同工程路径Mono 项目在 2001 年启动目标是在多平台实现 ECMA CLI 与 C# 生态Mono 1.0 于 2004 年发布。它后来经历 Novell、Xamarin 与 Microsoft 等阶段并长期服务于 Linux、移动端、游戏和嵌入式场景。2020 年前后Mono 的主要开发逐步进入dotnet/runtime这并不意味着历史 Mono、现代 .NET Mono 与所有下游 fork 在某一刻自动变成了同一份二进制。CoreCLR 源自微软 .NET Framework CLR 的工程积累在开源 .NET 时代持续演化。它和 Mono 都执行符合 CLI 约定的托管程序却有独立的类型加载器、执行引擎、GC、诊断系统和平台适配层。二者在dotnet/runtime共存更像共享仓库、类库与部分基础设施的两个运行时而不是一个运行时的“完整版”和“精简版”。Unity 的历史路径又不同。Unity 选择的托管环境要同时面对编辑器热迭代、主机平台禁止 JIT、移动包体、C 原生插件、引擎对象生命周期和多平台认证。Mono 后端便于快速脚本迭代IL2CPP 通过 AOT 满足 iOS、主机等平台约束也成为许多 Player 平台的常用后端。具体默认值和可选后端会随 Unity 大版本及目标平台变化所以“Unity 全用 Mono”与“Unity 已完全不用 Mono”都不成立。阅读任何测试报告时应先记录如下指纹运行时/后端CoreCLR、上游 Mono、Unity Mono 或 IL2CPP 精确版本.NET SDK/runtime 版本或 Unity 编辑器完整版本 构建方式Debug/ReleaseJIT/AOT/解释器脚本后端 平台操作系统、CPU 架构、设备型号 GC 配置工作站/服务器、是否增量、堆大小和相关开关 代码与数据源码提交、输入分布、预热、采样方法没有这些信息的“快 3 倍”既无法复现也无法用于选型。本文不会给出脱离环境的性能倍数。三、类型元数据怎样变成可执行对象3.1 从 CLI 元数据开始C# 编译器通常先产生程序集、CLI 元数据和 IL。元数据表中的TypeDef、TypeRef、MethodDef、Field、GenericParam等记录是持久化描述不等于运行时最终使用的数据结构。运行时还要完成程序集解析、类型加载、继承关系验证、接口展开、字段布局、泛型实例化、静态字段分配和方法入口建立。CoreCLR 源码中经常遇到MethodTable、EEClass、MethodDesc、FieldDesc与类型句柄等概念。粗略地说MethodTable承载对象方法分派与大量热路径需要的信息EEClass等结构保存其他类型描述MethodDesc表示可调用方法及其入口状态。但这些结构会随版本、类型种类和加载阶段变化不能仅凭名字推断所有字段都固定存在。Mono 源码中常见MonoClass、MonoType、MonoMethod、MonoClassField与MonoVTable。MonoClass描述已加载类型MonoType也用于表达签名中的类型形态MonoVTable则包含与运行上下文、静态数据和方法分派有关的状态。这里同样存在延迟初始化、缓存与按需构建。把差异概括成“MonoClass 是扁平单体CoreCLR 是先进的热冷分离”是不严谨的。首先两个实现都在长期调整布局和缓存其次MonoClass也会引用其他结构并延迟填充信息最后真正成本取决于访问路径、类型数量、泛型实例和版本而不是一张概念图的框数。3.2 类型加载不是一次完成的开关运行时通常按需求推进类型状态。例如反射只读取名称不一定要求所有方法都生成机器码首次访问静态字段可能触发类型初始化首次虚调用可能需要建立或修补分派信息。下列动作的时机和实现可能不同解析程序集引用与类型标记。验证父类、接口与泛型约束。计算实例字段布局、对齐和 GC 引用描述。建立虚方法槽与接口分派数据。为特定运行上下文准备静态字段。创建反射对象或方法调用入口。这解释了为何“第一次执行”常混合了加载、初始化、JIT/AOT 桩解析和缓存填充成本。数据结构基准若不区分冷启动与稳态测到的就不只是TryGetValue或Add。3.3 对象布局的共同约束与实现差异托管引用指向的对象通常含有运行时头部和类型关联信息后面才是实例字段数组还要保存长度等信息。具体头部大小、字段对齐、字符串布局、压缩引用策略都依赖运行时、架构和配置。不要把某次在 x64 CoreCLR 上测得的对象大小写成 CLI 保证也不要假设 Mono 与 IL2CPP 一致。对于数据结构真正值得追问的是Entry[]中值类型字段是否内联元素步长是多少节点式结构每个节点增加多少对象头、引用与对齐成本数组是否跨入某个运行时的大对象策略范围引用字段更新是否经过写屏障反射、裁剪和 AOT 是否要求额外保留元数据。这些问题可以测量且比“类型系统更扁平所以更慢”更接近因果关系。四、执行引擎不能再用“Mini JIT 没有 SSA”概括 Mono4.1 Mono 的执行路径历史 Mono 的 JIT 编译器常被称为 Mini。它将 IL 转换为内部表示执行控制流、局部优化、寄存器分配和机器码生成。不同年代、架构和配置拥有不同优化集合Mono 也存在 SSA 相关变换、内联、常量传播、死代码消除、循环优化与边界检查优化等能力并可在某些配置中使用 LLVM 后端。因此“Mono 没有 SSA”“完全不会消除边界检查”都是错误的绝对化描述。但存在某项优化并不意味着它与某版 RyuJIT 的实现范围、启发式和结果相同。优化能否触发取决于调用是否可见、异常语义、别名分析、循环形态、泛型共享方式、AOT 限制、编译时间预算以及目标 CPU。正确方法是对目标版本查看生成物而非用功能勾选表预测性能。Mono 还支持解释执行与 AOT。全 AOT 平台不能在运行时随意生成新机器码运行时必须提前准备泛型、调用桩和元数据混合模式则可能让 AOT 代码与解释器协作。.NET中的 Mono尤其 WebAssembly 与移动场景功能组合仍在演化不能照搬 Unity Mono 的旧经验。4.2 CoreCLR 与 RyuJITCoreCLR 通常使用 RyuJIT。现代 .NET 可采用分层编译方法先以较低编译成本获得代码热点方法随后以更充分的优化重新编译动态 PGO 可利用实际类型和分支信息辅助优化。ReadyToRun 可减少一部分启动期 JIT 工作但代码仍可能因分层编译等机制被替换。同样不能把“CoreCLR 有 PGO”简化为“每次调用都得到最优机器码”。短命进程可能尚未进入优化层级AOT 或关闭相关特性的部署会不同共享泛型、复杂控制流和动态加载仍会限制优化。运行时版本升级也会改变启发式。4.3 IL2CPP 是另一条编译链IL2CPP 读取托管程序集将 IL 和元数据转换为 C 表示再交给 Clang、MSVC 等平台工具链编译链接。最终程序仍需要 IL2CPP 配套运行时提供类型信息、异常、线程、反射、GC 与托管/原生互操作支持。所以“IL2CPP 使用 Mono JIT”是类别错误。它没有在 Player 中把同一方法交给 Mini JIT另一方面“转成 C 就天然比 JIT 快”也不成立。AOT 能利用平台编译器并避免运行期 JIT却失去基于真实运行数据进行动态再编译的一部分机会还要处理代码膨胀、泛型共享、裁剪和构建时间。最终结果必须以目标设备上的正确性与性能测试为准。五、泛型数据结构差异最容易被放大的地方ListT和DictionaryTKey,TValue大量依赖泛型。运行时既希望为具体类型生成高质量代码又不能为每个实例化无限复制机器码。CoreCLR 通常可让多种引用类型实例共享规范化泛型代码并通过运行时上下文获取类型相关信息值类型因为布局与操作不同往往需要更具体的实例化代码但实现仍可能采用不同层次的共享。Mono 有自己的 generic sharing 机制也发展过面向值类型共享的gsharedvt等方案。可用范围取决于 Mono 分支、版本、执行模式与 ABI。IL2CPP 同样必须在“生成具体代码”和“共享泛型实现”之间折中。Unity 版本和构建选项会影响泛型共享策略。AOT 最棘手的问题不是语法上的T而是运行时才通过反射组合、且构建期无法发现的封闭泛型实例。代码裁剪还可能移除静态分析认为不可达的类型或方法。以DictionaryMyKey, Enemy为例应逐层检查MyKey是引用类型还是值类型大小与对齐如何比较器是默认比较器、结构体比较器还是接口后分派GetHashCode与Equals能否内联是否发生装箱AOT 是否生成所需的封闭泛型与比较调用字典扩容时复制的Entry总字节量Enemy是普通托管对象还是具有 Unity 原生侧身份的包装对象。泛型性能不能只凭“值类型会特化”下结论。一个 64 字节键即使避免装箱也可能因复制、缓存占用和哈希成本输给紧凑整数 ID。六、GC 边界先区分算法再谈停顿6.1 CoreCLR GC 的准确说法CoreCLR GC 是精确追踪式、分代式 GC通常将对象划入小对象堆的第 0、1、2 代并另有大对象堆和固定对象堆等区域。它通过运行时生成或维护的 GC 信息识别引用而不是把任意“看起来像地址”的位模式都当作托管引用。“CoreCLR GC 总会压缩整个堆”仍然不对。小对象堆回收常包含压缩但具体行为受代、模式和启发式影响大对象堆通常采用不同策略可通过公开配置请求特定压缩行为固定对象本身也会约束移动。工作站 GC、服务器 GC、后台回收、容器内存限制等配置会显著改变吞吐、并行度和停顿表现。分代的核心依据是多数对象生存期短。年轻代回收无需每次遍历所有老对象但老到年轻的引用必须借助写屏障和卡表等机制记录。因此给大型老数组持续写入年轻对象引用不是“零成本”它可能产生屏障和后续扫描成本。6.2 上游 MonoBoehm 与 SGen 不能混为一谈历史 Mono 曾支持 Boehm-Demers-Weiser GC也开发了 SGen。Boehm 通常按保守式方式识别某些根一个非引用数值若形似堆地址可能让对象被保守地保留更久。这应称为保守保留不宜直接写成必然、永久的“内存泄漏”。SGen 是 Mono 的另一套 GC具有分代设计并能利用运行时掌握的托管对象布局进行精确堆扫描栈、寄存器、互操作边界或具体模式的处理仍应按实现与平台查证。SGen 包含 nursery、major collector、写屏障等机制其并发、压缩或 major collector 策略也会随版本和配置变化。因此“Mono GC 就是保守、非分代、不压缩”只能描述某个采用特定收集器的配置不能概括 Mono 家族。6.3 Unity Mono 与 IL2CPP共享误解最多的边界Unity 官方长期把其托管内存管理描述为使用 Boehm-Demers-Weiser GC并强调其非分代、非压缩特征增量 GC 的作用是把标记等工作拆到多个时间片以降低单次停顿尖峰它不会因此自动变成分代或压缩 GC。不同 Unity 大版本、平台和后端仍应以对应版本手册为准。特别要纠正不能声称“IL2CPP GC 是分代、精确、支持基本压缩”。在大量已发布 Unity 版本中Mono 与 IL2CPP 后端都接入 Unity 所采用的 Boehm 系 GC 路径IL2CPP 的 AOT 代码生成方式并不自动决定 GC 算法。未来若 Unity 更换实现也必须以明确版本证据更新结论而不是从“C 编译”推导“分代 GC”。Unity 的UnityEngine.Object还具有托管包装与原生对象双重生命期。被销毁的原生对象对应的托管包装可能表现出 Unity 定义的特殊null比较语义。字典或列表仍持有包装引用时GC 无法凭业务意义自动移除条目。这属于引擎对象模型问题不是保守 GC 的“误判”。6.4 数据结构真正施加给 GC 的压力以一个不断扩容的ListNode为例扩容会分配更大的引用数组、复制引用并让旧数组等待回收。若Node本身是对象又增加大量独立分配与引用追踪。链表则几乎每个节点一次分配局部性通常更差。Dictionary常有桶数组与条目数组删除不一定立即缩减这些后备存储。值得控制的是分配率、存活集、引用边数量、峰值容量和生命周期而不是机械执行“所有 class 改 struct”大结构体会增加数组复制和参数传递成本含引用字段的结构体仍需 GC 扫描其中引用结构体经接口调用、放入非泛型容器或转为object可能装箱对象池会把对象主动变成长寿命池过大反而扩大存活集Clear是否清空引用、是否归还数组要看具体容器实现与所有权协议预设Capacity能减少扩容却会提高常驻内存应该依据可解释的上界。优化目标应写成可测指标例如“战斗帧内托管分配接近零且池峰值不超过最近窗口需求”而非“用了对象池所以 GC 问题解决”。七、互操作、异常与线程基准之外的真实成本Mono、CoreCLR 和 IL2CPP 都要把托管世界连接到操作系统或原生库但入口机制不同。P/Invoke 需要封送参数、解析符号并跨越托管/原生边界反向回调还要保证委托和目标对象的生命周期。字符串编码、数组复制、结构体布局和调用约定错误可能比容器本身的操作昂贵得多。在 Unity IL2CPP 中生成的 C 仍遵守托管语义但最终 ABI、链接与平台限制参与进来。提交给原生侧长期保存的托管地址必须符合固定和生命周期规则不能因为最终产物是 C 就假设对象地址永久稳定。相反在非压缩 GC 下观察到对象通常不移动也不等于所有临时缓冲区、原生内存或未来版本都允许无契约保存地址。异常处理也可能改变热路径。一个依靠抛异常表示“查找失败”的容器封装会承担构造异常、栈展开和诊断信息等成本应使用TryGetValue一类预期分支表达正常缺失。但不要为了“避免异常”吞掉真正的不变量破坏。线程方面GC 安全点、线程挂起、原生线程附加方式都属于运行时实现。Dictionary的普通实例并不会因为运行时不同就变成线程安全。若多个线程读写应选择同步协议、不可变快照、并发容器或单写者架构并在目标后端验证内存可见性与原生插件交互。八、调试与部署可观测性也是运行时能力CoreCLR 生态通常可使用dotnet-trace、EventPipe、性能计数器、dump 与运行时诊断 API 等工具具体工具支持与事件名称受 .NET 版本和平台限制。上游或 .NET Mono 的诊断能力并非完全相同尤其 WebAssembly、移动 AOT 和沙箱环境可能限制采样、动态附加或代码生成。Unity 应优先使用与编辑器版本匹配的 Profiler、ProfilerRecorder、Memory Profiler 包、Player 日志及平台原生工具。编辑器测量包含编辑器本身、域重载、开发构建检查等噪声不能替代目标设备 Player。IL2CPP 性能剖析还需保留符号并正确映射生成代码托管调用栈、C 栈和平台采样器看到的是同一执行过程的不同切面。部署模型也影响正确性JIT 可在运行期为新发现的泛型实例生成代码受平台执行内存策略约束AOT 要在构建期尽可能闭合可达代码对反射和动态泛型更敏感裁剪可减小包体但动态访问需要注解、链接描述或显式引用IL2CPP 构建时间和生成代码规模可能随泛型实例数量增长调试构建、开发构建和发布构建的检查、优化与符号不同。因此容器封装若依赖反射创建Dictionary,的未知组合必须做 AOT Player 测试。编辑器 Mono 中运行成功只证明这条编辑器路径成功。九、源码阅读路线从入口到证据不靠二手表格源码目录会调整以下是“搜索路线”而不是永久路径承诺。先固定 tag 或 commit再在该版本中搜索类型和调用关系。9.1 CoreCLR 路线在dotnet/runtime的src/coreclr搜索MethodTable、EEClass、MethodDesc建立类型加载与方法入口概念。从src/coreclr/vm的类型加载、方法表构建、泛型字典与调用桩代码追踪一次方法解析。在src/coreclr/jit从 importer、内联决策、范围检查到 lowering 与 codegen 跟踪目标方法。在src/coreclr/gc阅读堆分代、分配上下文、卡表和收集阶段同时对照官方 GC 配置文档。回到System.Private.CoreLib中目标集合的托管源码明确哪些行为属于类库而非运行时。9.2 Mono 路线在目标 Mono 仓库或dotnet/runtime/src/mono搜索MonoClass、MonoVTable、MonoMethod的定义和初始化函数。从程序集镜像、元数据加载进入 class setup、vtable、接口偏移与泛型实例化。在mini相关目录跟踪 IL 转换、优化开关、内联、寄存器分配与架构后端不要只看文件名判断能力。若目标配置使用 SGen再从其 nursery、major collector、写屏障与线程挂起路径阅读不要把 SGen 结论套给 Boehm 配置。对 .NET Mono、历史上游 Mono 与 Unity fork 分别记录提交来源遇到同名函数也要检查实现差异。9.3 Unity 与 IL2CPP 路线Unity 引擎和 IL2CPP 并非所有实现都以同一种形式公开。可从对应 Unity 版本手册、脚本后端设置、生成的 IL2CPP C、构建日志、符号文件与可用的参考源码交叉验证。不要用最新在线手册解释旧 LTS也不要把编辑器进程的 Mono 版本当作 Player 的后端版本。阅读时建立证据卡问题DictionaryMyKey, Enemy.TryGetValue 是否发生装箱 环境Unity 具体版本 / IL2CPP / ARM64 / Release Player 托管入口目标类库 Dictionary 与 EqualityComparer 实现 生成证据生成 C、符号或反汇编中的比较调用 运行证据目标设备分配记录与 CPU 采样 结论边界只适用于上述键类型、比较器、版本和构建配置这样得到的是可复核结论而不是“听说 Mono 对泛型不好”。十、三组可复现的对照实验实验一分离冷启动与稳态目标是判断首次调用成本是否来自类型初始化、JIT 或缓存而非容器算法。static long LookupBatch(Dictionaryint, int map, int[] keys) { long sum 0; foreach (int key in keys) { if (map.TryGetValue(key, out int value)) sum value; } return sum; }先在独立进程或 Player 中记录第一次执行再进行足够但有记录的预热随后采集多批稳态数据。保证输入一致并消费返回值避免测试框架或编译器消除无效工作。结果同时报告中位数、分布、分配量和环境不能只贴最佳一次。实验二比较键设计而非比较运行时口号准备语义相同的三种键整数 ID、小型只读结构体、含字符串的复合键。分别验证Equals/GetHashCode契约、哈希分布、条目大小、查找命中率和装箱。若使用自定义比较器记录其类型和调用方式。这个实验可揭示数据布局与比较成本经常比“Mono 对 CoreCLR”更能指导业务设计。实验三比较 GC 策略下的生命周期构造两种等价工作负载一种反复创建临时节点另一种复用有上限的池。记录每秒分配、存活对象、池峰值、帧时间分位数和收集事件。在 CoreCLR 与指定 Unity Player 上分别运行但不要把收集次数直接横比因为 GC 算法和事件定义不同。最终问题是各自是否满足延迟和内存预算。实验必须先做正确性校验输出校验和一致、集合数量一致、池归还后状态被重置、没有跨帧持有已归还对象。错误但更快的程序没有比较价值。十一、场景决策表选择后端与数据结构策略场景首要约束优先验证合理策略Unity 编辑器工具迭代速度、域重载、反射编辑器所用运行时版本、重载前后静态状态接受编辑器路径特性但核心逻辑仍做 Player 测试iOS/主机 Player禁止或限制 JIT、平台认证IL2CPP AOT、裁剪、泛型实例、原生插件提前闭合反射路径保留必要元数据在真机验证长时间服务器吞吐、尾延迟、可观测性CoreCLR GC 模式、动态 PGO、容器限制依据负载选择 GC使用生产级 trace不照搬游戏帧优化WebAssembly下载体积、启动、受限线程/执行环境使用的 .NET Mono 版本、AOT/解释组合同时测启动、包体和稳态避免只优化单个循环高频实体索引稳定帧时间、内存局部性键大小、容量峰值、删除模式、分配率紧凑 ID、预估容量、批量更新是否池化由数据决定原生插件桥接ABI、生命周期、封送结构布局、字符串编码、回调保活、线程附加减少细粒度跨边界调用定义明确所有权协议运行时选择通常由平台先决定数据结构优化则仍有空间。即使无法更换 Unity 的脚本后端也能通过减小键、稳定容量、避免意外装箱、减少跨边界次数和控制对象生命周期获得确定收益。十二、审查清单看到“运行时差异”时追问什么“Mono”具体指上游 Mono、.NETMono、Unity fork还是其实指 IL2CPP结论绑定了版本、平台、架构和 JIT/AOT/解释模式吗比较的是同一份基础类库实现、同一输入与同一发布配置吗所谓缺少 SSA、内联或边界检查消除有生成代码证据吗GC 是 Boehm、SGen、CoreCLR GC还是 Unity 对应版本集成的实现“增量”是否被误写成“分代”“AOT”是否被误写成“没有运行时”对象大小和大对象阈值是实测还是把另一运行时的数字抄了过来编辑器结果是否在目标 Player、真机和发布配置中复核反射、泛型与裁剪是否在 AOT 构建中覆盖性能结论是否同时提供正确性、分配、内存峰值与尾延迟而非孤立平均值十三、总结比较实现不比较标签Mono 与 CoreCLR 共享 CLI 和大量 .NET 语义但它们不是同一套类型加载、代码生成、GC 与诊断实现。Mono 自身也包含跨年代、跨仓库、跨下游 fork 的多个谱系Unity Mono 不能代表现代.NETMonoIL2CPP 更不是 Mono 的一个开关。类型系统方面应从元数据加载、运行时结构、延迟初始化和分派路径分析不能把MonoClass粗暴称为“扁平结构”。执行引擎方面Mono 具备 SSA 相关与多种优化能力但触发范围随版本和模式变化RyuJIT 的分层编译和动态 PGO 也需要运行条件。泛型方面共享、具体化、AOT 可达性和代码规模共同决定结果。GC 方面CoreCLR 的精确分代设计、Mono 的 SGen、历史 Boehm 配置以及 Unity 长期采用的非分代非压缩 Boehm 系路径必须分开讨论。增量回收只是调度工作并不等于分代或压缩IL2CPP 的 C AOT 也不会自动带来“精确分代压缩 GC”。对数据结构开发者最可靠的工作流是固定版本与后端确认类库源码查看生成代码记录分配与存活集在目标设备复现实验然后把结论限制在证据覆盖的边界内。运行时知识的价值不是背诵谁更快而是知道该在哪一层寻找真正的成本。下一篇C# 诞生记Anders Hejlsberg 为什么要创造 C#
返回列表