跳到主要内容

Lua ↔ C# 交互性能对比

原则: 路径理论 + 可复现 Il2Cpp Player 基准;未覆盖项标「待测」。
环境: 正式结论 仅引用 Il2Cpp Player;Mono Editor 不代表 Player 性能。
实测: ZLua vs xLua 已有同机基准(§8);toLua / SLua 待测


1. 说明与假设

约定
数据性质§2–§7 为路径推演;§8 为实测benchmark/ 仓库对比报告)
ZLua 对比对象Il2Cpp Win64 Player / Release(实现已完成)
实测环境Unity 2022.3.62f3、Il2Cpp、Release、x64;mid = 5 轮中位数
单位ns/call(中位数);ratio = xLua_mid / ZLua_mid(>1 表示 ZLua 更快
待测toLua / SLua 同机同脚本;Delegate 稳态(P5);移动端 ARM64

必须打折的因素:

  • 互调即使快 5 倍,若脚本边界只占帧时间 2%,整帧仅省 ~1.6%。先 profiling 确认热点。
  • string、复杂 object、Listmarshal 占主导 时,方案差距缩小。
  • 首次 EnsureBinding、重载 dispatch、泛型 inflation 为 稳态外 成本。
  • 本基准 暂未测 GC Alloc(Il2Cpp Player 无可靠 GetAllocatedBytesForCurrentThread);GC 见 GC.md

2. 调用路径简图

2.1 Lua → C#(例:Demo.Add(3, 5) / obj:GetX()

xLua

lua_pcall
→ [MonoPInvokeCallback] 生成的 C# Wrap(Il2Cpp AOT)
→ LuaDLL.lua_tointeger / lua_push*(P/Invoke → libxlua)
→ 目标 C# 方法(Il2Cpp 生成体)
→ 再经 LuaDLL 写回栈
→ ObjectTranslator:userdata → pool index → object

toLua / SLua(典型)

lua_pcall
→ 生成的 C# Wrap(结构类似 xLua,具体随版本)
→ lua API(内嵌或经 tolua 绑定层)
→ 目标方法
→ 对象池 / translator 查表

ZLua(Il2Cpp Player)

lua_pcall
→ C++ MethodBridge(与 lua 同链接域)
→ lua_tointeger / lua_push*(直接 lua API,无 P/Invoke)
→ method->methodPointer(...)(Il2Cpp 生成体同级)
→ ObjectRegistry:userdata → slot(弱表缓存)

差异要点: xLua / toLua / SLua 的 Lua 栈操作常经 额外 C# / P/Invoke 边界;ZLua Player 在 C++ 一次完成 marshal + methodPointer

2.2 C# → Lua(例:[LuaInvoke] / 脚本回调)

xLua

C# 业务
→ LuaEnv / DelegateBridge
→ 多次 LuaDLL(getglobal、push、pcall、pop…)
→ libxlua → lua VM

单次调用常见 5–15 次 LuaDLL 跨界。

toLua / SLua

C# → LuaState / LuaFunction
→ 多次 native 绑定调用(次数随 API 封装而异,通常 ≥ xLua 同量级)
→ lua VM

ZLua

C# [LuaInvoke] → InternalCall(一次进 native)
→ LuaInvokeRuntime::Call(C++ 模板)
→ lua_rawgeti(funcRef) + Push* + lua_pcall + Pop

构建期解析 moduleRef / funcRef;运行时 无按名 getglobal

2.3 Lua function → C# Delegate(已绑定后)

xLuatoLua / SLuaZLua
机制C# delegate bridge + translator + LuaDLLLuaFunction → delegateLuaMethod + funcRef + closed delegate + C++ DelegateBridge
稳态C# invoke → Lua pcall同左同左,少 Wrap / LuaDLL 层

绑定完成后,三者都须「C# → Lua pcall」;ZLua 少中间层,但 string / 重对象 仍占主导。


3. 理论常数项拆解

成本项xLua (Il2Cpp)toLua / SLuaZLua (Il2Cpp 目标)备注
Lua 进入 C 回调~5–15 ns~5–15 ns~5–15 ns同在 VM 内
栈读写单次~30–150(经 LuaDLL)~20–100(视绑定层)~3–15(内联 lua API)主要差距来源
C#↔native 边界~20–80 / 次~15–60 / 次0 或 IC 1 次xLua 栈操作多次跨界
userdata → 对象~20–80~20–80~10–40均为池 + 缓存
方法分派~0–30~0–40~0–50(map / dispatch)重载两边都可能贵
调用 Il2Cpp 方法体~1–5~1–5~1–5同为生成代码
int 实例字段常经 Wrap ~150–500+经 Wrap ~100–400offset 直读 ~15–50字段差距最大
string 往返~200–2000+类似类似UTF + GC,瓶颈一致

4. 统一基准场景 P1–P6

编号统一为 P1–P6,便于四方案对照。

ID场景脚本 / C# 探针关注点
P1静态 int Add(int,int) 热循环for i=1,N do Demo.Add(1,2) end调用开销下限
P2实例无参 GetX()for i=1,N do o:GetX() end实例 + userdata 解引用
P2b实例字段 obj.xintfor i=1,N do local _=o.x end__index + 字段快路径
P3实例有参 + 简单重载两 overload 交替调用dispatch 成本
P4C#→Lua 少参[LuaInvoke] 空函数 / int 返回循环反向跨界次数
P5Delegate 已绑定后回调C# Action 循环调 Lua function稳态回调
P6string / 复杂 objectstringList<int>marshal 主导

建议: N = 1_000_000;记录 ns/callGC Alloc/帧;设备、Unity 版本、Lua 版本一并记录。


5. 分场景理论估计(ZLua vs xLua)

toLua / SLua 与 xLua 同量级(均经 C# Wrap + 多层绑定);相对 ZLua 的倍数 待测,趋势应接近 xLua 列。

符号:

  • 总耗时倍数 = T_其它 / T_ZLua(越大 ZLua 越快)
  • 互调开销削减 ≈ (T_其它 − T_native) / (T_其它 − T_函数体)

P1:静态 Add(int,int)

指标xLua 粗算ZLua 粗算总耗时倍数互调开销削减
单次~150–600 ns~30–100 ns2–5×(常见 ~3×)70%–90%

P2:实例 GetX()

指标估计
总耗时倍数3–6×
互调开销削减75%–90%

P2b:字段 obj.x

对比xLuaZLua
路径__index → Wrap → getter__index → C++ offset getter
总耗时倍数5–15×
互调开销削减85%–95%

P3:简单重载

指标估计
总耗时倍数2–4×(视 dispatch 实现)
备注四方案均可能在此场景拉开差距;待测

P4:C#→Lua

指标估计
总耗时倍数2–4×
互调开销削减60%–85%

参数越多,xLua「每次 push 都跨界」放大越明显。

P5:Delegate 回调(已绑定)

指标估计
总耗时倍数2–3×
互调开销削减50%–75%

首次绑定、泛型约束、未 codegen 签名时两边都贵。

P6:string / 复杂 object

指标估计
总耗时倍数1.1–1.8×
互调开销削减20%–50%

汇总表(Player / 轻量签名,ZLua vs xLua)

场景理论倍数实测代表(ratio)
P1 Lua→C# int 方法2–5×~1.8–2.6×(见 §8)
P2 实例 getter / 无参3–6×~2.6×(0arg ret)
P2b 读 int 字段5–15×~3.6×(field.get.int)
P4 C#→Lua 少参2–4×~1.1–1.2×(int arity2–3);数组/class 可达 5–6×
P5 Delegate 回调2–3×待测(基准未含)
P6 重 string / 对象1.1–1.8×string ~2.1×;int[] 可达 ~5×

实测结论(Il2Cpp Win64 Release,285 对齐 case): Lua→C# 全面领先(avg ~2.6×);C#→Lua 多数领先(avg ~1.7×),极简无参/arity0 有少数 xLua 更快。详见 §8。


6. Editor vs Player

后端xLua / toLua / SLuaZLua
Editorlib + Wrap,接近 PlayerMono Emit(已完成);性能结论仍以 Il2Cpp Player 为准
PlayerIl2Cpp + WrapIl2Cpp native 桥,设计目标性能在此体现

规则: 性能结论 仅引用 Il2Cpp Player;Editor 只用于功能开发。


7. 为何 ZLua Player 能更快(根因)

根因说明
消灭 libxlua / 多层 P/Invoke 往返栈操作不再每次跨界
消灭生成 C# Wrap 层(Lua→C#)C++ 一次 marshal + methodPointer
C#→Lua 单次 InternalCall非 C# 循环调 LuaDLL
字段 / 无参属性快路径Il2Cpp offset + methodPointer 同级
签名复用 stub控制体积同时避免 per-member C# 委托链(见 BRIDGE.md

不是单一黑科技 10 倍,而是 减少重复的 native↔managed↔native 折返


8. 实测(ZLua vs xLua,Il2Cpp Player)

8.1 数据来源

报告benchmark/reports/comparison_20260722_201933.md
ZLua CSVbenchmark/results/zlua_20260722_201933.csv
xLua CSVbenchmark/results/xlua_20260716_121413.csv
Unity / 后端2022.3.62f3 / Il2Cpp / Release / Win64
统计每 case 预热后 5 轮,取 mid ns/op;ratio = xLua / ZLua
范围对齐 case:cs2lua 54 + lua2cs 231;未含 Delegate(P5)、toLua、SLua

8.2 方向汇总

directioncasesZLua 更快xLua 更快avg ratio (xLua/ZLua)
lua2cs(Lua→C#)23123102.622
cs2lua(C#→Lua)544851.660
合计2852795

ratio > 1 → ZLua 更快;ratio < 1 → xLua 更快。

8.3 与 P1–P6 对齐的代表 case

单位:ns/call(mid)。toLua / SLua 列仍 待测

场景代表 case_idZLuaxLuaratio备注
P1 Lua→C# int 方法lua2cs.instance.method.int.3arg.ret41.6973.221.76三参 + 返回
P1 无参方法lua2cs.instance.method.0arg.ret22.5658.822.61调用下限附近
P2 实例 prop get intlua2cs.instance.prop.get.int22.0478.763.57getter
P2b 实例 field get intlua2cs.instance.field.get.int21.7978.353.60字段快路径
P2b field set Vector3lua2cs.instance.field.set.Vector321.1089.004.22值类型写
P4 C#→Lua int 少参cs2lua.int.arity2.ret35.1039.101.11差距较小
P4 C#→Lua int 三参cs2lua.int.arity3.ret34.9040.101.15
P4 C#→Lua classcs2lua.class.3arg.ret93.00484.005.20引用类型放大
P4 C#→Lua int[]cs2lua.int[].3arg.ret90.00602.006.69本轮最大领先之一
P5 Delegate待测基准未覆盖
P6 string Lua→C#lua2cs.instance.method.string.3arg.ret88.80189.402.13marshal 仍占主导但 ZLua 领先
P6 int[] Lua→C#lua2cs.instance.method.int[].3arg.void49.20263.805.36

8.4 xLua 更快的 case(仅 5 个,均 cs2lua 极简)

case_idZLuaxLuaratio
cs2lua.int.arity0.void36.8028.400.772
cs2lua.int.arity0.ret43.3033.800.781
cs2lua.int.arity1.ret37.0032.500.878
cs2lua.int.arity1.void30.8028.500.925
cs2lua.ulong.3arg.ret40.1039.400.983

解释:无参 / 单参 C#→Lua 路径上,xLua 预缓存 Delegate + 较少栈操作时,固定开销可略低;参数变多或引用类型 后 ZLua 重新领先并拉开。

8.5 解读

  1. Lua→C#(日常脚本调 C#)是 ZLua 主战场:231/231 领先,平均约 2.6×;字段/属性读常见 ~3.5–4×
  2. C#→Lua:平均约 1.7×int[] / class 等可达 5–6×;极简 arity0–1 有少量回落。
  3. 与 §5 理论同向:字段与多层 marshal 场景差距最大;极简 C#→Lua 理论「2–4×」偏乐观,实测更接近 1.1–1.7×(轻量)到 5×+(重类型)。
  4. toLua / SLua、移动端、Delegate(P5)、GC Alloc 仍待补测。

8.6 复现

benchmark/dev.md:同机构建两个 Il2Cpp Win64 Release Player,跑全量 case 导出 CSV,再生成对比报告。


9. 理论下限参考

同进程嵌入 Lua 调同签名 native 函数的下限约:

lua 回调 + 2×读栈 + 1×写栈 + methodPointer ≈ 20–60 ns

ZLua 热路径 接近 该下限;xLua 因多次 LuaDLL,通常 高一个数量级(P1 量级)。


10. 选型建议(性能视角)

选 xLua / toLua / SLua选 ZLua(Player)
互调不是 profiling 热点战斗公式、UI 每帧上千次小调用等 边界热点
不愿维护 libil2cpp fork可投入双端测试 + Unity merge
需要成熟工具链接受文档与热更自建

相关文档

文档内容
GC.md分配与「零 GC」边界
BRIDGE.mdstub 体积与间接层
FEATURES.md功能差异

§2–§7 为路径分析;§8 为 2026-07-22 Il2Cpp Win64 实测(ZLua vs xLua)。toLua / SLua / P5 待补。