Cnumber 补丁 |
|
CNUMBER 补丁(可在 LuaPowerPatches 获取)提供了一种更有效的机制,用于从 Lua 访问数值 C 变量。例如,考虑以下 C 变量
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++ 不同)。
此补丁通过提供所谓的“CNUMBER”来解决这些问题。CNUMBER 是在 Lua 中公开的变量,它是在 C 中实现的。在某种程度上,它类似于 Lua CFUNCTION,它是在 Lua 中公开并在 C 中实现的函数。对 CNUMBER 的访问几乎与对 Lua 局部变量的访问一样快,这并不奇怪,因为 CNUMBER 的处理方式与局部变量类似(忽略闭包和作用域)。
要将 CNUMBER 注册到 Lua,您必须定义三个函数,如下所示
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 代码中不一定实际存在 double 变量。
这三个函数必须在解析之前注册,如下所示
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 实现具有一些重要特性
此补丁的可能扩展包括以下内容