【问题标题】:LuaJIT FFI callback performanceLuaJIT FFI 回调性能
【发布时间】:2012-09-01 23:42:41
【问题描述】:

LuaJIT FFI docs 提到从 C 调用回 Lua 代码相对较慢,建议尽可能避免使用它:

不要将回调用于性能敏感的工作:例如考虑一个数值积分例程,它需要一个用户定义的函数来积分。从 C 代码调用用户定义的 Lua 函数数百万次是个坏主意。回调开销对性能绝对有害。

对于新设计,请避免使用推送式 API(C 函数为每个结果重复调用回调)。而是使用拉式 API(重复调用 C 函数以获得新结果)。通过 FFI 从 Lua 到 C 的调用比反过来要快得多。大多数设计良好的库已经使用拉式 API(读/写、get/put)。

但是,它们并没有给出任何感觉多少来自 C 的回调慢。如果我有一些我想加速使用回调的代码,如果我重写它以使用拉式 API,我大概可以期望多少加速?有没有人有任何基准来比较使用每种 API 风格的等效功能的实现?

【问题讨论】:

    标签: callback ffi luajit


    【解决方案1】:

    在我的计算机上,从 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...)
    【解决方案2】:

    两年后,我从Miles' answer 重新进行了基准测试,原因如下:

    1. 看看他们是否随着新的进步(在 CPU 和 LuaJIT 中)得到改进
    2. 为带有参数和返回的函数添加测试。 The callback documentation 提到除了调用开销之外,参数编组也很重要:

      [...] 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
    

    这个结果的代码是here

    结论:当与参数一起使用时,Lua 中的 C 回调具有非常大的开销(您几乎总是这样做),因此它们真的不应该在关键点使用。不过,您可以将它们用于 IO 或用户输入。

    我有点惊讶 PUSH/PULL 样式之间的差异如此之小,但也许我的实现并不是最好的。

    【讨论】:

      【解决方案3】:

      由于这个问题(以及整个 LJ)一直是我痛苦的根源,因此我想在戒指中提供一些额外的信息,希望它可以帮助将来的人。

      “回调”并不总是很慢

      LuaJIT FFI 文档中,当它说“回调很慢”时,非常具体指的是由 LuaJIT 创建的回调并通过 FFI 传递给需要函数指针的 C 函数的情况.这与其他回调机制完全不同,特别是与调用使用 API 调用回调的标准 lua_CFunction 相比,它具有完全不同的性能特征。

      话虽如此,真正的问题是:我们什么时候使用 Lua C API 来实现涉及 pcall 等的逻辑,而不是把所有东西都保存在 Lua 中?与性能一样,尤其是在跟踪 JIT 的情况下必须配置文件 (-jp) 才能知道答案。期间。

      我见过看起来相似但性能范围相反的情况;也就是说,我遇到过代码(不是玩具代码,而是在编写高性能游戏引擎的上下文中的生产代码),当结构化为仅 Lua 时性能更好,以及代码(似乎 em> 结构相似)通过调用 lua_CFunction 引入语言边界时性能更好,该 lua_CFunction 使用 luaL_ref 来维护回调和回调参数的句柄。

      在没有测量的情况下优化 LuaJIT 是一件傻事

      跟踪 JIT 已经很难推理,即使您是静态语言性能分析方面的专家。他们把你认为你知道的关于性能的一切都拿走了,然后把它粉碎了。如果编译记录的 IR 而不是编译函数的概念还没有消除一个人推理 LuaJIT 性能的能力,那么通过 FFI 调用 C 的事实在成功 JIT 时或多或少是免费的,但可能是一个命令-解释时比等效的 lua_CFunction 调用更昂贵...嗯,这肯定会将情况推到边缘。

      具体来说,你上周编写的一个性能大大优于 C 等效的系统本周可能会失败,因为你在所述系统中引入了一个 NYI,它很可能来自一个看似正交的代码区域,现在您的系统正在回退并抹杀性能。更糟糕的是,也许您很清楚什么是 NYI,什么不是 NYI,但是您在跟踪接近度中添加了足够代码,它超过了 JIT 记录的最大 IR 指令、最大虚拟寄存器, 调用深度, 展开因子, side trace limit...等。

      另外,请注意,虽然“空”基准有时可以提供非常笼统的见解,但对于 LJ(出于上述原因)更重要的是,在上下文中对代码进行分析。为 LuaJIT 编写具有代表性的性能基准非常非常困难,因为就其性质而言,跟踪是非本地的。在大型应用程序中使用 LJ 时,这些非本地交互会产生巨大影响。

      TL;DR

      这个星球上确实有 一个 人真正了解 LuaJIT 的行为。他叫迈克·帕尔。

      如果您不是 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 日志,它们对我来说是优化方面的“上帝之言”。

      【讨论】:

        【解决方案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找到。

        【讨论】:

        • 您对这些结果有什么解释/解释吗?乍一看,从 C 到 Lua 的调用似乎只比另一个方向慢两倍,这与我预期的差异要小得多。但是通过查看您的基准,我怀疑您要比较的是从 C 到 Lua 的 two 调用和 one 之间的区别;我不认为转换为 ctype 的 Lua 函数具有与实际 C 实现的函数相当的性能。
        • 你能提供 sum_push 和 sum_pull 作为纯 C 函数吗?我最近无法在我的开发机器上正确编译 C。
        猜你喜欢
        • 2014-08-08
        • 1970-01-01
        • 2011-08-07
        • 2014-07-20
        • 1970-01-01
        • 2015-10-13
        • 2018-01-21
        • 2012-12-10
        • 1970-01-01
        相关资源
        最近更新 更多