Cnumber Patch

lua-users home
wiki

簡介

CNUMBER patch (可以在 LuaPowerPatches 中取得)提供了一個更有效率的機制讓 Lua 存取 C 的數字變數。例如,可以將以下的 C 變數視為一個一般變數,在 Lua 中使用

  double foo = 10.0;
你可能想要在 Lua 中存取這個變數
  foo = foo + 1
在 Lua 5.1 中,最常見的做法是將你的元資料表附加到全域環境表,然後附加 "__index" 和 "__newindex" 函式到元資料表中。這兩個函式會處理「foo」變數的取得和設定事件,它們必須以 C 來實作,因為那裡儲存著資料。這是一個運作良好的方法,但是如果你非常重視效率(就像我一樣),這個程序會有點沒有效率。首先,它使用全域變數,這比局部變數的效率低,因為全域變數名稱會在執行時期,經由查找環境表來解析,這和在編譯時期才計算位置的局部變數不同。在 Lua 中,表格的查找是很有效率的,即使是用字串當作關鍵字(例如變數的名稱),因為字串會被「內部化」且有預先計算雜湊值,但是這還是個負擔。此外,這需要 Lua 元方法的呼叫,這會更耗費效能。另一個美學上的考量是,為了改變一個變數的行為而去修改整個環境的行為,這似乎有點不適當。請注意,這種變數無法存在於他們的環境外,因為行為是附加到環境而非到值本身。

另一種方法是從 C 公開 getter 和 setter 存取函式(或方法),而且 Lua 程式碼直接呼叫這些函式

  set_foo(get_foo() + 1)
這樣很醜,而且也不特別有效率。例如,「set_foo」和「get_foo」都是必須在執行時期解析的全域變數,而且都包含著 Lua 函式呼叫。一種相似的作法可以在 [PIL] 中找到。一個優點是行為附加到值本身,因此不需要改變環境。

另一個建議的方法可能是實作數字 C 變數做為重用者資料,並附加一個元資料表,附帶適當的事件,讓它們像一般值一樣表現。然而,這也沒辦法提供有效率或語法上乾淨的解法。你可以用元資料表事件來改變值的行為,但是你不能讓他們的行為與一般值完全一樣。例如,沒有「指定」元資料表事件會允許這類值做為左值(這與 C++ 不同)。

這個 patch 透過提供所謂的「CNUMBER」來解決這些問題。CNUMBER 是在 Lua 中公開的變數,是用 C 實作的。在某些方面它類似於 Lua CFUNCTION,這是一種在 Lua 中公開且用 C 實作的函式。存取 CNUMBER 的速度幾乎和存取 Lua 局部變數一樣快,這一點並不令人意外,因為 CNUMBER 的處理方式與局部變數相似(忽略 closure 和作用域)。

用法

要使用 Lua 註冊 CNUMBER,你必須定義下列三個函數

double foo = 0.0;
double foo2 = 0.0;
double foo3 = 0.0;

    int
iscnumber(lua_State * L, const char * varname, int * id) {
    if(strcmp(varname, "foo") == 0) {
        *id = 1; return 1; /* yes */
    }
    else if(strcmp(varname, "foo2") == 0) {
        *id = 2; return 1; /* yes */
    }
    else if(strcmp(varname, "foo3") == 0) {
        *id = 3; return 1; /* yes */
    }
    else {
        return 0; /* no */
    }
}

    double
getcnumber(lua_State *L, int id) {
    switch(id) {
        case 1: { return foo; }
        case 2: { return foo2; }
        case 3: { return foo3; }
        default: assert(0);
    }
    return 0;  /* should not occur */
}

    void
setcnumber(lua_State *L, int id, double value) {
    switch(id) {
        case 1: { foo  = value;  break; }
        case 2: { foo2 = value;  break; }
        case 3: { foo3 = value;  break; }
        default: assert(0);
    }
}
第一個函數,iscnumber,由 Lua 剖析器呼叫。它允許 Lua 判斷特定識別碼是否應該詮釋為 CNUMBER。如果函數將名稱辨識為 CNUMBER,函數會指派該識別碼一個 ID 以供日後使用。ID 是一個 18 位元無號數字(這個大小受 Lua 5.1 的運算碼限制),其意義僅 C 程式碼知道。當 Lua 辨識 CNUMBER 時,它會產生新的特殊 GETCNUMBER 和 SETCNUMBER 運算碼,並將 ID 與其儲存起來。請注意每一個 CNUMBER 僅會解析一次(於編譯時),所以它的存取在執行時更有效率。

Getcnumber 和 setcnumber 函數會在處理兩個運算碼時在執行時被呼叫,以分別取得或設定變數。每個函數會將前述的 ID 傳遞過來。這些函數可以用上述簡單的方式在 ID 上進行查詢,或可以是更複雜的方式(例如:索引陣列)。C 程式碼中實際上不一定要存在一個雙精度變數。

在剖析前必須註冊這三個函數,如下所示

  lua_setcnumberhandler(L, iscnumber, getcnumber, setcnumber);

你可以透過將這三個函數全部設為 NULL 來取消註冊。

效能基準

為以下簡單範例的 10 次反覆運算提供效能基準

  for n=1,10000000 do
    foo = foo + 1
  end
這些結果會比較 CNUMBER 與兩種系統中各種方式的效能
  Linux 2.4.20 (virtualized) / GCC / Intel(R) Xeon(TM) 3 GHz
  == Run Times (sec)
  LOOP           : 8.100000
  CNUMBER        : 23.490000 *
  LOCAL          : 15.200000
  GLOBAL         : 37.150000
  METATABLELOCAL : 99.980000
  METATABLETABLE : 198.790000
  CFUNCTION      : 72.010000

  WinXP / GCC Ming / Intel(R) P4 3 GHz
  == Run Times (sec)
  LOOP           : 3.361000
  CNUMBER        : 13.702000 *
  LOCAL          : 8.579000
  GLOBAL         : 25.640000
  METATABLELOCAL : 72.858000
  METATABLETABLE : 150.266000
  CFUNCTION      : 52.672000
說明每種方式:

如所示,CNUMBER 幾乎和 Lua 本機變數一樣快,且比 Lua 全域變數還要快。它們比 METATABLETABLE 和 CFUNCTION 方法快很多,否則這些方法是需要從 Lua 公開 C 變數。METATABLELOCAL 是 METATABLETABLE 的簡易版本(略去第二次查詢表查詢),但實際上沒有用,僅提供用於比較。此效能基準程式已包含在修補程式(cnumber.c)中。

在某個「實際」應用中,CFUNCTION 將執行時間縮短了約 40%。

設計筆記

CNUMBER 實作具有一些重要的特性

此修補程式的可能延伸包括:

--DavidManura


RecentChanges · 偏好設定
編輯 · 記錄
最後編輯時間為 2006 年 2 月 21 日 上午 12:02 (GMT) (diff)