LuaJIT 的研究笔记
Posted tms不熬夜
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LuaJIT 的研究笔记相关的知识,希望对你有一定的参考价值。
仔细算来, 从我第一次开始写代码到现在, 已经15年了. 其间用过多种语言, 也做过各种功能的系统和应用. 虽然现在离我理想的水准还有很大的差距, 但也总算是积累了一些经验, 消除了对陌生的技术领域的恐惧感.
我一向讨厌可读性差的代码, 讨厌过度设计, 讨厌对代码的绝对理想主义(典型的例子: 纯函数式编程). 我最看重的一点是, 代码要简洁实用.
目前来说, 我非常喜欢 Lua, 简单小巧, 快速方便. 最近阅读了很多 LuaJIT 的文章, 也看了一下 LuaJIT 的源代码, 整理成了这篇文章.
#1. 现实意义上的 Lua
Lua 作为一种编程语言被提出来是在里约天主教大学的论文 The design and implementation of a language for extending applications 里, 也有官方的实现, 但是 LuaJIT 是现实意义上的 Lua: 当我们用 Lua 做服务器端编程或者游戏客户端脚本时, 一般是在用 LuaJIT.
原因很简单, LuaJIT 比官方的 Lua 实现更快, 更好用. 根据 Elmar Klausmeier(德国人, 数学, 计算机科学家, 现居法兰克福) 去年的数据:
图中橙色 LuaJIT, 浅蓝色 Lua 5.4, 粉红色 C 语言, 数值越低越好. 可以看到 LuaJIT 的性能显著优于 Lua 最新的版本 5.4, 有些场景下甚至直逼 C 的性能. 一个直观的数字是, C 语言执行一段程序需要 1 秒的话, LuaJIT 执行大概需要 1.3 秒, 也就是说 LuaJIT 的性能大致是 C 语言的 77%
除了性能比官方 Lua 好之外, LuaJIT 还提供了调用外部 C 代码的 ffi 拓展, 免去了用户自己手写 C binding 的麻烦. 简单点说, 性能更好, 还更方便, 自然更多人用.
#2. LuaJIT 和 Lua 的关系
LuaJIT 是基于 Lua 5.1 的, 但也实现了 Lua 5.2 的绝大部分 feature 如 label, goto, load, math.log 等等. 那 LuaJIT 为什么没有完整的实现 Lua 5.2 呢? 因为 Lua 5.2 里的部分特性(_ENV)破坏了 ABI 稳定, 也就是以前编译出来的二进制可执行文件, 换了 5.2 之后直接不能跑了. 这对生产环境是不可接受的. 想象一下手机系统从 5.1 更新到 5.2, 以前安装的应用全都不能跑了, 那当然不更新了.
LuaJIT 是经过生产实践检验的, 稳定高效的 Lua 实现. Lua 语言去年已经发到了 5.4, 但目前广为使用并且还将长期占据主流的仍然是 LuaJIT. Lua 语言版本的割裂是制约它发展和流行的一个重要原因, 而编程语言作为一种工业界的产品, 最终会以社区广泛采用的版本为主流. LuaJIT 广泛运用在游戏客户端开发, 服务器端的开发平台 OpenResty 则 fork 并维护着自己版本的 LuaJIT. 不论在客户端和服务器端, LuaJIT 都占据着主流位置.
#3. LuaJIT 的机制
LuaJIT 是一个 JIT(Just-In-Time) 的编译器, 在函数第一次执行的时候做编译. 在运行时没用到的代码就不会被编译, 而且 LuaJIT 编译的速度很快, 在编译成本和编译效率上都做得很好.
LuaJIT 的启动, 按照运行先后顺序分别是: 新建一个 Lua 状态机, 加载标准库和命令行, 加载 Lua 文件, 把文件内容从字符串翻译成 bytecode, 然后执行. 听起来虽然不复杂, 实现起来却很难. 社区之前有人看了 LuaJIT 对 IR(Intermediate Representation, 中间表示符) 做优化的代码后感慨: Mike Pall is not human. (Mike Pall 是德国人, LuaJIT 的作者, 这句话是感慨他太厉害了, 不像人类)
LuaJIT 的编译是以函数为单元的, 每个函数都会带上一个状态码来标识编译状态(初值是 "None"), 如果函数没被编译, 会直接进入 LuaJIT 的后端(backend), 编译成机器码. 编译的过程基本是 bytecode 到机器码 1:1 的转换. 这部分是一个递归的过程, 函数中调用的别的函数也会被一起编译. 编译完成后函数的状态码会变成 "OK".
函数调用的过程, 在 LuaJIT 里是通过 call gates 来完成的. Call gates 会判断函数的编译状态并执行相应的操作, 没编译的去编译, 编译完的直接调用编译完的机器码. 编译过程有优先级次序, 优化算法的优先级较高, 而 debug 和 trace 的优先级较低.
LuaJIT 的编译优化模块是用 Lua 写的, 其中包括一些复杂的优化逻辑. 优化模块会忽略所有的控制语句(if, for, repeat), 并且检查操作对象的类型, 对应的生成 bytecode 以及类型提示, 这样编译器的后端就能根据提示(环境, debug信息, 类型信息等)去决定做指令替换, 合并, 还是做 C 内联等等.
对这样一个简单的函数:
function foo(t, k)
return t[k]
end
如果在运行时能知道 t 和 k 的类型, 对优化就有极大的帮助. JIT 在这里的优势是, 在编译函数时, 已经知道了传过来的参数类型, 可以根据参数的类型生成最优化的机器码. 当 t 是一个 table, k 是一个 number 时, 编译器会推断 t 是一个 array, 这样编译出来的结果, 比把 t 当 hash table 编译的性能高 3 倍.
#4. LuaJIT 的结构
编译器的典型结构是这样:
LuaJIT 的前端是轻量级的, 直接把表达式翻译成 bytecode, 中间没有生成 AST 这一步. 这是因为 Lua 的语法定义得非常精简, 也没有什么复杂的语法糖, 连 += 这种都没有. 最后生成的是一个 bytecode 的栈. bytecode 基本就是对操作符, 逻辑控制语句和关键字做一层抽象:
值得注意的是, LuaJIT 是有 Tailcall Optimization (尾部调用优化)的, 在尾部调用的深递归时可以复用内存, 可以无限层次的递归下去.
LuaJIT 的 IR 是 SSA(Static Single Assignment, 静态单步赋值)的形式:
IR 比 bytecode 更具体一些, 同样的bytecode, 也可能因为变量类型不同而产生不同的 IR. IR 更接近机器码, 在这一步的也有大量的数学和工程化的优化, 是整个 LuaJIT 性能提升的重点.
LuaJIT 的后端负责通过 IR 生成深度优化的机器码, 这个过程是面向硬件的, 所以涉及到多个指令集架构. LuaJIT 为了做机器码生成, 专门开发了一个名为 DynASM 的工具, 顾名思义是动态机器码生成器, 功能相当于 LLVM 的后端.
#5. 部分源代码分析
Lua 的运行时结构:
新建 Lua 运行时, 就是创建了这样一个结构体. 主要存储了栈的信息(上面三个红框)和 GC(最后一个红框), trace 的信息.
创建好运行时之后, 初始化 GC 和 Lua 的标准库:
然后开始执行主 Lua 文件:
loadfile 是一个读文件并编译成 bytecode 的过程, docall 则是执行 bytecode 的过程(从 bytecode 变成 IR 再变成机器码). 先看 loadfile. loadfile里的主要逻辑在这个函数里:
其中的 luaD_pcall 函数的第二个参数 f_parser 就是编译器的前端解析函数. 读入源代码之后会从主块开始执行, 循环翻译成 bytecode:
statement 函数就是编译成 bytecode 的主要逻辑:
bytecode 编译完成之后, 就是执行阶段了. docall 函数的主要逻辑基本是一个从 bytecode 到 IR 的转换:
再往后就是翻译成机器码执行的阶段了, 这个入口我没有找到, 只在 dynasm 模块里找到了 IR 到机器码翻译的部分:
到这里主要的逻辑就执行完毕了. LuaJIT 的代码还是很清晰的, 主要的实现都在 minilua.c 这个文件里.
#6. 其他
LuaJIT 的 trace 和优化策略非常多, 很多都是数学上和工程化上的优化, 而且有一些是非常前沿的创新型优化(比如用于优化动态内存分配的 Allocation Sinking Optimization), 文档也并不多. LuaJIT 3.0 的新 GC 算法目前也处于一个停顿的状态, 所以这里并没有再深入的挖掘内存分配和回收方便的内容. 考虑到 LuaJIT 的作者 Mike Pall 目前也没有把主要精力放在 LuaJIT 的升级上, LuaJIT 的下一个版本估计还要等挺久的时间了.
但不论如何, 现在的 LuaJIT 2.1 已经非常实用且好用了. 在服务器端的 OpenResty 还有自己维护的 LuaJIT 版本(添加了一些 OpenResty 需要的语言扩展, 修复了一些重要的bug). LuaJIT 已经足够成熟可靠, 能够广泛的运用于生产环境.
LuaJIT 本身是一个编译器的优秀范例, 阅读 LuaJIT 的代码也能让人对 JIT 编译器的实现有直观的认知. LuaJIT 和 Linux 内核一样, 开发人员都是用 mailing list 来通信, 并没有把主要的开发沟通过程放在 github 上, 所以信息也没有那么容易公开获取. 想了解 LuaJIT 的开发情况并和开发者沟通的话, 可以订阅 LuaJIT 的开发者 mailing list https://www.freelists.org/list/luajit
LuaJIT 是 C 和汇编写的, 源码阅读起来还是有一点难的, 不过源代码本身的质量过硬, 读下来也能学到不少.
LuaJIT 的介绍就到这里, 下一篇会讲一讲 Lua 语言的语法, 标准库和常用的工具库.
以上是关于LuaJIT 的研究笔记的主要内容,如果未能解决你的问题,请参考以下文章
RNN 代码的 luajit/lua5.1/lua5.2/lua5.3 内存问题