luajit开发文档wiki中文版(三)性能调优和测试

2022年6月10日14:37:02

 

luajit开发文档中文版(一)下载和安装

luajit开发文档中文版(二)LuaJIT扩展

luajit开发文档中文版(三)FAQ 常见问题

 

luajit开发文档wiki中文版(一) 总目录

luajit开发文档wiki中文版(二) LuaJIT 扩展

luajit开发文档wiki中文版(三)性能调优和测试

luajit开发文档wiki中文版(四) LuaJIT 内部结构

luajit开发文档wiki中文版(五) 系统集成

luajit开发文档wiki中文版(六) LuaJIT 开发

尚未实现的功能

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

下表列出了对各种内置库函数的调用是否会被编译。这可能取决于传递的参数(尤其是它们的类型)和调用的确切情况。

基础库

FunctionCompiled?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)

该库正在进行中。更多功能将很快添加。尚未编译任何内容。

FunctionCompiled?Remarks
buffer.encode stitch  
buffer.decode stitch  

String Library

FunctionCompiled?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

FunctionCompiled?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

FunctionCompiled?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

FunctionCompiled?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

FunctionCompiled?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

FunctionCompiled?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 上缝合)

调试库

FunctionCompiled?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- 你会看到哪些行没有被执行。

当然,正如我之前所说,并非所有未使用的代码都可以删除。

 

 

数值计算性能指南

这是Mike Pall 的邮件列表帖子

使用 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 的输出。

在决定某种算法之前,您需要考虑所有这些因素。如果更简单的算法具有更少的无偏分支,则理论上快速的高级算法可能比更简单的算法慢。

posted on 2022-06-13 10:01  zh7314  阅读(725)  评论(0编辑  收藏  举报