Cnumber Patch |
|
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 實作具有一些重要的特性
此修補程式的可能延伸包括: