LuaJIT FFI docs提到从C调用Lua代码的速度相对较慢,建议尽可能避免使用它:
不要将回调用于对性能敏感的工作:例如考虑一个数值集成例程,它采用用户定义的函数进行集成。从C代码调用用户定义的Lua函数数百万次是一个坏主意。回调开销对性能绝对不利。
对于新设计,请避免推式API(C函数会为每个结果重复调用回调)。而是使用拉式API(重复调用C函数以获得新结果)。通过FFI从Lua到C的呼叫比其他方式快得多。大多数设计良好的库已经使用了拉式API(读/写,获取/放置)。
然而,他们并没有给出任何来自C的回调次数多少的感觉。如果我有一些我希望加速使用回调的代码,那么如果我重新编写它以使用拉式API,我可以期待多少加速?有没有人有任何基准来比较使用每种API的等效功能的实现?
答案 0 :(得分:6)
在我的计算机上,从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
答案 1 :(得分:4)
这些结果显示出显着的性能差异:
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
可以找到此基准的代码here。
答案 2 :(得分:4)
两年后,我从Miles' answer重新编写基准,原因如下:
[...] C to 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
此结果的代码为here。
结论:与参数一起使用时,进入Lua的C回调有很大的开销(这是你几乎总是这样做的),所以它们真的不应该用在关键点上。您可以将它们用于IO或用户输入。
我有点惊讶PUSH / PULL风格之间差别不大,但也许我的实施并不是最好的。
答案 3 :(得分:2)
由于这个问题(以及一般的LJ)一直是我痛苦的根源,我想在戒指中加入一些额外的信息,希望将来可以帮助那些人。
LuaJIT FFI文档,当它说回调很慢时,'将非常具体地引用到由LuaJIT创建的回调的情况,并通过FFI传递给期望函数指针的C函数。这与其他回调机制完全不同,特别是与调用使用API调用回调的标准lua_CFunction相比,它具有完全不同的性能特征。
话虽如此,真正的问题是:我们何时使用Lua C API来实现涉及pcall等的逻辑,而不是保留Lua中的所有内容?与性能一样,但尤其是在跟踪JIT 的情况下,一个必须分析( - jp)才能知道答案。周期。
我看到类似的情况却落在了性能范围的两端;也就是说,我遇到了代码(不是玩具代码,而是在编写高性能游戏引擎的上下文中的生产代码),当结构化为Lua-only以及代码(似乎通过调用使用luaL_ref维护回调和回调参数句柄的lua_CFunction,在引入语言边界时表现更好的em>结构相似。
即使您是静态语言性能分析专家,追踪JIT也很难理解。他们会把您认为对性能有所了解的所有内容都打包成碎片。如果编译记录的IR而不是编译函数的概念并没有消除推理LuaJIT性能的能力,那么当成功JIT时,通过FFI调用C的事实或多或少是免费的。但是,当解释时,它可能比等效的lua_CFunction调用更加昂贵......好吧,这肯定会推动这种情况超越边缘。
具体来说,你上周写的一个大大超过C等价物的系统本周可能因为你引入了一个跟踪所述系统的NYI,这可能来自一个看似正交的代码区域,现在你的系统正在退缩并消除性能。更糟糕的是,也许您已经清楚地意识到NYI是什么和不是什么,但是您已经将足够的代码添加到它超出JIT的跟踪距离中。最大记录的IR指令,最大虚拟寄存器,调用深度,展开因子,侧面跟踪限制......等等。
另外,请注意,虽然“空”'基准测试有时可以提供非常一般的见解,对于LJ(出于上述原因),代码在上下文中被分析更为重要。为LuaJIT编写具有代表性的性能基准是非常非常困难的,因为痕迹本质上是非本地的。在大型应用程序中使用LJ时,这些非本地交互会产生极大的影响。
这个星球上确实有一个的人真正了解LuaJIT的行为。他的名字叫Mike Pall。
如果你不是Mike Pall,那么不会假设有关LJ的行为和表现。使用 -jv (详细;注意NYI和后备), -jp (探查器!与jit.zone结合使用自定义注释;使用-jp = vf查看%你的时间因为后备而花费在翻译中,并且,当你真的需要知道发生了什么时, -jdump (追踪IR & ASM)。测量,测量,测量。对LJ性能特征进行概括,除非它们来自人本人,或者你在特定的使用案例中对它们进行了测量(在这种情况下,它毕竟不是概括)。请记住,正确的解决方案可能全部在Lua中,它可能全部在C中,它可能是Lua - > C到FFI,它可能是Lua - > lua_CFunction - > Lua,......你明白了。
来自一次被愚弄的人再次认为他已经理解了LuaJIT,但在接下来的一周被证明是错的,我真诚地希望这些信息可以帮助那里的人:)就个人而言,我根本就没有更长时间的教育猜测'关于LuaJIT。我的引擎输出每次运行的jv和jp日志,它们是上帝的话语'对我而言,在优化方面。