C#跟Lua如何超高效能傳遞資料

李嘉的部落格發表於2019-07-21

前言

UWA學堂上線那天,我買了招文勇這篇Lua互動的課程,19塊還算值,但是前段時間太忙,一直沒空研究,他的demo是基於xlua的,今天終於花了大半天時間在tolua下跑起來了,記錄一下我的理解

C#跟Lua如何超高效能傳遞資料

效能,仍然是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位貌似沒支援好。所以如果真正要使用的話,還要改很多東西

相關文章