美文网首页
C#跟Lua如何超高性能传递数据

C#跟Lua如何超高性能传递数据

作者: 李嘉的博客 | 来源:发表于2019-07-21 00:51 被阅读0次

    前言

    UWA学堂上线那天,我买了招文勇这篇Lua交互的课程,19块还算值,但是前段时间太忙,一直没空研究,他的demo是基于xlua的,今天终于花了大半天时间在tolua下跑起来了,记录一下我的理解

    性能,仍然是Lua中与C#混用的大坑

    Lua跟C#交互的性能问题是老生常谈的了,c#跟lua数据交互是通过lua虚拟栈,进行压栈、出栈来传递的,一次调用就需要执行很多指令,性能会随着调用次数的频繁,函数参数的增多而变差。直接操作内存的方式,可以在c#端修改lua内存,省去了操作虚拟栈,函数调用的大把指令,性能也就很高效了

    腾讯的UnLua(给虚幻4用的)中也有类似的直接操作内存的交互方式,看来这种方式会渐渐成为主流,毕竟性能摆在这呢

    UnLua

    Lua跟C#高效共享大量数据的一种方法

    原理其实很简单,在c#端定义好lua table的结构体,必须在内存中对齐lua端的table,然后在c#端拿到lua table的指针,读写这块内存,就能读写这个lua table了。

    是不是觉得非常简单,哈哈哈哈。感觉自己马上就能弄出来了

    想要实现这套东西,还得搞懂几个问题,下面开始一一讲解

    Lua Table结构体是什么样的?

    想在c#端写一个lua table结构体,那就先看看lua端这个结构体是怎么实现的吧。在tolua下,我们使用的是luajit,jit的源码跟lua是不一样的,luajit又分32位跟64位。所以我们这个table结构体也需要做多套才行

    luajit中的GCTab就是Table的结构体了

    typedef struct GCtab {
      GCHeader;
      uint8_t nomm;     /* Negative cache for fast metamethods. */
      int8_t colo;      /* Array colocation. */
      MRef array;       /* Array part. */
      GCRef gclist;
      GCRef metatable;  /* Must be at same offset in GCudata. */
      MRef node;        /* Hash part. */
      uint32_t asize;   /* Size of array part (keys [0, asize-1]). */
      uint32_t hmask;   /* Hash part mask (size of hash part - 1). */
    #if LJ_GC64
      MRef freetop;     /* Top of free elements. */
    #endif
    } GCtab;
    

    GCHeader是每一个GC对象都要包含的一个宏,定义了这些属性

    #define GCHeader    GCRef nextgc; uint8_t marked; uint8_t gct
    

    lua的table支持数组、哈希表两种用法,甚至可以同时是数组又是哈希表。我们主要处理数组的数据交互,结构体中的MRef array;就是这个table的所有数据存储的地方了,而asize就等于这个数组的长度+1。所以我们重点关注这2个字段的内存地址

    如何设计c#端的table结构体呢?

    我们把GCTab结构体展开成这样看

    GCRef nextgc; 
    uint8_t marked; uint8_t gct; uint8_t nomm; int8_t colo;
    MRef array;
    GCRef gclist;
    GCRef metatable;
    MRef node;
    uint32_t asize;
    uint32_t hmask;
    MRef freetop;//这个是64位的才会有
    

    GCRef 跟 MRef 都是一个jit中封装的指针类型,会自动根据宏展开为32位跟64位。
    GCRef 表示这是一个GC对象的指针
    MRef 表示非GC对象的内存指针

    在c#中都可以用IntPtr类型代替

    uint8_t 是8字节的,我们把4个8字节的放在一起,可以用一个int32位占用

    那么转换到c#中,结构体就变成了这样

    // GC64 version
    public struct LuaJitGCtabGC64
    {
        IntPtr nextgc;
    
        UInt32 masks;
    
        IntPtr array;
    
        IntPtr gclist;
    
        IntPtr metatable;
    
        IntPtr node;
    
        UInt32 asize;
    
        UInt32 hmask;
    
        IntPtr freetop; // only valid for LJ_GC64
    }
    

    指针array指向的数据是什么?

    在lj_tab.c中看tab的实现,我们很快就能找到array里存的是TValue结构,TValue其实是一个联合体。

    联合体是多个结构体可以共享同一块内存,访问的时候可以用不同的结构体方式去访问。具体什么是联合体可以自行百度哦

    TValue源码

    /* Tagged value. */
    typedef LJ_ALIGN(8) union TValue {
      uint64_t u64;     /* 64 bit pattern overlaps number. */
      lua_Number n;     /* Number object overlaps split tag/value object. */
    #if LJ_GC64
      GCRef gcr;        /* GCobj reference with tag. */
      int64_t it64;
      struct {
        LJ_ENDIAN_LOHI(
          int32_t i;    /* Integer value. */
        , uint32_t it;  /* Internal object tag. Must overlap MSW of number. */
        )
      };
    #else
      struct {
        LJ_ENDIAN_LOHI(
          union {
        GCRef gcr;  /* GCobj reference (if any). */
        int32_t i;  /* Integer value. */
          };
        , uint32_t it;  /* Internal object tag. Must overlap MSW of number. */
        )
      };
    #endif
    #if LJ_FR2
      int64_t ftsz;     /* Frame type and size of previous frame, or PC. */
    #else
      struct {
        LJ_ENDIAN_LOHI(
          GCRef func;   /* Function for next frame (or dummy L). */
        , FrameLink tp; /* Link to previous frame. */
        )
      } fr;
    #endif
      struct {
        LJ_ENDIAN_LOHI(
          uint32_t lo;  /* Lower 32 bits of number. */
        , uint32_t hi;  /* Upper 32 bits of number. */
        )
      } u32;
    } TValue;
    

    这中间有很多宏,看着很乱,但其实我们只需要用2种模式就行了,因为我们只实现int跟double。作者给出的方式是如下这种

    [StructLayout(LayoutKind.Explicit, Size = 8)]
    public struct LuaJitTValue
    {
        // uint64
        [FieldOffset(0)]
        public UInt64 u64;
    
        // number
        [FieldOffset(0)]
        public double n;
    
        // integer value
        [FieldOffset(0)]
        public int i;
    
        // internal object tag for GC64
        [FieldOffset(0)]
        public Int64 it64;
    
        // internal object tag
        [FieldOffset(4)]
        public UInt32 it;
    }
    

    但这里我有一些我还没弄明白,因为我实际运行起来后,不管lua赋值的是整形,还是浮点,int i始终没有值,值都存在了double n中。那为啥作者要弄一个int i跟UInt32 it; 这个it还偏移了4字节

    在c#端我们可以使用[StructLayout(LayoutKind.Explicit)][FieldOffset(0)]来实现c语言中的联合体,具体方式可以看这篇文章
    https://blog.csdn.net/wonengxing/article/details/44302661

    如何用unsafe模式读写结构体?

    结构体都定义好了,接下来我们看看怎么读写一个double

    LuaJitGCtab32* TableRawPtr; //需要拿到Lua端Table的指针
    
     //赋值操作
    TableRawPtr->array[index].n = val;
    //取值操作
    TableRawPtr->array[index].n;
    

    没错,就是这么简单。直接就可以操作lua内存了。

    如何拿到lua端table的指针?

    在lua端传入一个table参数过来,我们可以在c#端操作虚拟栈转成指针

    System.IntPtr arg0 = LuaDLL.lua_topointer(L, 1);
    

    看到这里,相信大部分的谜团都已经解开了,真的自己可以实现一套出来了。

    总结

    作者提供的方案里,只支持int、double。只支持array类型的table。还有luajit64位貌似没支持好。所以如果真正要使用的话,还要改很多东西

    相关文章

      网友评论

          本文标题:C#跟Lua如何超高性能传递数据

          本文链接:https://www.haomeiwen.com/subject/iruulctx.html