Tolua 介绍

  1. 在 C#与 Lua 交互中,大量数据共享方案
    1. 性能瓶颈
    2. 如何传输大量数据,而不会有性能问题
    3. 代码

在 C#与 Lua 交互中,大量数据共享方案

性能瓶颈

    1. 在 Lua 代码 gameobject.transform.position = pos中,有GameObject类型,pos是Vector3.
      调用代码结构以及顺序:

|代码调用顺序 C#/C++/Lua| 详情|
|—|—|—|
|第一步(gameobject.transform)||
|UnityEngine_GameObjectWrap.get_transform|Lua 想从gameobject中拿到transform,对应的gameobject.transform|
|ToLua.ToObject/LuaDLL.tolua_rawnetobj|把 Lua 中的 gameobject变成 C#可以辨认的 ID|
|ObjectTranslator.Get|用这个 ID,从ObjectTranslator中获取 C#的gameobject对象|
|UnityEngine.GameObject obj = (UnityEngine.GameObject)o; UnityEngine.Transform ret = obj.transform;|准备这么多,这里终于真正执行 C#获取gameobject.transform|
|ToLua.Push/ObjectTranslator.AddObject|给 transform 分配一个 ID,这个 ID 会在 Lua 中用来代表这个 transform,transform 要保存到ObjectTranslator供未来进行查找|
|LuaDLL.tolua_pushnewudata/tolua_newudata(C++)|在 Lua 分配一个 userdata,把 ID 存进去,用来表示即将返回给 Lua 的transform|
|lua_setmetatable(C++)|给这个 userdata 附上 metatable,让你可以transform.position 这样使用它|
|lua_pushvalue/lua_rawseti/lua_remove/(C++)|返回 transform,后面做些收尾|
|第二步(transform.position = pos)||
|UnityEngine_TransformWrap.set_position|Lua 想把 pos 设置到 C#的transform.position 上去|
|ToLua.ToObject/LuaDLL.tolua_rawnetobj|把 Lua 中的 transform变成 C#可以辨认的 ID|
|ObjectTranslator.TryGetValue|用这个 ID 从ObjectTranslator中获取 C#的 transform 对象.|
|UnityEngine.Vector3 arg0 = ToLua.ToVector3(L, 2) LuaDLL.tolua_getvec3|从Lua 中拿到 Vector3的 3 个 float 值返回给 C#|
|lua_getref+lua_tonumber(3 次)(C++)|拿到 xyz 的值|
|lua_pop(C++)|退栈|
|transform.position = new Vector3(x, y, z)|给当前物体的位置赋值|

在这个地方,频繁的取值,入栈,从 C#到 Lua 的类型转换,每一步都是满满的 CPU 时间,还不考虑中间产生了各种内存分配和后面的 GC

    1. 在Lua中引用C#的Object,代价昂贵,主流的 Lua+Unity 都是使用一个 ID 表示 C#对象,使用 dictionary 来存 ID 以及对象,同时因为有了这个 dictionary 的引用也保证了 C#的 object 在 Lua 有引用的情况下不会被垃圾回收掉.
      因此,lua 中每次参数中带有 object,要从 Lua 中的 ID 表示转换回 C#的 object,就要做一次 dictionary 查找;每次调用一个 object 的成员方法,也要先找到这个 object 也就是要 dictionary 查找.
      如果之前缓存过对象,就是查找dictionary 的工作 ,如果没有,就是需要上面一大堆的查找存取赋值的操作.
      如果刚在 Lua 中的 userdata 与 dictionary 索引可能会因为 Lua 的引用被 GC 删除,下次又要重复出现这个流程.
    1. 在Lua和C#间传递Unity独有的值类型(Vector3/Quaternion等)更加昂贵.
      Lua调用C#对象缓慢,如果每次vector3.x都要经过C#,那性能基本上就处于崩溃了,所以主流的方案都将Vector3等类型实现为纯Lua代码,Vector3就是一个{x,y,z}的table,这样在Lua中使用就快了。
      但是这样做之后,C#和Lua中对Vector3的表示就完全是两个东西了,所以传参就涉及到Lua类型和C#类型的转换,例如C#将Vector3传给Lua,整个流程如下:
      A. C#中拿到Vector3的x、y、z三个值;
      B. Push这3个float给Lua栈;
      C. 然后构造一个表,将表的x,y,z赋值;
      D. 将这个表push到返回值里。
      一个简单的传参就要完成3次push参数、表内存分配、3次表插入,性能可想而知。
      直接在函数中传递三个float,要比传递Vector3要更快。例如void SetPos(GameObject obj, Vector3 pos)改为void SetPos(GameObject obj, float x, float y, float z)
    1. Lua和C#之间传参、返回时,尽可能不要传递以下类型:
      严重类: Vector3/Quaternion等Unity值类型,数组
      次严重类:bool string 各种object
      建议传递:int float double
      Lua、C、C#由于在很多数据类型的表示以及内存分配策略都不同,需要进行转换(术语parameter mashalling),这个转换消耗根据不同的类型会有很大的不同.
      先说次严重类中的 bool和 string类型,涉及到C和C#的交互性能消耗,根据微软官方文档,在数据类型的处理上,C#定义了Blittable Types和Non-Blittable Types,其中bool和string属于Non-Blittable Types,意思是他们在C和C#中的内存表示不一样,意味着从C传递到C#时需要进行类型转换,降低性能,而string还要考虑内存分配(将string的内存复制到托管堆,以及utf8和utf16互转)。大家可以参考https://msdn.microsoft.com/zh-cn/library/ms998551.aspx,这里有更详细的关于C和C#交互的性能优化指引。
      严重类型是尝试Lua对象与C#对象对应时的瓶颈所致,Lua中的数组只能以table表示,没有直接的对应关系,因此从C#的数组转换为Lua table只能逐个复制,如果涉及object/string等,更是要逐个转换。
    1. 频繁调用的函数,参数的数量要控制
      无论是Lua的pushint/checkint,还是C到C#的参数传递,参数转换都是最主要的消耗,而且是逐个参数进行的,因此,Lua调用C#的性能,除了跟参数类型相关外,也跟参数个数有很大关系。一般而言,频繁调用的函数不要超过4个参数,而动辄十几个参数的函数如果频繁调用,你会看到很明显的性能下降,手机上可能一帧调用数百次就可以看到10ms级别的时间。
    1. 优先使用static函数导出,减少使用成员方法导出
      一个object要访问成员方法或者成员变量,都需要查找Lua userdata和C#对象的引用,或者查找metatable,耗时甚多。直接导出static函数,可以减少这样的消耗。
      像obj.transform.position = pos。我们建议的方法是,写成静态导出函数,类似
      static class LuaUtil
      {
      static void SetPos(GameObject obj, float x, float y, float z)
      {
      obj.transform.position = new Vector3(x, y, z); 
      }
      }
      然后在Lua中LuaUtil.SetPos(obj, pos.x, pos.y, pos.z),这样的性能会好非常多,因为省掉了transform的频繁返回,而且还避免了transform经常临时返回引起Lua的GC。
    1. 注意Lua拿着C#对象的引用时会造成C#对象无法释放,这是内存泄漏常见的起因
      C# object返回给Lua,是通过dictionary将Lua的userdata和C# object关联起来,只要Lua中的userdata没回收,C# object也就会被这个dictionary拿着引用,导致无法回收。最常见的就是gameobject和component,如果Lua里头引用了他们,即使你进行了Destroy,也会发现他们还残留在mono堆里。不过,因为这个dictionary是Lua跟C#的唯一关联,所以要发现这个问题也并不难,遍历一下这个dictionary就很容易发现。uLua下这个dictionary在ObjectTranslator类、SLua则在ObjectCache类。
    1. 考虑在Lua中只使用自己管理的ID,而不直接引用C#的Object
      想避免Lua引用C# Object带来的各种性能问题的其中一个方法就是自己分配ID去索引Object,同时相关C#导出函数不再传递Object做参数,而是传递int。这带来几个好处:

A. 函数调用的性能更好;
B. 明确地管理这些Object的生命周期,避免让ULua自动管理这些对象的引用,如果在Lua中错误地引用了这些对象会导致对象无法释放,从而内存泄露;
C. C#Object返回到Lua中,如果Lua没有引用,又会很容易马上GC,并且删除ObjectTranslator对Object的引用。自行管理这个引用关系,就不会频繁发生这样的GC行为和分配行为。

例如,上面的LuaUtil.SetPos(GameObject obj, float x, float y, float z)可以进一步优化为LuaUtil.SetPos(int objID, float x, float y, float z)。然后我们在自己的代码里头记录objID跟GameObject的对应关系,如果可以,用数组来记录而不是dictionary,则会有更快的查找效率。如此下来可以进一步省掉Lua调用C#的时间,并且对象的管理也会更高效。

    1. 合理利用out关键字返回复杂的返回值
      在C#向Lua返回各种类型的东西跟传参类似,也是有各种消耗的。比如 Vector3 GetPos(GameObject obj) 可以写成 void GetPos(GameObject obj, out float x, out float y, out float z)。表面上参数个数增多了,但是根据生成出来的导出代码(我们以uLua为准),会从:LuaDLL.tolua_getfloat3(内含get_field + tonumber 3次) 变成 isnumber + tonumber 3次。get_field本质上是表查找,肯定比isnumber访问栈更慢,因此这样做会有更好的性能。

如何传输大量数据,而不会有性能问题

    1. 直接将数据存储到Lua分配的数组内,然后将Lua底层 的内存结构直接暴露给C#,C#绕过Lua API,直接以unsafe的方式直接读写 Lua的内存。
    1. 为什么不简单地做一个共享内存,让Lua直接访问呢?
      A:尽管共享内存在C#上实现和访问非常容易,但是在Lua上没有原生的办法 高效访问这片内存区域,唯一的做法就是导出C或者C#的API去读写,如 果Lua上的读写非常频繁且粒度细碎,那么这个地方依然会逐渐成为性能 瓶颈。
      B:其次,要做到高效,共享内存最优的实现方式还是使用Lua to C的访问方 式,这意味着需要编译C语言代码,跨平台编译相对来说维护和易用性会 麻烦一些,很多团队并不是很乐于去解决这个坑。
      C:LuaJIT对原生指令集优化较好,能直接使用原生的方式读写数组肯定要比 通过C API效率要高。
    1. 整个流程:
      1. 通过Lua_topointer,直接获取Lua table的内存指针。
      2. 由于Lua/LuaJIT的table内存结构是可以确认的,我们可以对照其C代码在 C#中声明结构体,这样就可以通过table指针拿到array的指针以及array的长度。
        // Table from lua source lobject.h
        [StructLayout(LayoutKind.Sequential)]
        public struct LuaTableRawDef
        {
           public IntPtr next;
        
           // lu_byte tt; lu_byte marked; lu_byte flags; lu_byte lsizenode;
           public uint bytes;
           // unsigned int sizearray
           public uint sizearray;
           // TValue* array
           public IntPtr array;
           // Node* node
           public IntPtr node;
           // Node* lastfree
           public IntPtr lastfree;
           // Table* metatable
           public IntPtr metatable;
           // GCObejct* gclist
           public IntPtr gclist;
        }
      3. 但是,这里有一个难点,就是要处理Lua/LuaJIT的差异,以及在不同编译选 项下产生出来的32位、64位的差异。所以可以看到我们是分LuaAdapter.cs和 LuaJitAdapter.cs两套实现,并且各自提供了32/64位的结构体声明。
      4. 不管是Lua还是LuaJIT,array数组存储的不是int或者double,而是一个叫 TValue的联合体,TValue除了存储数值本身,还存储了类型信息。我们在读写 的时候,需要先判断类型信息,不然就会无法获得正确的结果。
      5. 在了解这些信息之后,整个过程就是:拿到table指针,用对应平台的结构 体指针获得array指针,再通过数组index拿到array中正确位置的TValue,最后 根据TValue的类型信息获得/写入int或者double。

代码


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1487842110@qq.com

Love

Title:Tolua 介绍

文章字数:2.8k

Author:诸子百家-谁的天下?

Created At:2020-05-08, 11:41:32

Updated At:2021-05-24, 00:40:07

Url:http://yoursite.com/2020/05/08/Unity/ToLua/%E5%A4%A7%E6%95%B0%E6%8D%AE%E4%BA%A4%E4%BA%92%E6%80%A7%E8%83%BD/

Copyright: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

爱你,爱世人