天地之灵

LuaJIT之callback大坑绕路记

近期在做node.jsLuaJIT portLuaJIT是当前已知最快的脚本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提供的卓越性能。

posted on 2013-02-24 14:36 天地之灵 阅读(19334) 评论(7)  编辑 收藏 引用

评论

# re: LuaJIT之callback大坑绕路记 2013-02-25 09:42 emptyhua

和这个项目比做了哪些改进呢?https://github.com/luvit/luvit  回复  更多评论   

# re: LuaJIT之callback大坑绕路记 2013-02-25 10:08 天地之灵

@emptyhua

原本的目的是多用ffi,少对libuv库进行修改,避免对lua C API的调用,看能否取得更好的扩展性和性能提升。
在之前的项目里采用C API去封装回调,遇到了一些有关coroutine的坑,譬如在coroutine里开始一个主循环,在另一个coroutine里再注册一些回调,使用C API有时候很难完美解决。

所以这个项目的思路是,尽可能直接使用第三方的C库,使用ffi来访问API,而非去实现一个Lua C Module,这也是LuaJIT官方所推荐的,这会让luaJIT的优化达到极致(C API访问,以及对Lua-C Module里函数访问时的传参,会有不能被LuaJIT编译优化的开销,虽然这个开销对于非频繁调用的内容并不大)
另外一个原本预期中的好处是,希望这个项目最终能只有Lua代码,以及luajit主程序、编译好的其他库的动态链接版本,便于去修改、发布及调试。只是目前来看完全这么做还是有点困难,因为现在已经对libuv做了一些修改。但是使用其他callback不那么常见的库可能会相对轻松。我可能再进行一些尝试后再决定如何折衷,或者放弃这个,去fork和参与luvit。

另外luvit与我这边的实验均证明,使用LuaJIT会取得比V8好数倍的性能~所以这个方向应该是没错的。  回复  更多评论   

# re: LuaJIT之callback大坑绕路记 2013-03-01 06:04 essayforce

最终能只有Lua代码,以及l\主  回复  更多评论   

# re: LuaJIT之callback大坑绕路记 2013-05-14 14:42 imjj

我测试的结果跟你有所不同,参看
https://bitbucket.org/lijia/pieceofcode/src/d02e87d97ab38226f44549b9f5ea0a4d408482a5/lua-uv-test?at=master  回复  更多评论   

# re: LuaJIT之callback大坑绕路记 2013-08-22 13:26 天地之灵

@imjj
原因不明。我这里的性能明显下降应该有gc的影响。  回复  更多评论   

# re: LuaJIT之callback大坑绕路记 2014-10-01 00:31 jiakai1000@gmail.com

你好,我想请问:
3、小幅度改写使用的C库,公开一些必须的内容,然后把其中的一部分在lua里实现,确保所有callback调用的时机均在lua中,废弃掉原始的C API。这样相对来说不用改变任何的接口,但是工作量也不小,取决于库的复杂程度。
"确保所有callback调用的时机均在lua中",能说一下具体是怎么做的吗?这样的话就不需要从lua调用c了吗?
谢谢。  回复  更多评论   

# re: LuaJIT之callback大坑绕路记 2015-09-10 23:08 se

sb,没事找事,秀技能?  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


<2013年2月>
272829303112
3456789
10111213141516
17181920212223
242526272812
3456789

导航

统计

常用链接

留言簿(3)

随笔档案

文章档案

搜索

最新评论

阅读排行榜

评论排行榜