尚未实现的功能
Lua 的所有方面都在 LuaJIT 的解释器中实现,但并非所有方面都在 LuaJIT 的 JIT 编译器中实现。此页面可作为快速参考来确定某些事情是否已实施。希望这意味着您可以在性能关键代码中避免它们,而不是困惑为什么您会看到性能不佳的代码和来自-jv
.
请注意,LuaJIT 的目标不仅是生成快速代码,而且还拥有一个快速且紧凑的 JIT 编译器。编译所有内容并不是一个既定目标,因为解释器对于许多任务来说足够快。对于在程序运行期间只完成几次的任何事情,速度并不重要。例如,编译require()
或module()
(事实上,这些最终应该被重写为普通的 Lua 函数)是绝对没有意义的。
但是,根据需求和用户反馈,JIT 编译案例的数量会随着时间的推移而增加。
下表提供了一个特性是否是 JIT 编译的指示:
- yes - 始终 JIT 编译。
- partial - 可能是 JIT 编译的,具体取决于具体情况。否则将退回到解释器或缝合。
- bytecode - 该函数在 LuaJIT 2.1 中使用 lua 代码实现。
- 2.1 - 从 LuaJIT 2.1 开始编译。
- no - 不是 JIT 编译的(还),总是回退到解释器。
- never - 同上。即使在未来的版本中,也不会被 JIT 编译。
字节码
几乎所有字节码都被编译,除了这些:
字节码 | 编译? | 评论 |
CAT | 2.1 | 连接运算符'..'。 |
FNEW | 不 | 创建闭包。 |
FUNCC | 2.1针 | 通过经典 API 调用 C 函数。 |
FUNCCW | 2.1针 | 通过经典 API 调用封装的 C 函数。 |
FUNC* | 部分的 | 调用内置函数。见下文。 |
ISNEXT | 不 | 检查 next() 循环优化。如果值为 `next` 函数,则为 NYI,否则为否。 |
ITERN | 不 | 优化循环中对 next() 的调用。 |
IFORL | 不 | 解释器强制调用 FORL 指令。 |
ITERL | 不 | 解释器强制调用 ITERL 指令。 |
ILOOP | 不 | 解释器强制调用 LOOP 指令。 |
IFUNCF | 不 | 解释器强制调用 FUNCF 指令。 |
IFUNCV | 不 | 解释器强制调用 FUNCV 指令。 |
CALLT | 部分的 | 尾声。未编译对低于跟踪起始帧的帧的一些尾调用。 |
RET* | 部分的 | 从函数返回。返回到 C 帧和一些返回到低于跟踪起始帧的帧不会被编译。 |
TSETM | 2.1 | 使用多个返回值初始化表。 |
UCLO | 不 | 关闭upvalues。 |
VARG | 部分的 | Vararg 运算符'...'。多结果 VARG 仅在与 select() 一起使用时编译(并且与 const 正数一起使用)。 |
笔记:
- 未编译对混合密集/稀疏表的表访问。
- 永远不会编译会导致解释器出错的字节码执行。
- LuaJIT 2.1 添加了跟踪拼接功能,允许跟踪在经典的 C 函数或未编译的内置函数处停止,返回解释器,运行 C 函数或内置函数,然后在返回后开始新的跟踪。这不是特别有效,但它避免了由于 NYI 函数而导致的跟踪中止,这以前会强制解释此类函数周围的整个代码路径。
您将在-jv
输出中看到字节码编号。您可以使用此 bcname.lua 脚本将它们转换为它们的名称或:
$ # source: http://www.freelists.org/post/luajit/frames-and-tail-calls,1
$ cat >bcname.lua <<'END'
local x = tonumber(arg[1])
print(string.sub(require("jit.vmdef").bcnames, x*6+1, x*6+6))
END
$ luajit-2.1 bcname.lua 71
VARG
库
下表列出了对各种内置库函数的调用是否会被编译。这可能取决于传递的参数(尤其是它们的类型)和调用的确切情况。
基础库
Function | Compiled? | Remarks |
assert | yes | |
collectgarbage | 2.1 stitch | |
gcinfo | 2.1 stitch | |
dofile | never 2.1 stitch | |
error | never | |
getfenv | 2.1 partial | 只有 getfenv(0) 被编译。 |
getmetatable | yes | |
ipairs | yes | |
load | never 2.1 stitch | |
loadfile | never 2.1 stitch | |
loadstring | never 2.1 stitch | |
newproxy | 2.1 stitch | |
next | 2.1 partial | NYI 如果 ISNEXT 在通用 for 循环中找到此函数,否则缝合 |
pairs | yes | 对本身不会触发 NYI,请参阅“下一步”功能。 |
pcall | yes | |
print | partial | NYI 如果发现错误。始终在 2.0 中,在 2.1 中表现不同。例如,如果调用了“error”函数,则编译,但如果调用了“assert”函数,则 NYI,其他例外适用 |
rawequal | yes | |
rawget | yes | |
rawlen (5.2) | yes | |
rawset | yes | |
require | 2.1 stitch | |
select | partial | 仅当第一个参数是常量时编译(如果与 varg 一起使用,则必须为正)。 |
setfenv | 2.1 stitch | |
setmetatable | yes | |
tonumber | partial | 不会为 10 以外的基数编译,其他例外情况适用。 |
tostring | partial | 仅针对带有 __tostring 元方法的字符串、数字、布尔值、nil 和值进行编译。 |
type | yes | |
unpack | 2.1 stitch | |
xpcall | partial | 参见“pcall”。 |
字符串缓冲区库 (2.1)
该库正在进行中。更多功能将很快添加。尚未编译任何内容。
Function | Compiled? | Remarks |
buffer.encode | stitch | |
buffer.decode | stitch | |
String Library
Function | Compiled? | Remarks |
string.byte | yes | |
string.char | 2.1 | |
string.dump | never 2.1 stitch | |
string.find | 2.1 partial | 仅纯字符串搜索(无模式)。 |
string.format | 2.1 partial | 不适用于 %p 或 %s 的非字符串参数 |
string.gmatch | 2.1 stitch | |
string.gsub | 2.1 stitch | |
string.len | yes bytecode | |
string.lower | 2.1 | |
string.match | 2.1 stitch | |
string.rep | 2.1 | |
string.reverse | 2.1 | |
string.sub | yes | |
string.upper | 2.1 | |
Table Library
Function | Compiled? | Remarks |
table.concat | 2.1 | |
table.foreach | no bytecode | NYI 在 ITERN |
table.foreachi | 2.1 bytecode | |
table.getn | yes bytecode | |
table.insert | partial | 只有pushing的时候。 |
table.maxn | 2.1 stitch | |
table.pack (5.2) | 2.1 stitch | |
table.remove | 2.1 bytecode | 部分在 2.0 中:仅在popping. |
table.move (5.3) | yes bytecode | |
table.sort | 2.1 stitch | |
table.unpack (5.2) | 2.1 stitch | |
Math Library
Function | Compiled? | Remarks |
math.abs | yes | |
math.acos | yes | |
math.asin | yes | |
math.atan | yes | |
math.atan2 | yes | |
math.ceil | yes | |
math.cos | yes | |
math.cosh | yes | |
math.deg | yes bytecode | |
math.exp | yes | |
math.floor | yes | |
math.fmod | 2.1 stitch | |
math.frexp | 2.1 stitch | |
math.ldexp | yes | |
math.log | yes | |
math.log10 | yes | |
math.max | yes | |
math.min | yes | |
math.modf | yes | |
math.pow | yes | |
math.rad | yes bytecode | |
math.random | yes | |
math.randomseed | 2.1 stitch | |
math.sin | yes | |
math.sinh | yes | |
math.sqrt | yes | |
math.tan | yes | |
math.tanh | yes | |
IO Library
Function | Compiled? | Remarks |
io.close | 2.1 stitch | |
io.flush | yes | |
io.input | 2.1 stitch | |
io.lines | 2.1 stitch | |
io.open | 2.1 stitch | |
io.output | 2.1 stitch | |
io.popen | 2.1 stitch | |
io.read | 2.1 stitch | |
io.tmpfile | 2.1 stitch | |
io.type | 2.1 stitch | |
io.write | yes | |
Bit Library
Function | Compiled? | Remarks |
bit.arshift | yes | |
bit.band | yes | |
bit.bnot | yes | |
bit.bor | yes | |
bit.bswap | yes | |
bit.bxor | yes | |
bit.lshift | yes | |
bit.rol | yes | |
bit.ror | yes | |
bit.rshift | yes | |
bit.tobit | yes | |
bit.tohex | 2.1 | |
FFI Library
Function | Compiled? | Remarks |
ffi.alignof | yes | |
ffi.abi | yes | |
ffi.cast | partial | 与 ffi.new 相同的限制(强制转换是 cdata 创建的一种形式)。 |
ffi.cdef | never 2.1 stitch | |
ffi.copy | yes | |
ffi.errno | partial | 不是在设置新值时。 |
ffi.fill | yes | |
ffi.gc | 2.1 | 部分在 2.0 中:不是在清除终结器时。 |
ffi.istype | yes | |
ffi.load | never 2.1 stitch | |
ffi.metatype | never 2.1 stitch | |
ffi.new | partial | 2.0:不适用于 VLA/VLS,> 8 字节对齐或 > 128 字节。2.1:不适用于 VLA/VLS 或 > 128 字节或 > 16 个数组元素的非默认初始化 |
ffi.offsetof | yes | |
ffi.sizeof | partial | 不适用于 VLA/VLS 类型(见下文) |
ffi.string | yes | |
ffi.typeof | partial | 仅适用于 cdata 参数。从不用于 cdecl 字符串 |
有关更多详细信息,请参阅FFI 库的当前实施状态。
注意:避免ffi.sizeof(cdata)
使用可变长度类型 (VLA/VLS)。此调用未编译,并且可能成为使用New Garbage Collector的相当昂贵的操作,因为实际长度可能不再存储在 cdata 对象中。
协程库
不编译任何函数。(在 2.1 上缝合)
操作系统库
不编译任何函数。(在 2.1 上缝合)
包库
不编译任何函数(将替换为内置字节码)。(在 2.1 上缝合)
调试库
Function | Compiled? | Remarks |
debug.getmetatable | 2.1 | |
debug.* | no/never | Unlikely to change |
JIT 库
没有函数被编译(不太可能改变,除了可能的未来编译器提示)。(在 2.1 上缝合)
减少错误的测试用例
所以你认为你在 LuaJIT 中发现了一个错误?
屏住呼吸一分钟——你确定这不是你的错吗?
使用 PUC 运行您的代码lua
,使用 luajit -joff
并检查您没有看到错误,并且使用luajit
.
检查您是否不依赖任何未定义的行为。我曾经以为我发现了一个错误,但实际上我是依赖于pairs
遍历哈希表的顺序。两者lua
并且luajit -joff
总是以完全相同的顺序luajit
遍历表,但有时合法地以不同的顺序遍历表。
如果你仍然认为这可能是 LuaJIT 的错,请继续阅读......
这些东西很难抓
JIT 错误很难捕捉。JIT 编译器使用许多启发式方法来决定要编译哪些跟踪,并且您出现的错误可能取决于从正确的行开始的特定跟踪被选择在正确的时间进行编译。
几乎任何东西都可以触发选择跟踪的启发式方法并使错误消失,因此将代码缩减为更小的测试用例的过程可能会很慢。即使删除未使用的代码也可以移动或隐藏问题 - 但有时您在某个阶段无法删除的代码可能会在其他更改之后被删除。
坚持到底会赢。
别print
如果你像我一样,你会很想print
在你的代码中加入 s 来看看你是否能找出发生了什么。 print
没有作为快速函数实现,因此包含的跟踪print
被中止。因此,如果您print
在问题跟踪的中间放置一个,您将停止正在编译的跟踪并隐藏您的问题。
如果您想获得一些io.write
不会中止跟踪的输出使用,但请注意 - 它确实会激发启发式方法。
转储它
尝试转储您的代码: luajit -jdump=+rsx,<dump_file_name> <file.lua>
. 不幸的是,记录转储也会影响启发式方法,从而移动或隐藏问题。
转储文件越小,最终追查问题就越容易,使用文件的大小作为衡量您在减少案例方面做得如何的指标。
尝试所有选项
LuaJIT 的不同编译器选项可以移动、隐藏或暴露问题,因此值得尝试其中的一些来找出问题。我发现通常有一个值-Ohotloop=X
会关闭错误,所以值得尝试一些。
这是一个 shell 脚本,它将使用一系列编译器选项运行您的代码。它假定您的代码在发生错误时具有非零退出代码。
lua$1 > /dev/null
echo "lua OK"
luajit -joff $1 > /dev/null
echo "luajit -joff OK" for i in {1..50} do echo -Ohotloop=$i luajit -Ohotloop=$i -jdump=+rsx,test.dump $1 > /dev/null if [ $? -ne 0 ] then echo "Error on " luajit -Ohotloop=$i -jdump=+rsx,test.dump $1 "> /dev/null" mv test.dump error.dump for o in "" "1" "2" "3" "-fold" "-cse" "-dce" "-narrow" "-loop" "-fwd" "-dse" "-abc" "-fuse" do echo -O$o -Ohotloop=$i luajit -O$o -Ohotloop=$i -jdump=+rsx,test.dump $1 > /dev/null done break fi done rm test.dump
关闭编译器
即使您有一个非常大的测试用例并且不能减少太多,问题跟踪仍然可能是一小段代码。您可以通过逐一关闭模块的 jit 编译器来减小转储的大小并跟踪问题。放在if jit then jit.off(true, true) end
模块的顶部,看看问题是否消失。如果它确实消失了,请移除该线。如果问题仍然存在,则不需要编译该模块来显示问题,并且您只是使转储更小。
查找未使用的代码
对不起这个无耻的插件,但我使用 luatrace 来查找可以(可能)删除的未使用代码,再次减少了测试用例的大小。运行lua -luatrace.profile <file.lua>
并查看annotated-source.txt
- 你会看到哪些行没有被执行。
当然,正如我之前所说,并非所有未使用的代码都可以删除。
数值计算性能指南
使用 LuaJIT 获得最佳数值计算速度需要以下事项(按重要性排序):
- 减少无偏/不可预测分支的数量。
- 严重偏向的分支(在一个方向上>95%)很好。
- 首选无分支算法。
- 使用 math.min() 和 math.max()。
- 使用 bit.*,例如用于条件索引计算。
- 使用 FFI 数据结构。
- 使用 int32_t,避免使用 uint32_t 数据类型。
- 使用双精度,避免浮点数据类型。
- 元方法很好,但不要过度使用它们。
- 仅通过 FFI 调用 C 函数。
- 避免调用琐碎的函数,最好在 Lua 中重写它们。
- 避免回调——改用拉式 API(读/写)和迭代器。
- 使用简单的“for i=start,stop,step do ... end”循环。
- 首选普通数组索引,例如'a[i+2]'。
- 避免指针算术。
- 为展开找到合适的平衡点。
- 避免迭代计数低(< 10)的内部循环。
- 仅当循环体没有太多指令时才展开循环。
- 考虑使用模板而不是手动展开(请参阅 GSL Shell)。
- 您可能需要进行一些实验。
- 只定义和调用模块中的“local”(!) 函数。
- 在 upvalues 中缓存来自其他模块的常用函数。
- 例如 local sin = math.sin ... local function foo() return 2*sin(x) end
- 不要对 FFI C 函数执行此操作,而是缓存命名空间,例如 local lib = ffi.load("lib")。
- 避免发明自己的调度机制。
- 更喜欢使用内置机制,例如元方法。
- 不要试图猜测 JIT 编译器。
- 写 'z = x[a+b] + y[a+b]' 是完全可以的。
- 不要手动尝试 CSE(通用子表达式消除),例如 'local c = a+b'。
- 如果临时的生命周期比需要的长,它可能会变得有害。如果编译器不能推断出它已经死了,那么无用的临时将阻塞一个寄存器或堆栈槽和/或它需要存储到 Lua 堆栈中。
- 涉及彼此相对接近的基本算术运算符的重复表达式(并且可能在同一跟踪中)不应手动 CSEd。如果别名分析可能失败,则只需手动提升负载。
- 写 'a[i][j] = a[i][j] * a[i][j+1]' 是完全可以的。
- 不要尝试缓存部分 FFI 结构/数组引用(例如 a[i]),除非它们是长期存在的(例如在大循环中)。
- 有很多“简单”的优化,编译器可以更好地执行它们。更好地关注困难的事情,比如算法改进。
- 小心混叠,尤其是。使用多个数组时。
- LuaJIT 使用严格的基于类型的消歧,但由于符合 C99,这有一定的限制。
- 例如在 'x[i] = a[i] + c[i]; y[i] = a[i] + d[i]' a[i] 的加载需要执行两次,因为 x 可以别名 a。在这里使用 'do local t = a[i] ... end' 确实有意义。
- 减少实时临时变量的数量。
- 最好在定义时初始化,例如'local y = f(x)'
- 是的,这意味着您应该将其与其他代码交错。
- 不要将变量定义提升到函数的开头——Lua 不是 JavaScript 也不是 K&R C。
- 使用 'do local y = f(x) ... end' 来绑定变量生命周期。
- 不要散布昂贵或未编译的操作。
- print() 未编译,使用 io.write()。
- 例如避免 assert(type(x) == "number", "x is a "..mytype(x)")
- 问题不在于 assert() 或条件(基本上是免费的)。问题是字符串连接,每次都必须执行,即使断言永远不会失败!
- 观察 -jv 和 -jdump 的输出。
在决定某种算法之前,您需要考虑所有这些因素。如果更简单的算法具有更少的无偏分支,则理论上快速的高级算法可能比更简单的算法慢。