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,它是在C中实现的并在Lua中公开的函数。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的opcodes限制),其含义仅为C代码所知。当Lua识别CNUMBER时,它会生成新的特殊GETCNUMBER和SETCNUMBER opcodes,并将其ID存储起来。请注意,每个CNUMBER只解析一次(在编译时),因此在运行时访问它更有效。在处理这两个opcodes时, getcnumber和setcnumber函数会在运行时被调用,分别获取或设置变量。每个函数都会收到前面提到的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的实现有一些重要的特性
此补丁可能的扩展包括这些