LuaJIT FFI回调性能

7

LuaJIT FFI文档提到,从C回调到Lua代码的调用相对较慢,并建议尽可能避免:

不要在性能敏感型工作中使用回调:例如考虑一个数值积分例程,它需要一个用户定义的函数来进行积分。从C代码中调用一个用户定义的Lua函数数百万次是个坏主意。回调开销将会对性能产生绝对的负面影响。

对于新设计,请避免使用推式API(C函数重复调用每个结果的回调)。而是使用拉式API(反复调用C函数以获取新结果)。通过FFI从Lua到C的调用比另一种方式快得多。大多数设计良好的库已经使用拉式API(读/写,获取/放置)。

然而,他们没有给出回调从C语言到Lua语言有多慢的概念。如果我有一些想要加速的使用回调的代码,如果我重写它以使用拉式API,我可以期望获得多少加速?是否有任何基准测试来比较使用每种API风格实现等效功能的实现?

4个回答

11

在我的计算机上,从LuaJIT调用C函数的开销为5个时钟周期(值得注意的是,在纯C中通过函数指针调用函数同样快),而从C回调Lua则需要135个时钟周期,慢27倍。尽管如此,对于需要从C调用Lua一百万次的程序来说,只会给程序的运行时间增加约100毫秒的开销;虽然避免在大部分缓存数据的紧密循环中使用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

基准测试代码:https://gist.github.com/3726661


很棒的代码片段,不过如果里面还有一些相对标准的编译器指令(例如:gcc -shared...),我会更加感激。 - Aktau

8

由于这个问题(以及LJ总体)给我带来了巨大的痛苦,我想在这里提供一些额外的信息,希望能帮助未来的某个人。

“回调”并不总是慢

LuaJIT FFI文档中所说的“回调很慢”是指通过FFI将由LuaJIT创建并传递给期望函数指针的C函数的回调。这与其他回调机制完全不同,特别是与使用API调用回调的标准lua_CFunction相比,具有完全不同的性能特征。

因此,真正的问题是:我们何时使用Lua C API实现涉及pcall等内容的逻辑,而不是将所有内容保留在Lua中?始终如一地考虑性能,但尤其是在跟踪JIT的情况下,必须进行分析(-jp)以了解答案。无论如何,必须进行性能分析。

我曾经遇到过看起来相似但性能却截然不同的情况;也就是说,我遇到过代码(不是玩具代码,而是在编写高性能游戏引擎的上下文中的生产代码),当仅结构化为Lua时表现更好,以及代码(看起来结构相似)在介绍调用lua_CFunction时表现更好,该函数使用luaL_ref来维护对回调和回调参数的句柄。

优化LuaJIT而没有测量是愚蠢的行为

跟踪JIT已经很难理解了,即使您是静态语言性能分析方面的专家。它会打破您对性能的所有认识。如果将记录的IR编译而不是编译函数的概念已经摧毁了一个人对LuaJIT性能的理解力,那么通过FFI调用C基本上是免费的(当成功JITed时),但在解释时可能比等效的lua_CFunction调用昂贵一个数量级……这肯定会将情况推向极端。

具体而言,您上周写的系统远远优于C等效系统,但由于在与该系统相邻的位置引入了NYI,因此本周可能会崩溃,这可能来自看似无关的代码区域,现在您的系统正在回退并且性能完全被摧毁。更糟糕的是,也许您非常清楚什么是NYI,但是您添加了足够的代码以使其超过JIT的最大记录IR指令、最大虚拟寄存器、调用深度、展开因子、侧面跟踪限制等。

需要注意的是,虽然“空”基准有时可以提供非常一般的见解,但更重要的是在LJ(由于上述原因)中,代码应该在 上下文 中进行分析。编写LuaJIT的代表性性能基准非常困难,因为跟踪本质上是非局部的。在使用LJ的大型应用程序中,这些非局部交互会产生巨大的影响。

TL;DR

地球上只有一个人真正了解LuaJIT的行为。他的名字叫Mike Pall。

如果你不是Mike Pall,请不要假设关于LJ的行为和性能的任何内容。使用-jv(详细模式;查看NYIs和fallbacks),-jp(分析器!与jit.zone组合使用以进行自定义注释;使用-jp=vf查看由于fallbacks而导致的解释器花费的时间百分比),当您 需要真正知道发生了什么时,使用-jdump(跟踪IR& ASM)。测量、测量、测量。除非来自该人或您已在特定使用情况下测量过它们(在这种情况下,它不是一般化),否则不要轻信LJ性能特征的概括。并且请记住,正确的解决方案可能全部使用Lua,也可能全部使用C,也可能是Lua->C通过FFI,也可能是Lua->lua_CFunction->Lua,......你明白了。

从一个多次被愚弄而认为自己已经理解了LuaJIT的人的角度来看,接下来的一周就被证明是错误的,我真诚地希望这些信息对某个人有所帮助:) 个人而言,我不再对LuaJIT进行“有根据的猜测”。我的引擎每次运行都会输出jv和jp日志,对于优化而言,它们是“上帝的话语”。


7
两年后,我重新根据Miles的回答进行基准测试,原因如下:
  1. 看看它们是否随着CPU和LuaJIT的新进展而改善
  2. 为带有参数和返回的函数添加测试。回调文档提到除了调用开销之外,参数编组也很重要:

    [...] C到Lua转换本身具有不可避免的成本,类似于lua_call()或lua_pcall()。参数和结果的编组增加了这个成本[...]

  3. 检查PUSH风格和PULL风格之间的差异。

我的结果,在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

这个结果的代码在这里结论:当与参数一起使用时(这是你几乎总是要做的),将C回调到Lua中会产生非常大的开销,因此它们不应该在关键点上使用。但是,您可以将它们用于IO或用户输入。
我有点惊讶PUSH/PULL样式之间的差异如此之小,但也许我的实现不是最好的。

5

根据以下结果,可以看出性能差异显著:

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

这个基准测试的代码可以在此处找到。


你对这些结果有什么解释/解读吗?乍一看,从C调用Lua似乎只比另一个方向慢两倍,这个差异显得没有我想象中那么明显。但是从你的基准测试中看来,我怀疑你比较的是从C调用Lua的两次调用和一次调用之间的差异;我认为将Lua函数转换为ctype后的性能与实际的C实现函数不可相提并论。 - Miles
你能否提供sum_push和sum_pull作为纯C函数?最近我在我的开发机上无法正确编译C。 - Deco

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接