Mil*_*les 3 callback ffi luajit
该LuaJIT FFI文档提到,在C中调用回Lua代码是相对缓慢的,并建议避免它在可能的情况:
不要将回调用于对性能敏感的工作:例如,考虑一个数字集成例程,它采用用户定义的函数进行集成.从C代码调用用户定义的Lua函数数百万次是一个坏主意.回调开销对性能绝对不利.
对于新设计,请避免使用推式API(C函数会为每个结果重复调用回调).而是使用拉式API(重复调用C函数以获得新结果).通过FFI从Lua到C的呼叫比其他方式快得多.大多数设计良好的库已经使用了拉式API(读/写,get/put).
但是,他们并没有意识到C的回调速度有多慢.如果我有一些我希望加速使用回调的代码,那么如果我重新编写它以使用拉式API,我可以期待多少加速?有没有人有任何基准比较使用每种API的等效功能的实现?
在我的计算机上,从LuaJIT到C的函数调用具有5个时钟周期的开销(特别是,与通过普通C中的函数指针调用函数一样快),而从C调用回Lua有135个周期开销,慢了27倍.话虽这么说,需要从C进入Lua的百万次调用的程序只会给程序的运行时增加约100ms的开销; 尽管在一个紧密循环中避免FFI回调可能是值得的,这个回调主要在高速缓存数据中运行,如果它们被调用(例如,每次I/O操作一次),回调的开销可能不会明显. I/O本身的开销.
$ luajit-2.0.0-beta10 callback-bench.lua
C into C 3.344 nsec/call
Lua into C 3.345 nsec/call
C into Lua 75.386 nsec/call
Lua into Lua 0.557 nsec/call
C empty loop 0.557 nsec/call
Lua empty loop 0.557 nsec/call
$ sysctl -n machdep.cpu.brand_string
Intel(R) Core(TM) i5-3427U CPU @ 1.80GHz
Run Code Online (Sandbox Code Playgroud)
基准代码:https://gist.github.com/3726661
两年后,我根据Miles 的回答重新制定了基准,原因如下:
[...] C 到 Lua 的转换本身具有不可避免的成本,类似于 lua_call() 或 lua_pcall()。参数和结果编组会增加该成本 [...]
我的结果,在Intel(R) Core(TM) i7 CPU 920 @ 2.67GHz:
operation reps time(s) nsec/call
C into Lua set_v 10000000 0.498 49.817
C into Lua set_i 10000000 0.662 66.249
C into Lua set_d 10000000 0.681 68.143
C into Lua get_i 10000000 0.633 63.272
C into Lua get_d 10000000 0.650 64.990
Lua into C call(void) 100000000 0.381 3.807
Lua into C call(int) 100000000 0.381 3.815
Lua into C call(double) 100000000 0.415 4.154
Lua into Lua 100000000 0.104 1.039
C empty loop 1000000000 0.695 0.695
Lua empty loop 1000000000 0.693 0.693
PUSH style 1000000 0.158 158.256
PULL style 1000000 0.207 207.297
Run Code Online (Sandbox Code Playgroud)
这个结果的代码是here。
结论:当与参数一起使用时,C 回调到 Lua 有很大的开销(这几乎是你经常做的),所以它们真的不应该在关键点使用。不过,您可以将它们用于 IO 或用户输入。
我有点惊讶 PUSH/PULL 样式之间的差异如此之小,但也许我的实现不是最好的。
如以下结果所示,存在显着的性能差异:
LuaJIT 2.0.0-beta10 (Windows x64)
JIT: ON CMOV SSE2 SSE3 SSE4.1 fold cse dce fwd dse narrow loop abc sink fuse
n Push Time Pull Time Push Mem Pull Mem
256 0.000333 0 68 64
4096 0.002999 0.001333 188 124
65536 0.037999 0.017333 2108 1084
1048576 0.588333 0.255 32828 16444
16777216 9.535666 4.282999 524348 262204
Run Code Online (Sandbox Code Playgroud)
可以在此处找到此基准测试的代码。
由于这个问题(以及一般的 LJ)一直是我痛苦的根源,因此我想将一些额外的信息扔进戒指中,希望它可以在将来对那里的人有所帮助。
LuaJIT FFI 文档,当它说“回调很慢”时,非常具体地指的是由 LuaJIT 创建并通过 FFI 传递给需要函数指针的 C 函数的回调的情况。这与其他回调机制完全不同,特别是与调用标准的 lua_CFunction 使用 API 调用回调相比,它具有完全不同的性能特征。
话虽如此,真正的问题是:我们什么时候使用 Lua C API 来实现涉及 pcall 等的逻辑,而不是将所有内容都保留在 Lua 中?与性能一样,但尤其是在跟踪 JIT 的情况下,必须分析(-jp) 才能知道答案。时期。
我见过看起来相似但又处于性能范围两端的情况;也就是说,我遇到的代码(不是玩具代码,而是在编写高性能游戏引擎的上下文中的生产代码)在结构为 Lua-only 时性能更好,以及代码(看起来结构相似)通过调用使用 luaL_ref 维护回调和回调参数的句柄的 lua_CFunction 引入语言边界时,性能更好。
即使您是静态语言性能分析方面的专家,也很难理解跟踪 JIT。他们把你认为你知道的关于性能的所有东西都打碎了。如果编译记录的 IR 而不是编译函数的概念还没有消除人们对 LuaJIT 性能进行推理的能力,那么成功 JIT 后通过 FFI 调用 C 的事实或多或少是免费的,但可能是一个命令-在解释时,比等效的 lua_CFunction 调用更昂贵……好吧,这肯定会将情况推向边缘。
具体来说,你上周编写的一个系统性能远远超过 C 等价物,本周可能会崩溃,因为你引入了一个接近于所述系统的 NYI,它很可能来自一个看似正交的代码区域,现在您的系统正在回退并降低性能。更糟糕的是,也许您很清楚什么是 NYI,什么不是 NYI,但是您向跟踪接近度添加了足够多的代码,它超过了 JIT 的最大记录 IR 指令、最大虚拟寄存器、调用深度、展开因子,侧线限制...等。
另外,请注意,虽然“空”基准有时可以提供非常普遍的洞察力,但对于 LJ(出于上述原因),在上下文中对代码进行概要分析更为重要。为 LuaJIT 编写具有代表性的性能基准是非常非常困难的,因为跟踪本质上是非本地的。在大型应用程序中使用 LJ 时,这些非本地交互会产生巨大的影响。
这个星球上只有一个人真正了解 LuaJIT 的行为。他的名字是迈克·鲍尔。
如果您不是 Mike Pall,请不要对 LJ 的行为和表现做出任何假设。使用-jv(详细;注意 NYIs 和回退),- jp(分析器!结合 jit.zone 进行自定义注释;使用 -jp=vf 来查看由于回退而在解释器中花费的时间百分比) , 并且,当你真的需要知道发生了什么时,-jdump(跟踪 IR 和 ASM)。测量,测量,再测量。对 LJ 性能特征的概括持保留态度,除非它们来自该人本人或者您已经在您的特定使用案例中对其进行了测量(毕竟,在这种情况下,这不是概括)。请记住,正确的解决方案可能全部在 Lua 中,也可能全部在 C 中,可能是 Lua -> C 通过 FFI,也可能是 Lua -> lua_CFunction -> Lua,......你明白了。
来自一个一次又一次地被愚弄的人认为他已经理解了 LuaJIT,但在接下来的一周中被证明是错误的,我真诚地希望这些信息能帮助那里的人:) 就个人而言,我只是不再做 '关于 LuaJIT 的有根据的猜测。我的引擎每次运行都会输出 jv 和 jp 日志,就优化而言,它们是我的“上帝之言”。