LuaJIT之callback大坑绕路记

LuaJIT之callback大坑绕路记

近期在做 node.js的 LuaJIT port。 LuaJIT是当前已知最快的脚本JIT编译器,拿来做服务器再好不过。
发现node.js底层所用的库 libuv简直是个神器,包含了网络、文件系统、计时器等等一堆堆的有用功能,windows、linux、MacOS等均支持,而且是纯C的API,和LuaJIT结合会比较友好,理论上不用任何额外的C代码,依靠 ffi库就可以搞定,经过 试验也确实如此,于此同时发现LuaJIT也真神器也,居然可以直接把Lua函数当做C函数指针传进去当回调!正当我踌躇满志的准备跑下性能测试就开始做上层封装的时候,结果楞了:

1、Lua版的idle示例,等待一个idle事件被调用1e7(一千万)次,在C下只需要区区0.1秒,在lua下需要足足30秒多!并且内存在这个过程里猛涨猛涨再猛涨,最后的gc过程耗费了更久的时间!
    原版的在 这里,Lua版的在 这里。
2、尝试添加1000次idle事件,LuaJIT直接报错:too many callbacks
3、其他不同的尝试均体现,性能严重不过关。

然后在ffi的说明里发现了 这个,提到了几个问题:
1、callback占用某些总量有限的系统资源,所以用过的callback需要释放,并且同时存在的callback只能有500-1000个。
2、callback函数不会被自动gc,需要用一些麻烦的办法手动来释放
3、callback会很慢。文中提到了类似于lua_call的消耗及argument marshalling的消耗。这点会在下面详细讲述。

总的来说,luajit里的callback,是在内存里生成了一小段代码,这小段代码的功能是把参数转换好,然后再调用对应的lua函数。(还有一些奇奇怪怪的开销,我个人认为这才是主要开销,后面会详细讲述),因此有同时存在的总量上限(虽然我也不明白为什么就因此了,但大致就是那么回事吧),并且很慢,很慢,很,慢,很……慢……

基本上,解决方法就那么几种:
1、做一些特定的封装,用C额外编写一个函数做一些处理,在这个函数里用其他方式(lua_pcall等)去调用,这样调用参数的类型会受限一些。经测试这个只能提升50%左右(距离之前的300倍差距还差得远……),主要是还有一些关键的开销(在下面详细讲述)无法避免。
2、改写被使用的C库,拒绝回调,用其他办法实现。这是LuaJIT官方所推荐的,原文如下:
For new designs  avoid push-style APIs : a C function repeatedly calling a callback for each result. Instead  use pull-style APIs : call a C function repeatedly to get a new result. Calls from Lua to C via the FFI are much faster than the other way round. Most well-designed libraries already use pull-style APIs (read/write, get/put).
但像libuv这样的库,改写难度有些大……关键在于重新设计整个结构为pull-style很困难,同时会导致相关文档废弃,增加了额外的工作量。
3、小幅度改写使用的C库,公开一些必须的内容,然后把其中的一部分在lua里实现,确保所有callback调用的时机均在lua中,废弃掉原始的C API。这样相对来说不用改变任何的接口,但是工作量也不小,取决于库的复杂程度。

最终我在node.lua中选择了方案3。事实证明效果确实很好,在还有一些会带来额外开销的功能没加进去的情况下,之前的test优化到了0.08s左右,预计全部完成后开销在0.15s之内,很接近纯C实现的性能。

然后我又做了若干实验,并且在freelist里和LuaJIT的创始人Mike请教了一会,得到了一些结论:

1、回调的argument marshalling是重大瓶颈之一。虽然不知道为什么,Lua对C的调用,返回值的marshalling性能很高,我推测是由于原因3。
2、把Lua-function cast成C function pointer是另一重大瓶颈,如果存在反复的类型转换,这里会很要命。这里包含了之前所说的生成指令序列的开销,但cast本身也会具有巨大的开销,我尝试将一个C function cast成 C function pointer,都带来了极大的开销。据Mike说,这个开销也是原因3导致的
3、导致程序运行很慢的原因,归根结底:某些行为会导致JIT失效!在没有JIT的情况下,本身运行性能差不多就有几十倍的损失,再加上一些额外开销会因此被放大,最后就得到了不可接受的性能损失……

最后总结,目前应该在LuaJIT的ffi库中避免使用函数指针,使用Lua本身来封装回调函数(如果接口需要),方可获得LuaJIT提供的卓越性能。

你可能感兴趣的:(LuaJIT之callback大坑绕路记)