回复:关于在LuaJIT2中实现高效指令集模拟器的建议(2011年)
摘要
这个2011年的邮件列表讨论串探讨了编译器在优化解释器循环方面的挑战,以及LuaJIT2如何通过固定寄存器分配等汇编级技术实现更好的性能。
<p><a href="https://lobste.rs/s/v4bdwl/re_suggestions_on_implementing">评论</a></p>
查看缓存全文
缓存时间: 2026/09/23 08:47
# Gmane -- 邮件转新闻组再溯源头
来源:https://web.archive.org/web/20180603053407/http://article.gmane.org/gmane.comp.lang.lua.general/75426
```
Josh Haberman 写道:
> Josh Haberman gmail.com> 提问:
> > Mike Pall mike.de> 回应:
> > > 解释器主循环对编译器(及CPU)而言始终是艰巨挑战。大多数解释器采用C语言编写,
> > > 因此C编译器多年来针对此场景持续优化。但即便如此,其生成的代码相比手写汇编
> > > 仍显平庸。
> >
> > 您能否总结在优化解释器循环时,超越C编译器的几种核心策略?我对这个问题极
> > > 感兴趣。虽然能(也确实在)研读LuaJIT代码,但这无法揭示原始编译输出存在
> > > 哪些不足。
>
> 嗯,这个问题可能过于宽泛。或许更精准的提问应是:在解释器主循环场景中,编
> > > 译器至今仍存在哪些缺陷?通常认为现代编译器已足够优秀,人类几乎无法超越。
> > > 虽然LuaJIT明显突破了此观念,但具体体现在哪些方面?编译器在解释器循环中
> > > 究竟做了哪些错误决策?
采用C语言switch结构的解释器控制流图如下:
.------.
V |
| |
L | L = 指令加载
D | D = 指令分派
/ /|\ \ |
/ / | \ \ |
C C C C C | C = 操作数解码
X X X X X | X = 指令执行
\ \ | / / |
\ \|/ / |
| |
V |
`------'
单条指令的执行流程呈现为:
......V......
:X | :
: |\ \ :
: F S S : F = 快速路径
: |/ / : S = 慢速路径
: | :
:.....V.....:
此处涉及数十种指令与数百条慢速路径。编译器需处理整套复杂流程,由此引发三类问题:
* 菱形控制流被证实为多数优化与寄存器分配的最差场景,嵌套的菱形结构更为棘手。
* 编译器缺乏足够信息区分快速路径与慢速路径。即便提供相关提示,仍将其视为单一巨型控制流图。
循环内任何操作都可能影响其他部分,因此几乎无法进行代码提升或消除。慢速路径阻碍快速路径优化机会,复杂指令则破坏简单指令的优化可能。
* 常规寄存器分配启发式算法在此规模下失效,导致编译器难以将关键变量保留在寄存器中。
我们亦可使用直接/间接线程化的C语言解释器,例如利用GCC的computed 'goto &'特性:
* * * * *
| | | | |
C C C C C C = 操作数解码
X X X X X X = 指令执行
L L L L L L = 下一指令加载
D D D D D D = 下一指令分派
| | | | |
V V V V V
这有效复制了加载与分派操作,有助于CPU分支预测。但仍存在固有缺陷:
* 编译器无法识别整体循环结构。所有goto指令可跳转至代码任意位置,导致编译器无法提升任何操作——总会存在慢速路径破坏别名存储的所有优化机会。
* 寄存器分配器只能分段处理,效果极差。无法为其设定"每次goto前保持相同寄存器分配"这类目标函数。
* 尾部合并与公共子表达式消除会将所有指令的常见尾部合并为单一的分派点。糟糕!虽可尝试为此段代码禁用部分优化,但会损害所有执行路径。
* 一旦开始与编译器对抗,便陷入了必败之局。
若用汇编编写解释器循环,则能实现更优效果:
* 为所有指令保持固定的寄存器分配方案。
* 快速路径中将所有数据保留在寄存器,仅慢速路径进行溢出/重载操作。
* 将慢速路径移至独立区域,提升指令缓存密度。
* 预加载指令并预解码操作数。
具体形态如下:
* * * * *
| | | | |
C C C C C C = 当前指令的部分操作数解码
F> F> F> F> F> F = 快速路径,> = 退出至慢速路径
L L L L L L = 下一指令加载
C C C C C C = 下一指令的部分操作数解码
D D D D D D = 下一指令分派
| | | | |
V V V V V
最终可精简至数条机器指令。曾在Reddit发布x86版LuaJIT解释器示例:
http://www.reddit.com/r/programming/comments/badl2/luajit_2_beta_3_is_out_support_both_x32_x64/c0lrus0
在PPC/e500架构上需运用更多技巧:例如合并操作数解码与索引缩放('rlwinm'指令在此极具价值),或手动调度快速路径中的指令加载次序,甚至使用向量指令进行类型检查。
任何先进的C编译器都无法自动完成这些优化。
解释器还有其他设计目标,例如每个字节码指令仅保留单条快速路径等...此处不再展开,但请谨记:每个百分点的优化都至关重要!
结语:现代编译器包含大量相互影响的启发式算法,针对"平均"代码场景调优。而解释器本质上是截然不同的存在,因此必将带来令人失望的编译结果。
--Mike
```
相似文章
使用libgccjit为玩具解释器添加JIT编译
本教程演示了如何使用libgccjit为简单的基于栈的解释器添加JIT编译,包括代码示例和解释。
LuaJIT 3.0 提议的语法扩展
LuaJIT 3.0 提议的语法扩展已公布,为 Lua 的 LuaJIT 即时编译器带来潜在的新功能。
Tiny-Lua-Compiler: 可能是有史以来最小的 Lua 编译器
Tiny-Lua-Compiler 是一个用于教学的、自举的 Lua 5.1 编译器和虚拟机,完全用纯 Lua 编写。其设计目标是体积足够小以便于研究,同时又功能完备到足以处理真实的语言特性。
如何构建高性能动态语言解释器
本文是一篇深度技术分析,详细阐述了如何针对动态类型语言 Zef 优化基于抽象语法树(AST)遍历的解释器。通过改进值的内部表示、引入内联缓存、优化对象模型及其他多项加速技术,最终实现了 16 倍的运行速度提升,使 Zef 的性能达到了可与 Lua、QuickJS 和 CPython 相媲美的高水准。
LuaJIT 中的一个 NYI 操作悄然毒害了无关的热循环
本文探讨了 LuaJIT 的一个陷阱:诸如 unpack 之类的未实现(NYI)操作会悄然导致 trace 黑名单化,从而使基准测试性能降低 20 倍,并提供了在 CI 中防范此类问题的方法。