基于ToLua的C#和Lua内存共享方案保姆级教程

[复制链接]
发表于 2025-12-24 00:50:13 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
C#和Lua内存共享方案保姆级教程

媒介

在先容C#和Lua内存共享方案之前,先先容下面两个点来支持这个方案的须要性
跨语言交互很费

Lua和C#交互最早是基于反射的方式实现的,厥后为了提拔性能发展成Luajit+C#静态方法导出注入到lua捏造机的方式至此Lua+Unity的性能才到达了实用的级别,但我们如果用的欠好照旧会存在诸多性能题目,详细的一些使用注意事项参考引用里《用好lua+unity,让性能飞起来——lua与c#交互篇》,用文章中的一个例子来加深一下我们lua点点调用一个对象的属性的写法在Lua+C#的组合模式下是有多么的费劲能,固然写起来很轻松,但现实交互都是调用的捏造栈,会履历压栈,出栈等利用,性能会随着调用次数的频仍和参数通报的越多而变的越差,看了下面的交互步调之后就意识到尽大概的克制这种点属性的链式写法
game.bject.transorm.position = pos写法在项目中很常见,但大量使用就比力糟糕,由于每点一次属性就意味着要颠末堆栈调用,有的乃至还要颠末Dictionary容器查询利用,内部又是好几条指令,每一次指令就意味着时间开销,只管一次很小但调用次数多了之后就可观了,要实现这个坐标赋值的利用,lua堆栈调用如下
第一步
  1.   GameObjectWrap.get_transform        lua想从gameobj拿到transform,对应gameobj.transform
  2.   LuaDLL.luanet_rawnetobj             把lua中的gameobj变成c#可以辨认的id
  3.   ObjectTranslator.TryGetValue        用这个id,从ObjectTranslator中获取c#的gameobject对象
  4.   gameobject.transform                准备这么多,这里终于真正执行c#获取gameobject.transform了
  5.   ObjectTranslator.AddObject          给transform分配一个id,这个id会在lua中用来代表这个transform,transform要保存到ObjectTranslator供未来查找
  6.   LuaDLL.luanet_newudata              在lua分配一个userdata,把id存进去,用来表示即将返回给lua的transform
  7.   LuaDLL.lua_setmetatable             给这个userdata附上metatable,让你可以transform.position这样使用它
  8.   LuaDLL.lua_pushvalue                返回transform,后面做些收尾
  9.   LuaDLL.lua_rawseti
  10.   LuaDLL.lua_remove
复制代码
第二步:
  1.   TransformWrap.set_position                      lua想把pos设置到transform.position
  2.   LuaDLL.luanet_rawnetobj                         把lua中的transform变成c#可以辨认的id
  3.   ObjectTranslator.TryGetValue                    用这个id,从ObjectTranslator中获取c#的transform对象
  4.   LuaDLL.tolua_getfloat3                          从lua中拿到Vector3的3个float值返回给c#
  5.   lua_getfield + lua_tonumber 3次                 拿xyz的值,退栈
  6.   lua_pop
复制代码
transform.position = new Vector3(x,y,z) 准备了这么多,终于实验transform.position = pos赋值了
上面的内部堆栈调用分析白lua点属性大概方法调用比力费性能,语言交互代价比力"昂贵"
我们在用unity profiler的时间也会发现有的调用比力深的堆栈一页都拉不到底,固然大概是存在递归调用的环境,这调用嵌套越,实验泯灭的时间就越多,而上面的gameobject.transform每次如果暂时使用很快就会被lua gc掉,下次再调用又会重新走一遍这个堆栈调用的流程就会很费性能
参数通报也很费

用传统的捏造栈通报参数的方式同样的原理,尤其是Unity特有的值范例结构(Vector3,Quaternion等),lua中一个Vector3就是一个table,跟C#是Vector3是两种结构,就会涉及到这两种范例的转换,比方将C# Vector3通报给Lua,流程如下:
      
  • C#中拿到Vector3的x、y、z三个值;  
  • Push这3个float给Lua栈;  
  • 然后构造一个表,将表的x,y,z赋值;  
  • 将这个表push到返回值里。
    一个传vector的利用会履历三次push参数、表内存分配、和3次表插入等利用,固然单次指令也就那么大几个,但这种频仍利用的代码会导致性能降落
现在项目中参数通报的两种方式

第一种、SendMessage的方式

通过Lua捏造栈接纳数据压栈和出栈的方式(获取对应的LuaTable的方法,传参的方式)


第二种、C#持有LuaTable

C#通过LuaState获取到对应的table,然后给这个table添加对应的数据,也可以添加上table,这种方式有显着的利弊,可读性差,如果接纳哈希的结构会有key字符串的内存开销,针对复杂的嵌套范例写起来极其"恶心",下不了手,得当简单的范例照旧可以思量的

上面两种方式的性能对比

天之之前测试血条、蓝条将SendMessage的方式改成LuaTable共享的方式做的对比测试

      
  • SendMessage Call Function的方式

      
  • LuaTable共享的方式


由上面的图可见LuaTable的方式照旧比方法调用的方式传参性能要好很多,但看他们性能图照旧会稳固的性能尖峰,只管是峰值从42ms低落到10ms,但稳固的10ms尖峰照旧必要优化的
上面这两种方式PK,LuaTable数据共享的方式已经胜出,以是第一种方式就不思量了,下面针对引进内的存共享方案针再跟这两种方案做一个性能对比分析
内存共享方案

引入须要性

基于以上两点理论,再联合我们项目,在战斗过程中,会有大量的网络消息包从服务器下发到客户端,我们项目标消息处置处罚方案是C#继续PB消息包然后分析消息包,再将UI中Lua需表现的数据再打包封装传给Lua使用,知道上面的两点导致性能题目标理论底子之后,我们通过"大量","通报参数"等字眼就知道会有比力大的性能题目,如果我们能克制频仍的举行参数通报和跨语言方法调用是不是就能有效的克制这部门的性能题目,理论上是行得通的。
方案原理

我们一个lua的table的对象,既然是对象那就有存储地点,两种语言都是能根据地点去获取数据的,这里的地点一样平常是首地点,至于这个地点存储的是什么内容是多长的长度,Lua和C#就都必要知道,然后才气准确的获取到对应数据范例的数据,条件条件缺一不可,根据这个思绪就存在两种方式
第一种:照旧通过Lua捏造栈的方式通报内存地点和长度,这种得当序列化和反序列化的方式,由于两种语言共用一种数据协议格式
第二种:通过内存对齐,C#封装跟LuaTable一样的结构体大概连合体,做到跟LuaC中LuaTable的结构对齐,如许两端只要知道LuaTable的首地点就能按照数据格式依次的分析出来,由于结构里界说了每个字段的内存对齐方式和字段占用的长度
起首看一下LuaTable在C中的界说,由于我们要参照这个结构来实现C#版的LuaTable,tolua中使用的是luajit,luajit又区分32位和64位,以是我们C#的table也要适配做多套才行
luajit中GCTab就是Table的结构体
  1. typedef struct GCtab {
  2.   GCHeader;
  3.   uint8_t nomm;                /* Negative cache for fast metamethods. */
  4.   int8_t colo;                /* Array colocation. */
  5.   MRef array;                /* Array part. */
  6.   GCRef gclist;
  7.   GCRef metatable;        /* Must be at same offset in GCudata. */
  8.   MRef node;                /* Hash part. */
  9.   uint32_t asize;        /* Size of array part (keys [0, asize-1]). */
  10.   uint32_t hmask;        /* Hash part mask (size of hash part - 1). */
  11. #if LJ_GC64
  12.   MRef freetop;                /* Top of free elements. */
  13. #endif
  14. } GCtab;
复制代码
此中GCHeader是一个每个GC对象都要包罗的宏界说,包罗了下面属性
  1. #define GCHeader        GCRef nextgc; uint8_t marked; uint8_t gct
复制代码
lua的table支持数组、哈希两种写法,乃至可以同时又是数组又是哈希表。我们紧张处置处罚数组的数据交互对应结构体中的MRef array,这就是table全部数据存储的地方,而asize就即是这个数组的长度,以是我们重点关注这两个内存地点
怎样筹划对应C#端的table结构呢?
我们把table结构睁开看
  1. GCRef nextgc;
  2. uint8_t marked; uint8_t gct; uint8_t nomm; int8_t colo;
  3. MRef array;
  4. GCRef gclist;
  5. GCRef metatable;
  6. MRef node;
  7. uint32_t asize;
  8. uint32_t hmask;
  9. MRef freetop;//这个是64位的才会有
复制代码
此中GCRef和MRef都是jit中封装的指针范例,会主动根据宏睁开为32和64位
GCRef表现这是一个GC对象的指针
MRef表现非GC对象的内存指针
他们都可以用C#中的IntPtr取代
uint8_t是8字节的,我们把4个8字节放在一起,可以用一个int32位占用
那么转换到C#结构中,结构体就酿成
  1. // GC64 version  C#对应的luatable的结构
  2. public struct LuaJitGCtabGC64
  3. {
  4.     IntPtr nextgc;
  5.     UInt32 masks;
  6.     IntPtr array;
  7.     IntPtr gclist;
  8.     IntPtr metatable;
  9.     IntPtr node;
  10.     UInt32 asize;
  11.     UInt32 hmask;
  12.     IntPtr freetop; // only valid for LJ_GC64
  13. }
复制代码
那么指针array指向的是什么?
在luac的代码中lj_tabl.c可以看到tab的实现,找到array是TValue结构,此中TValue是一个连合体。至于连合体是什么,可以本身补一下https://blog.csdn.net/m0_57180439/article/details/120417270
简而言之就是连合体是多个结构共享一块内存,访问的时间可以根据差异结构去访问,好比int和char构成的连合体,它可以存int也可以存char,获取的时间就根据现实存的是什么数据就获取什么数据,就明白为一个内存块巨细有限,两者只能存一种,恣意你存取哪一种范例
TValue源码
  1. /* Tagged value. */
  2. typedef LJ_ALIGN(8) union TValue {
  3.   uint64_t u64;                /* 64 bit pattern overlaps number. */
  4.   lua_Number n;                /* Number object overlaps split tag/value object. */
  5. #if LJ_GC64
  6.   GCRef gcr;                /* GCobj reference with tag. */
  7.   int64_t it64;
  8.   struct {
  9.     LJ_ENDIAN_LOHI(
  10.       int32_t i;        /* Integer value. */
  11.     , uint32_t it;        /* Internal object tag. Must overlap MSW of number. */
  12.     )
  13.   };
  14. #else
  15.   struct {
  16.     LJ_ENDIAN_LOHI(
  17.       union {
  18.         GCRef gcr;        /* GCobj reference (if any). */
  19.         int32_t i;        /* Integer value. */
  20.       };
  21.     , uint32_t it;        /* Internal object tag. Must overlap MSW of number. */
  22.     )
  23.   };
  24. #endif
  25. #if LJ_FR2
  26.   int64_t ftsz;                /* Frame type and size of previous frame, or PC. */
  27. #else
  28.   struct {
  29.     LJ_ENDIAN_LOHI(
  30.       GCRef func;        /* Function for next frame (or dummy L). */
  31.     , FrameLink tp;        /* Link to previous frame. */
  32.     )
  33.   } fr;
  34. #endif
  35.   struct {
  36.     LJ_ENDIAN_LOHI(
  37.       uint32_t lo;        /* Lower 32 bits of number. */
  38.     , uint32_t hi;        /* Upper 32 bits of number. */
  39.     )
  40.   } u32;
  41. } TValue;
复制代码
这内里有很多宏,看着很乱,但我们只用此中的int和double,以是就可以改成对应的C#结构
  1. [StructLayout(LayoutKind.Explicit, Size = 8)]
  2. public struct LuaJitTValue
  3. {
  4.     // uint64
  5.     [FieldOffset(0)]
  6.     public UInt64 u64;
  7.     // number
  8.     [FieldOffset(0)]
  9.     public double n;
  10.     // integer value
  11.     [FieldOffset(0)]
  12.     public int i;
  13.     // internal object tag for GC64
  14.     [FieldOffset(0)]
  15.     public Int64 it64;
  16.     // internal object tag
  17.     [FieldOffset(4)]
  18.     public UInt32 it;
  19. }
复制代码
至于C#端这个内存结构的特性StructLayout,不知道的同砚可以学习一下
https://blog.csdn.net/wonengxing/article/details/44302661
LayoutKind范例有三种情势
1.Explicit指定内存对齐格式,并限定结构巨细
2.Sequential就是次序存储,结构巨细是全部字段的和
3.auto就是主动盘算,根据结构里最大的字段占用的长度来算最大长度
上面结构就是8个字节,前面四个从第0字节开始,第5个是从第4个字节开始
C#读取利用Lua天生的Table
  1. LuaJitGCtab32* TableRawPtr; //需要拿到Lua端Table的指针
  2. //赋值操作
  3. TableRawPtr->array[index].n = val;
  4. //取值操作
  5. TableRawPtr->array[index].n;
复制代码
落地到ToLua

落地demo地点
https://github.com/dingxiaowei/MessagePack
用例测试

一、针对复杂范例和嵌套范例的数据结构使用MessagePack+内存共享方案测试
  1. [MessagePackObject]
  2. public struct Data1
  3. {
  4.     [Key(0)]
  5.     public int Id { get; set; }
  6. }
  7. [MessagePackObject]
  8. public struct MyData
  9. {
  10.     [Key(0)]
  11.     public int Id { get; set; }
  12.     [Key(1)]
  13.     public string Name { get; set; }
  14.     [Key(2)]
  15.     public Dictionary<int, long> Attributes { get; set; }
  16.     [Key(3)]
  17.     public List<string> ListValues { get; set; }
  18.     [Key(4)]
  19.     public Data1 data { get; set; }
  20. }
复制代码
包罗了常用的数据结构,但这里包罗了内存共享不支持的数据范例,引入了me
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表