让 lua 编译时计算
lua 里其实也颇多奇技淫巧,使用时应三思。
如果你读过 kepler 的代码,就会发现,多次编译这种技巧用的很多,甚至迭代几次使用。即,第一次加载代码时,用一段 lua 程序生成真正需要的源代码,然后再将其编译出来。
由于 lua 的编译速度相当快,而且这种迭代编译的过程仅仅在程序加载的时候进程一次,故而可以带来性能的提高:一些在系统初始化时可以决定的参数(比如从配置文件中读出来的数据)直接编译为常量置入程序中。
云风写了一小段 lua 程序,简化这种迭代编译的过程,算作周末自娱吧。
例如这样一个例子,我们在程序中需要用一个常量,这个常量可能是通过加载配置文件得到。假设允许编译期计算,我们可以这样:
function foo() for i=1,| config "max" | do execute(i) end end
这个例子里,循环的终值是通过调用 config "max" 得到的,如果每次运行这个程序时都去查询 config 必然影响效率。我们需要在程序加载时,第一次得到 config "max" 的结果即可。
这里,我使用 | 夹在中间 | 表示这段代码应该在加载时运行。
有点像 php 写网页用的模版?呵呵,可以接着往下看。
姑且我们把这种技术叫做代码模版吧,对于 C 程序员,则更接近于宏替换,C++ 程序员看来可能是一个高级 template 技巧。不过 lua 能做的更强一些。
我来演示一下,代码模版的上下文变量。
| ALPHA = math.pi / 4 | function foo(a) return a * math.sin(|ALPHA|) end
这个例子里,一开始给代码模版变量 ALPHA 赋了值为 pi/4 。然后在下面的函数中,使用了这个变量。这段代码经过处理后,会把常量 0.78539816339745 (pi/4) 直接编译进入最终执行的代码。而 ALPHA 这个变量,将只在模版预处理期间可见。
下面再看一个更实用一点的例子。
有时候,我们需要一个增强版的 unpack 。我们知道,lua 自带的 unpack 可以把一直数组(只有连续数字下标的 table)展开成一串返回值,但是对用字符串或别的东西做 key 的 table 无能为力。
function unpackex(tbl,args) local ret={} for _,v in ipairs(args) do table.insert(ret,tbl[v]) end return unpack(ret) end
下面我们可以用这个函数展开一个数组,
print ( unpackex( { one=1,two=2,three=3 }, { "one","two","three" } ))
unpackex 将按第 2 个参数表中给出的字符串次序解开第一个参数表。这里我们将看到输出 1,2,3 。
btw, 如果你真的需要一个高效的 unpackex ,推荐用 C 来实现,这样能省掉其中的临时 table ret 。
接下来,我们在使用 unpackex 时,还有可能遇到一个性能问题。因为第二个参数表通常是个常量数组,但由于 lua 的语义,下列函数中的这个常量表,可能在每次进入函数 foo 时构造一个新的出来。
function foo(tbl) return unpackex(tbl,{"one","two","three"}) end
如果 foo 对性能很敏感,有经验的 lua 程序员或许会这样优化一下:
local const_list={"one","two","three"} function foo(tbl) return unpackex(tbl,const_list) end
但这破坏了代码的直观。如果有了代码模版,我们实际上可以写成这样:
function foo(tbl) return unpackex(tbl,| {"one","two","three"} |) end
ok. 现在基本能看出来这个代码模版做了些什么。
它在分析源码字符串时,碰到 | 夹住的部分,将立即运行,获得结果。如果没有结果(例如只是运行一些代码,无返回值),则跳过。如果有结果值(可以由 return 返回,也可以本身是一个表达式),先判断类型是否是一个简单类型(nil ,boolean ,number 或 string ),就把值直接插入源码。如果是一个复杂类型,则创建一个 local 变量记住结果,并将这个 local 变量插入代码。
这里,简单类型 string 会被加上 [=[ 这样的括号插入。而有时候,我们需要直接把字符串插进去。
例如,有时候我们期望 lua 有 C 的 include 那种功能,直接把一文本文件插入源码。
为了效率,我们通常在每个 .lua 文件最前面写上诸如:
local pairs=pairs local table=table local string=string
这样的语句。
这可以提高 lua 程序访问内置 api 的效率(减少一次对全局表的查询)。
但每个文件前都写这么一串过于繁琐,也容易出错。如果有 include 功能就好了。那么就让我们实现一个:
function include(filename) local f=assert(io.open(filename)) local ret=f:read "*a" f:close() return ret end
这个函数可以读入一段文本返回。
代码模版可以通过 |# include "local.lua"|
这样的语法,将 include 函数的返回值插入代码。(注:遇到 | 后紧跟一个 # ,这个区间的返回值就必须是一个 string ,并且这个 string 将被插入代码)
最后,来看看代码模版的实现吧,使用 lua 本身就可以轻易搞定。有兴趣的同学点这里。
Comments
Posted by: Anonymous | (10) September 2, 2008 04:41 PM
Posted by: Anonymous | (9) September 2, 2008 04:39 PM
Posted by: 邪风天烬 | (8) August 22, 2008 09:06 PM
Posted by: Lord Congou | (7) August 18, 2008 10:21 AM
Posted by: Atry | (6) August 16, 2008 11:24 PM
Posted by: Cloud | (5) August 16, 2008 10:47 PM
Posted by: Atry | (4) August 16, 2008 10:21 PM
Posted by: 夏天 | (3) August 16, 2008 05:02 AM
Posted by: binghe | (2) August 16, 2008 02:45 AM
Posted by: Maya | (1) August 16, 2008 12:27 AM