跳到主要内容

ZLua Mono 热点优化报告(对照 xLua 源码核实版)

:::info 谁该读本文 关心 Editor 性能、对比 xLua CodeEmit 路径的实现者与贡献者。 游戏业务优化见 最佳实践;Player 性能目标见 Il2Cpp 架构。 :::

要点摘要:

  1. ZLua Editor fast path 对标 xLua CodeEmit 档(档 B),非预生成 Wrap 档 A
  2. 方法桥接用 Expression 编译,避免 MethodInfo.Invoke 热路径
  3. 字段/属性经元表 getter;P0–P1 已优化直连,仍有 tostring key GC 热点
  4. Delegate 回调已 typed push/pop,无 object[] 装箱链
  5. Il2Cpp Player 优化在 C++ 层单独规划,本文主要针对 Mono Editor

快速入口: 调用路径概览 · 路线图


依据 xLua 源码与 ZLua 当前实现(P1–P7 + Phase 1 元表直连)逐项核对。

结论: ZLua fast path 应对标 xLua Editor CodeEmit 档(档 B);全量预生成 Wrap(档 A)为长期目标。P0 已完成(§5);P1 · 4.5–4.7 已完成(§6)。


0. 核实结论:常见误解修正

先前说法对照 xLua 源码后
xLua 一律「预生成 Wrap + 直接调用、无反射」不准确。xLua 有 三档路径(§1)
xLua 重载分派远优于 ZLua部分准确。生成/CodeEmit 与 ZLua 类似;ReflectionWrap 也是 Check 链 + Invoke
ZLua 方法调用明显弱于 xLua需分场景。Editor lazy 路径下 ZLua ≈ xLua CodeEmit 档
xLua 字段访问总是 offset 直读不准确。生成 Wrap 直读;ReflectionWrap 用 field.GetValue
xLua Delegate 全无 object[]不准确MethodWrapsCache 仍用 object[] + Invoke;仅 DelegateBridge 生成为 typed push

1. xLua 三档调用路径

档 A:预生成 Wrap([LuaCallCSharp] + Generator)

  • 模板 LuaClassWrap.tpl.txt:直接 gen_to_be_invoked.Method(...) / typed push/pop
  • __index 由 libxlua gen_obj_indexer(C)组合 method 表 + getter 表

档 B:Editor 运行时 CodeEmit(TryDelayWrapLoaderEmitTypeWrap

  • IL Call / Callvirt / Newobj,无 MethodInfo.Invoke
  • 与 ZLua Expression.Compile 架构同级

档 C:ReflectionWrap / MethodWrapsCache

  • OverloadMethodWrapobject[] + Invoke + PushAny
  • 单 overload 无默认参数时可跳过 Check 链

对比含义: ZLua fast path 对标档 B;档 C 下 ZLua 通常 ≥ xLua


2. 核心热点核实摘要

热点vs xLua 档 B主要差距
Lua→C# 单方法85%–95%EnsureConvertible、返回值 boxing、scope 开销
Lua→C# 重载85%–92%同 argc+lua_type 重载仍走 CanConvert 链
字段/属性读70%–85%P0 已挂 metatable getter;仍经 __index C# 回调 + tostring key(GC 热点,见 §3 · 4.8)
构造函数40%–55%仍 Invoke stub
C#→Lua Invoke92%–100%error handler 已缓存 registry ref
Delegate→Lua75%–90%P1 已 typed push/pop,无 object[]

3. 优化方案与优先级

P0(已完成)

ID内容参考 xLua
4.1构造函数 fast path 直连CodeEmit Newobj
4.2字段/属性 getter 挂 instance metatableobj_getter + obj_indexer
4.3InstanceIndex 去双次 GCHandlegetter 内单次 PopTarget
4.4编译桥 typed pop/push(校验与直读分离)fixPush / GetPushStatement

P1

  • 4.5 Delegate 桥去 object[] ✅
  • 4.6 重载 (argc, typeMask) 快速表 ✅
  • 4.7 C#→Lua 缓存 error handler ref ✅
  • 4.8 obj_indexer 统一模型 + 成员访问零 C# string key — 规格见 ../meta-table-spec.md(Mono);VM 远期见 ../vm-index-spec.md

4.8 obj_indexer 统一模型 + 零 C# string key

问题: P0 虽将 getter/setter 挂到 instance metatable,但 userdata 成员读写仍走 C# __index / __newindex。每次访问在 LuaManagerObject.InstanceIndex 等路径上执行 LuaDllExtension.tostring(L, 2)(UTF-8 → 托管 string,GC)+ Dictionary<FieldKey/PropertyKey> 等 C# string 查表。高频 obj.field 场景下开销显著。

原则: hot path 禁止 lua_tostring → C# string → 字典查找;成员分派应尽量在 Lua/C 层完成,对标 xLua gen_obj_indexer(档 A)。

涉及文件: LuaManagerObject.csInstanceIndex / InstanceNewIndex / StaticTypeIndex / StaticTypeNewIndex)、TypeFieldRegistration.csTypePropertyRegistration.cs、注册期 metatable 构建逻辑;阶段 C 可选 native zlua_obj_indexer

阶段内容预期收益
ALua 栈 key 直查去掉 hot path 每次 GC string + 字典查表
B注册期 Lua string ref 缓存动态/反射 key 首次 miss 后不再重复 alloc
C(P1)__index 模型对齐 xLua已注册成员可能完全不经 C# 回调

阶段 A — Lua 栈 key 直查(必做)

  • InstanceIndex / InstanceNewIndex / StaticTypeIndex / StaticTypeNewIndex:去掉首行 tostringlua_getmetatablelua_pushvalue(L, keyIdx) + lua_rawget(mt) 查成员。
  • 新增 RawGetFieldByStackKey(luaState, mt, keyStackIndex),替代 RawGetField(luaState, mt, cSharpString)(后者会把 C# string 再 push 进 Lua)。
  • 命中 metatable 上的 compiled bridge 后直接 lua_call,不再 IsInstanceFieldGetter(typeId, string)
  • miss 路径(反射、GetField(key) 等)才允许 tostringlua_tolstring + 按需 intern。

阶段 B — 注册期 Lua string ref 缓存

  • 注册成员时在 Lua 侧 lua_pushliteral + luaL_ref 存 string ref;metatable key 用 ref push(lua_rawgeti / registry ref)。
  • 维护 (typeId, lua_string_ptr/hash) → memberId 弱缓存;动态 key 首次 miss 后复用,避免重复 marshal。

阶段 C — __index / __newindex 模型对齐 xLua(P1)

  • 注册期 TypeMemberLuaIndexer 为每个 type 生成 Lua __index/__newindex;getters/setters 作 closure upvalue。
  • 已注册成员查找在 Lua VM 内 O(1) rawget + call 完成;C# 回调仅 unknown key fallback。
  • 可选后续:native zlua_obj_indexer(Mono/Il2Cpp 一致,进一步减少 closure 开销)。完整规格见 ../vm-index-spec.md

验收标准

  • 已注册成员 hot path 无 LuaDllExtension.tostring / 无 C# string 字典查表(Profiler 可证)。
  • 现有 marshal / 成员访问 / 只读属性 / 重载测试全绿;Mono 与 Il2Cpp 行为一致。
  • 阶段 C 完成后,字段/属性读热点对标 xLua 档 A/B 的 indexer 模型。

与 P0 · 4.2 关系: 4.2 完成「成员桥挂 metatable」;4.8 完成「访问路径不再 marshal key 到 C#」及 indexer 模型统一。

P2

  • 4.9 Editor 预生成 Wrap
  • 4.10 ObjectPool 替代 GCHandle
  • 4.11 Reflection.Emit 可选
  • 4.12 Benchmark 基线

4. 实施路线

  1. Sprint 1(P0): 4.3 → 4.4 → 4.2 → 4.1 ✅
  2. Sprint 2(P1): 4.5–4.8 ✅
  3. Sprint 3: 预生成 + Benchmark

5. P0 实施记录

状态说明
4.3 InstanceIndex 顺序✅ 完成fast path 先 TryGet/TryInvoke,fallback 才 TryGetUserDataTarget
4.4 typed pop/push✅ 完成校验与 typed pop 分离EnsureArgumentTypeCanConvertArgumentValue)保留类型校验;校验通过后 Pop* 直读栈,避免 ReadValue boxing;返回值 typed push
4.2 metatable getter 绑定✅ 完成BindInstanceAccessorsToMetatable + InstanceIndex 直连 invoke
4.1 构造函数 fast path✅ 完成LuaToCSharpConstructorBridgeFactory + 重载 dispatch + 元表直连

P0 实施过程中的 Bugfix

问题根因修复
AmbiguousMatchExceptionValidateExactArgCountGetMethod 未指定 types 参数LuaToCSharpMethodBridgeFactory.csLuaToCSharpConstructorBridgeFactory.cs 补全 types
instance metatable missing(2 个 marshal 测试)BindInstanceAccessorsToMetatableRawGetFieldPubliclua_pop,栈泄漏导致 __instance_mt 被写成 nilTypeFieldRegistration.csTypePropertyRegistration.cs 每次 RawGetFieldpop
全量测试 crash(只读属性赋值)LuaCallbackBoundary.Throw + catch + lua_error 混用 longjmpTrySetInstanceProperty/Field 等改为直接 LuaDllExtension.error;新增 AssignInstanceProperty
重载 dispatch crashThrowNoOverload 返回 void,invoke 返回 int,分支类型不一致LuaToCSharpOverloadDispatchFactory.csLuaToCSharpConstructorOverloadDispatchFactory.csThrowNoOverload 改为 return 0
类型校验回归(4 个 expect_errorP0 初版去掉 Pop* 校验后 lua_toboolean/tostring 静默接受错误类型LuaToCSharpBridgeMarshaling.cs 新增 EnsureArgumentType,所有 Pop* 先校验再直读栈

新增/修改文件

  • Docs/OPTIMIZATION.md — 本报告
  • Bridges/LuaToCSharpBridgeExpressionBuilder.cs — 共享 typed pop/push Expression
  • Bridges/LuaToCSharpConstructorBridgeFactory.cs — ctor 编译桥(argStart 1/2)
  • Bridges/LuaToCSharpConstructorOverloadDispatchFactory.cs — ctor 重载分派
  • TypeMethodRegistration.cs — ctor fast invoker、PushCompiledBridge
  • TypeFieldRegistration.cs / TypePropertyRegistration.cs — metatable 绑定
  • LuaManagerObject.cs — InstanceIndex/InstanceNewIndex 优化
  • LuaToCSharpBridgeMarshaling.csEnsureArgumentType + fast path typed pop

6. P1 实施记录(4.5–4.7)

状态说明
4.5 Delegate 桥 typed push/pop✅ 完成DynamicBridgeFactory 编译期生成 typed push/pcall/pop,不再 object[] + InvokeWithArgs
4.6 重载 fast table✅ 完成静态重载 (argc, LuaDataType) O(1) 查表;实例重载走 CanConvert 链(含 this 校验)

P1 Bugfix

问题根因修复
fast table 崩溃UserData 粗粒度 key 误分派 + ThrowTryCatch/lua_error 混用fast path 增加 TryCanConvertArgumentsEnsureArgumentType/ValidateExactArgCountLuaDllExtension.error
Calculator.Run 类型校验失败实例重载 fast path 跳过 this 校验且可能误分派实例重载禁用 fast table;TryCanConvertArguments 校验 index 1 this
delegate 测试全失败innerDelegate 误含 targetParam 参数Expression.Lambda(delegateType, body, argExprs)
delegate compile 失败TryFinallyfinally 引用了仅在 try Block 内声明的 L变量提升到外层 Expression.Block 包裹整个 TryFinally
4.7 error handler ref✅ 完成LuaEnv.EnsureErrorHandlerRef + PushErrorHandlerLuaCallInvoker / LuaInvokeBridge / RunLuaFunc 改用 lua_rawgeti

新增/修改文件

  • Bridges/CSharpToLuaBridgeExpressionBuilder.cs — C#→Lua typed push/pop Expression
  • Bridges/OverloadTypeMask.cs(argc, typeMask) 快速分派表
  • DynamicBridgeFactory.cs — 重写为 typed 编译桥
  • LuaEnv.cs — 缓存 __zluaErrorHandler registry ref
  • LuaMethod.csPushErrorHandlerToStack
  • LuaCallInvoker.cs / Bridges/LuaInvokeBridge.cs — 使用缓存 ref
  • Bridges/LuaToCSharpOverloadDispatchFactory.cs / LuaToCSharpConstructorOverloadDispatchFactory.cs — 集成 fast table

7. P1 实施记录(4.8)

状态说明
4.8 阶段 A — Lua 栈 key 直查✅ 完成fallback 路径 trytolstring + TypeMemberNameCache,避免 hot path 首行 tostring
4.8 阶段 B — 成员名缓存✅ 完成注册期预填 + miss 路径 UTF-8 hash 缓存
4.8 阶段 C — Lua VM indexer✅ 完成注册期 TypeMemberLuaIndexer 生成 Lua __index/__newindex;getters/setters 作 closure upvalue

架构要点(对齐 xLua 档 A)

  • 已注册成员 hot path 在 Lua VM 内完成,不经 C# __index 回调:rawget(getters,key)getter(obj);miss → rawget(mt,key) 取 method/enum 常量;再 miss 才调 C# fallback closure。
  • 注册期绑定bind_instance_index(getters, setters, fallback_index, fallback_newindex) / bind_static_index(mt, getters, setters, ...)TypeMemberLuaIndexer.EnsureLoaded 加载;每个 type 的 getters/setters 表作为 upvalue,运行时无 "__zlua_getters" 字符串查找。
  • C# fallback 仅 missInstanceIndex / InstanceNewIndex / StaticTypeIndex / StaticTypeNewIndex 保留反射与未注册成员逻辑;不再承担 hot path。
  • 静态/实例对称: BindStaticMetatable / BindInstanceMetatable 在 class/struct/enum 注册流程统一调用。

新增/修改文件

  • TypeMemberLuaIndexer.cs — Lua 侧 indexer 工厂与 metatable 绑定
  • LuaStackTableAccess.cs — 子表 ensure、栈 key 工具(注册期)
  • TypeMemberTableNames.cs — 子表字段名常量
  • TypeMemberNameCache.cs — fallback 成员名 intern
  • LuaDllExtension.cstrytolstring(无 alloc 探针)
  • TypeFieldRegistration.cs / TypePropertyRegistration.cs — getter/setter 子表绑定
  • LuaManagerObject.cs — fallback indexer + 注册期 Bind*Metatable

后续: Mono 落地 ../meta-table-spec.md;PUC-Rio 5.4 VM patch 见 ../vm-index-spec.md