« Lua 不是 C++ | 返回首页 | _alloca 函数的实现 »

让 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

我们公司是用lua做插件,还有网络小游戏插件

顶顶~~~~~支持~~~~一流的程序员

一年前我看《高级游戏脚本编程》看不太懂…只记得里面有提到Lua…那书是从C++的基础开始讲脚本,直到最后如何实现自己的脚本编译器?于是之后的一年时间我开始学C++
学到现在刚刚开始摆脱控制台界面,直到上星期才刚刚开始学着用API 写个hello…直到刚才看到这个帖(文章)我才想起来…我一年前是想学脚本的?学着学着偏离航向了…不过现在也没空去看脚本了…还有众多API 和DirectX等着我去啃…
学了一年多我还没真正意义上能做出自己憧憬的游戏(图形界面),学习还真是漫长啊…

meta-programming

就算在loadstring中传入chunk名字,行号还是不对,那个行号是会从 [[ 的第一行开始计算,而不是文件第一行。

把 compile 改成读文件,然后在 loadstring 里正确传入 chunk 名字即可解决行号问题。(需要做一点额外小修改会更完美一些)

有一个致命问题,如果出错,错误信息里的行号不对。

堆栈虚拟机更多利用内存运算代码,寄存器虚拟机更多利用寄存器运算代码,当运行的代码量增大时候(上万行)两者没有速度差异了,这就是java不屑一顾的原因。
我的include也是这样实现的,用语言本身扩展语言,lua编写一个编译器也没有问题,lua也有jit项目使用C嵌入汇编完成,大多数平台都能编译运行。

原来 lua 里这些都算是奇技淫巧啊?Common Lisp 廿年前就有宏了,其中一种用途就是编译期计算,详见 Pual Graham 《On Lisp》的第 13 章。

学习ing~~~~~

Post a comment

非这个主题相关的留言请到:留言本