ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

.NET 运行时 Mono 泛型共享(Generic Sharing)设计解析:RGCTX、MRGCTX 与惰性获取 Trampoline 全解

.NET 运行时 Mono 泛型共享(Generic Sharing)设计解析:RGCTX、MRGCTX 与惰性获取 Trampoline 全解 .NET 运行时 Mono 泛型共享Generic Sharing设计解析RGCTX、MRGCTX 与惰性获取 Trampoline 全解【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime泛型共享是 Mono 运行时在保持泛型代码语义的前提下让Listint与Liststring共用同一份 JIT 编译产物的核心技术它直接决定了 .NET 应用在内存占用与启动性能上的表现。本文以 docs/design/mono/web/generic-sharing.md 为骨架结合当前仓库中 mini-generic-sharing.c、mini-trampolines.c 及各架构tramp-arch.c的真实实现系统讲解 RGCTX/MRGCTX 的传递机制、寄存器约定、惰性获取 trampoline 的工作流程以及如何在栈帧中回溯泛型信息。读完你将理解 Mono 泛型共享的完整数据流并能在阅读 JIT 与 trampoline 源码时快速定位相关实现。什么是泛型共享为什么需要它在 .NET 的泛型实现中Listint和Liststring是同一个泛型类型定义generic type definition的两个不同实例化instantiation。如果 JIT 为每个实例化都生成一份独立的本机代码那么当泛型参数的数量巨大时例如DictionaryKeyValuePairTKey, TValue, ListMyType这类复杂嵌套代码量会呈组合爆炸式增长内存消耗难以承受。Mono 采用的做法是泛型共享generic sharing让多个兼容的实例化共用同一份编译后的代码。文档明确指出约束constraint决定了共享的范围如果约束是object则该泛型参数可以匹配所有引用类型如果约束是int则可以匹配int以及所有基类型为int的枚举依此类推。因此Liststring和Listobject可以共享代码而Listint与Listlong是否共享则取决于它们是否满足相同的底层约束。这份共享的代码在运行时**必须能拿到当前正在处理的具体类型是什么**这一信息——这正是 RGCTX 与 MRGCTX 存在的意义。RGCTX 与 MRGCTX泛型上下文信息的载体共享代码本身不携带类型参数它依赖一个运行时的泛型上下文来回答我是谁的问题。Mono 中该上下文分两种形态RGCTXRuntime Generic Context服务于非泛型方法non-generic methods挂在类的 VTable 上。当共享代码属于某个具体类如引用类型的实例方法时通过this-vtable-rgctx即可访问。MRGCTXMethod Runtime Generic Context服务于泛型方法generic methods它除了包含类型上下文还额外携带方法本身的泛型实参method_inst。从当前仓库的布局看RGCTX 数组的每个元素都是gpointer指针宽度数组按 2 的幂次增长详见后文 mono_class_rgctx_get_array_size 一节并通过next指针链接成链表结构。MRGCTX 的特殊首数组布局文档给出了 MRGCTX 的结构要点MRGCTX 以一个MonoMethodRuntimeGenericContext结构开头该结构包含指向类 VTable 的指针和指向方法泛型实参MonoGenericInst的指针。与 RGCTX 不同的是MRGCTX 的第一个数组前两个槽位被特殊占用-------------------------------------------------------- | vtable | method_inst | next | slot 0 | slot 1 | slot 2 | -------------------------|------------------------------ . .第 1 个槽类 VTable 指针第 2 个槽方法的method_inst泛型实参实例第 3 个槽next链接指针第 4 个槽起才是真正的 slot 0。而其余数组的布局与 RGCTX 数组一致next | slot | slot | ...只是长度可能不同。这种区分在惰性获取 trampoline 中通过编码的槽号来判别。RGCTX 数组布局链式增长RGCTX 本身是一个由多个定长数组通过next指针串联而成的链表。文档给出的结构示意--------------------------------- | next | slot 0 | slot 1 | slot 2 | --|------------------------------ | | --------------------------------- -| next | slot 3 | slot 4 | slot 5 .... --|------------------------------ | | ------------------------------------ -| next | slot 10 | slot 11 | slot 12 .... --|--------------------------------- . . .每个数组的next指针可能为 NULL表示尚未分配后续数组每个槽位也可能为 NULL表示该槽的内容尚未惰性填充详见下文懒加载机制。RGCTX 寄存器泛型上下文的传递约定共享代码在 JIT 编译后是一份机器码但不同类型/方法的调用点需要把各自的上下文塞进这份共享代码。Mono 通过一个专用寄存器MONO_ARCH_RGCTX_REG完成传递。根据被调用方法的形态传递内容分三种情况见文档第 1–3 条引用类型的非泛型非静态方法RGCTX 随this参数走——this-vtable-rgctx即可取得不需要额外寄存器引用类型的非泛型静态方法、值类型的非泛型方法需要在MONO_ARCH_RGCTX_REG寄存器中传入调用者类的 VTable 指针泛型方法需要在MONO_ARCH_RGCTX_REG寄存器中传入MRGCTX 指针。文档强调两个关键约束MONO_ARCH_RGCTX_REG不能被 trampoline 破坏clobber——因为 trampoline 是夹在调用链中的代码若它改写了该寄存器被调用的共享方法就拿不到正确的上下文了在所有平台上MONO_ARCH_RGCTX_REG与 IMT 寄存器是同一个寄存器。理由是RGCTX 寄存器用于向已知的具体方法传递信息而 IMT 寄存器用于被调用方法未知的间接调用同一处调用不会同时用到两者因此共用寄存器不会冲突。此外文档给出寄存器生命周期的精确定义从调用点加载它的位置开始到被调用方 prologue 将其丢弃或存入局部变量为止。这提示各架构的 prologue 必须小心处理避免在调用约定寄存器之外额外地破坏状态。为什么不用普通参数传递 RGCTX文档给出了否定的理由调用方并不总能知道被调方是否需要 RGCTX 参数。被调方可能是非共享方法甚至是非泛型方法例如Actionint最终可能调到foo(int)也可能调到fooT(T)以int实例化若把 RGCTX 设计成常规参数调用约定的处理会变得异常复杂。文档还建议避免使用参数传递寄存器作为 RGCTX 寄存器否则会显著增加调用约定相关代码的复杂度。间接调用下的 rgctx trampoline对于直接调用调用方在编译期就知道被调方的形态可以自行加载正确的寄存器值。但间接调用虚调用、委托、runtime invoke 等不同调用方在运行时才知道真正的被调方法因而不知道应该传什么 RGCTX 值。解决方式是使用rgctx trampoline由mono_create_static_rgctx_trampoline()创建调用方调用 trampolinetrampoline 把 RGCTX 设为需要的值然后跳转到真正的被调方法。这些 trampoline 被插入到间接调用的调用链中。当前仓库中该函数实现于 mini-trampolines.c并且使用了哈希表static_rgctx_trampoline_hash按方法缓存已创建的 trampoline避免重复分配见 mini-trampolines.c。方法 prologue接收并保存 RGCTX共享代码在进入方法时RGCTX 已经在MONO_ARCH_RGCTX_REG寄存器中。JIT 编译流程中mono_arch_emit_prolog必须检查MonoCompile::rgctx_var字段如果该字段被设置即本方法需要 RGCTX就在 prologue 中把寄存器内容存入局部变量从而保证后续寄存器可以被安全复用。文档以 mini-x86.c 为参考实现。从源码结构看MonoCompile结构中确实保留了rgctx_var成员见 mini.h且还关联了 DWARF 位置列表用于调试信息见 mini.h说明该局部变量的位置信息会被记录供栈回溯与调试器使用。泛型参数的底层类型处理共享代码在 JIT 编译期和运行期如何看待泛型参数文档给出的答案是共享方法中使用的泛型参数由一个MonoGenericParam表示其gshared_constraint字段指向一个MonoType该类型标识了此泛型参数被约束到的类型集合。约束为object→ 可匹配所有引用类型约束为int→ 可匹配int及基类型为int的枚举其余约束以此类推。JIT 通过mini_get_underlying_type()获取约束类型从而无需特判每个泛型参数例如被约束为引用类型的泛型参数可以像MONO_TYPE_OBJECT一样处理。该函数在仓库中被广泛调用例如 aot-compiler.c 在处理 AOT 泛型共享代码的签名时会用mini_get_underlying_type()把泛型参数规约到底层类型后再进行签名匹配与复制。这解释了共享代码为何能在不同实例化间复用只要底层约束一致类型操作就一致。(M)RGCTX 惰性获取 trampoline泛型上下文槽位如这个泛型实参对应的 vtable往往在第一次使用前并不存在。Mono 采用惰性填充策略共享代码通过RGCTX lazy fetch trampoline读取槽位若槽位尚未初始化则转入非托管代码填充后再返回。编码的槽号trampoline 的创建函数mono_arch_create_rgctx_lazy_fetch_trampoline()接收一个编码后的槽号encoded slot number。当前仓库 mini.h 中定义了完整的编解码宏#define MONO_RGCTX_SLOT_MAKE_RGCTX(i) (i) /* 普通 RGCTX 槽 */ #define MONO_RGCTX_SLOT_MAKE_MRGCTX(i) ((i) | 0x80000000) /* MRGCTX 槽最高位置 1 */ #define MONO_RGCTX_SLOT_INDEX(s) ((s) 0x7fffffff) #define MONO_RGCTX_SLOT_IS_MRGCTX(s) (((s) 0x80000000) ? TRUE : FALSE)最高位0x80000000标记该槽属于 MRGCTX其余 31 位存放槽索引。在mono_class_rgctx_get_array_size()等接口的调用点如 method-to-ir.c会依据in_mrgctx布尔值用上述宏生成对应编码。RGCTX 槽的获取伪代码以获取 slot 11位于第 2 个数组偏移 2 个槽位为例文档给出的伪代码完整还原了遍历过程; vtable ptr in r1 ; fetch RGCTX array 0 r2 *(r1 offsetof(MonoVTable, runtime_generic_context)) if r2 NULL goto unmanaged ; fetch RGCTX array 1 r2 *r2 if r2 NULL goto unmanaged ; fetch RGCTX array 2 r2 *r2 if r2 NULL goto unmanaged ; fetch slot 11 r2 *(r2 2 * sizeof (gpointer)) if r2 NULL goto unmanaged return r2 unmanaged: jump unmanaged_fetch_code要点拆解入口参数trampoline 以普通整数参数接收 VTable 指针每层数组先取next指针任一next为 NULL 都意味着后续数组尚未分配需要转非托管到达目标数组后按slot_index * sizeof(gpointer)取槽槽为 NULL 同样转非托管非托管分支会初始化 RGCTX 结构并填充槽位实际由mono_class_fill_runtime_generic_context()/mono_method_fill_runtime_generic_context()完成见 mini-trampolines.c每个数组的槽数由mono_class_rgctx_get_array_size()给出。数组大小规则RGCTX 与 MRGCTX 不同文档指出数组槽数必须从mono_class_rgctx_get_array_size()获取。当前仓库 mini-generic-sharing.c 给出了精确实现static inline int class_rgctx_array_size (int n) { return 32 n; } static inline int method_rgctx_array_size (int n) { return 6 n; } int mono_class_rgctx_get_array_size (int n, gboolean mrgctx) { g_assert (n 0 n 30); if (mrgctx) return method_rgctx_array_size (n); else return class_rgctx_array_size (n); }即RGCTX第 n 个数组有32 n个槽32、64、128……MRGCTX第 n 个数组有6 n个槽6、12、24……且第一个数组的 6 个槽中前 2 个被 vtable 与 method_inst 占用next在第 3 个槽真正的 slot 0 从第 4 个槽开始数组编号 n 必须在[0, 30)范围内否则触发断言。由此可见MRGCTX 的数组比 RGCTX 更紧凑——因为方法级泛型上下文的数量通常远小于类级上下文采用更小的增长基数可以显著节省内存。MRGCTX 获取的两个不同点MRGCTX 与 RGCTX 在惰性获取上有两点本质差异文档明确列出入口不同MRGCTX 的 trampoline 接收的不是 VTable 指针而是直接指向 MRGCTX 的指针且该指针保证非 NULL但next与槽位仍可能为 NULL首数组布局不同如前文所述MRGCTX 首数组前两个槽被 vtable 与 method_inst 占用。其余数组布局一致仅长度可能不同。非托管填充代码与返回约定惰性获取 trampoline 落入非托管分支时实际执行的是另一个 trampoline由mono_arch_create_specific_trampoline()创建、类型为MONO_TRAMPOLINE_RGCTX_LAZY_FETCH。它把槽号作为 trampoline 参数同时把 VTable/MRGCTX 指针放在MONO_ARCH_VTABLE_REG寄存器中与 generic class init trampoline 的寄存器约定一致。MONO_TRAMPOLINE_RGCTX_LAZY_FETCH在仓库中的实际处理函数为 mini-trampolines.c 中的mono_rgctx_lazy_fetch_trampoline()它从寄存器中取回参数用MONO_RGCTX_SLOT_INDEX/MONO_RGCTX_SLOT_IS_MRGCTX解码槽号然后分别调用mono_method_fill_runtime_generic_context()MRGCTX 情形或mono_class_fill_runtime_generic_context()RGCTX 情形完成填充并将结果返回。一个重要的返回约定RGCTX 惰性获取 trampoline 的返回值不是需要跳转到的代码地址而就是槽位的值本身。因此与那些返回代码地址供跳转的 trampoline如 JIT trampoline不同其通用的 trampoline 框架代码mono_arch_create_trampoline_code()生成的通用部分在它返回后必须做普通返回而不是分支跳转。从栈帧中获取泛型信息当调试器、GC 或运行时需要从一段正在执行的共享代码中反推出当前是哪个实例化时需要一套反向查询机制。文档描述了MonoJitInfo与MonoGenericJitInfo的配合若某方法以泛型共享方式编译其MonoJitInfo会设置has_generic_jit_info位此时调用mono_jit_info_get_generic_jit_info()可返回一个MonoGenericJitInfo结构。当前仓库 jit-info.h 确认MonoJitInfo中存在has_generic_jit_info:1位域jit-info.c 在编译方法时置位并在多处如 jit-info.c依据该位决定是否需要处理附加的泛型 JIT 信息。MonoGenericJitInfo在 jit-info.h 中的定义与文档完全吻合typedef struct { MonoGenericSharingContext *generic_sharing_context; int nlocs; MonoDwarfLocListEntry *locations; gint32 this_offset; guint8 this_reg; guint has_this:1; guint this_in_reg:1; } MonoGenericJitInfo;其中记录了 this/vtable/MRGCTX 变量的存放位置has_this置位才说明该信息存在this_in_reg置位变量直接存放在编号为this_reg的寄存器中this_in_reg未置位变量存放在this_reg寄存器所指地址偏移this_offset处。该变量指向的内容随方法形态不同而不同与 RGCTX 寄存器传递的三种情形一一对应引用类型的非泛型非静态方法→ 指向this对象引用类型的非泛型静态方法、值类型的非泛型方法→ 指向类的 vtable泛型方法→ 指向方法的 MRGCTX。从结构定义还可以看到MonoGenericJitInfo携带generic_sharing_context与 DWARF 位置列表locations/nlocs这些被用于调试与异常处理等场景的精确回溯。泛型共享与 AOT 的关系虽然本文主文档聚焦 JIT 场景但泛型共享机制同样深刻影响 AOTAhead-of-Time编译。从源码证据看AOT 编译器在生成共享代码时会调用mini_get_underlying_type()规约泛型参数见 aot-compiler.c与 JIT 路径保持一致的类型处理策略AOT 映像需要保存 trampoline 信息MonoTrampInfo以支持 full-aot 模式下的惰性获取相关讨论参见 trampolines.mdMonoJitInfo的from_aot位jit-info.h标记了代码来自 AOT 映像而has_generic_jit_info位jit-info.h在 AOT 加载路径中同样会被处理见 aot-compiler.c。因此可以推断AOT 编译的共享泛型代码同样依赖 RGCTX 惰性填充机制在运行期补全实例化信息这与 JIT 场景的架构是一致的。源码地图快速定位实现泛型共享相关的核心代码集中在src/mono/mono/mini/目录下关注点文件泛型共享的核心逻辑RGCTX 模板、槽编码、inflate 计算、数组大小mini-generic-sharing.c跨平台 trampoline 框架与 C 侧处理函数mono_rgctx_lazy_fetch_trampoline、mono_create_static_rgctx_trampolinemini-trampolines.c各架构 trampoline 生成amd64 / x86 / arm / arm64 / ppc / riscv / s390x / wasmtramp-amd64.c、tramp-x86.c、tramp-arm.c、tramp-arm64.c、tramp-ppc.c、tramp-riscv.c、tramp-s390x.c、tramp-wasm.c各架构 JIT 后端prologue 保存 rgctx_var、寄存器约定mini-x86.c 等mini-arch.cMonoJitInfo/MonoGenericJitInfo结构定义jit-info.hJIT 信息管理has_generic_jit_info置位与读取jit-info.c槽编解码宏mini.h泛型与 trampoline 的配套设计文档generics.md、trampolines.md、gsharedvt.md泛型共享还与其他 Mono 设计文档紧密关联泛型实例的表示与规范化见 generics.md其中说明实例化由MonoGenericClass/MonoMethodInflated表示、通过 metadata.c 中的缓存规范化trampoline 的通用框架通用部分 特定部分、mono_arch_create_specific_trampoline()见 trampolines.md而针对值类型参数无法共享的特例gsharedvt则是泛型共享的姊妹机制见 gsharedvt.md。总结Mono 泛型共享是一套精巧的以空间换上下文、以惰性换启动速度的设计一份代码多种实例化通过gshared_constraint把泛型参数规约到底层约束类型约束一致的实例化共享同一份机器码专用寄存器传上下文MONO_ARCH_RGCTX_REG在三种方法形态下分别传递this携带的 RGCTX、VTable 指针或 MRGCTX并通过 rgctx trampoline 解决间接调用的上下文注入问题惰性填充控制内存(M)RGCTX 以next链接的数组链表组织槽位首次访问时才通过 lazy fetch trampoline 转入非托管代码填充数组按32 nRGCTX与6 nMRGCTX的规则扩容可回溯MonoGenericJitInfo记录 this/vtable/MRGCTX 变量在寄存器或栈上的位置支撑调试、GC 与异常处理的泛型信息还原。理解这套机制是深入阅读 Mono JIT 后端mini-arch.c、trampoline 生成tramp-arch.c以及 AOT 共享代码路径的一把钥匙。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表