最近正在做 Cocos Creator 的 Lua 支持。因为 Creator 使用了一个精简的 cocos2d-x 引擎,所以我也基于这个引擎来做 Cocos Creator 的 Lua 支持。
cocos2d-x 目前使用一套基于 tolua++ 的 Luabinding 层。而 tolua++ 这货已经停止维护快十年了,所以换掉 tolua++ 势在必行。另外我一直认为 tolua++ 存在性能问题,替换为更高效的 luabinding 可以充分发挥 Lua 的性能优势。
这篇文章的重点就是选择新的 Luabinding。
性能测试
既然以提高性能为目的,必须用数据来说话。所以我做了一个性能测试工程,放在 github 的 cocos2dx_benchmark 仓库里。
在 iPhone 6 上,这个 Lua 测试例可以在保证 55fps+ 帧率的基础上跑 4500 个星星。
为了看到 Lua 和 C++ 的性能差距到底有多大,我又在 quick2d-engine 仓库中做了一个新的测试例。
C++ 版的测试结果是可以跑到 12000 个星星。可以看出 cocos2d-x Lua 的综合性能只有 C++ 的 37.5%。
选择新 Luabinding
找了好些 Luabinding 库,根据这个性能测试选择了 Sol2 。但经过一天多的试验,这个库缺少一个及其重要的特性 concatenate the metatables together。而且作者刚刚发布 2.5 版后就说他不玩了。。因为太累 -_-#
好吧,我换成第二快的 kaguya。
这个库采用大量 C++11 的新特性,采用模板的形式来导出 C++ 接口,代码如下:
auto cc = _lua["cc"] = _lua.newTable(); cc["Ref"].setClass(kaguya::ClassMetatable<Ref>() .addMemberFunction("retain",&Ref::retain) .addMemberFunction("release",&Ref::release) .addMemberFunction("getReferenceCount",&Ref::getReferenceCount)); cc["Scheduler"].setClass(kaguya::ClassMetatable<Scheduler,Ref>()); cc["Director"].setClass(kaguya::ClassMetatable<Director,Ref>() .addStaticFunction("getInstance",&Director::getInstance) .addMemberFunction("endDirector",&Director::end) .addMemberFunction("getScheduler",&Director::getScheduler)); cc["Node"].setClass(kaguya::ClassMetatable<Node,Ref>() .addStaticFunction("create",&Node::create) .addMemberFunction("addChild",static_cast<void(Node::*)(Node*)>(&Node::addChild)) .addMemberFunction("addChild",static_cast<void(Node::*)(Node*,int)>(&Node::addChild)) .addMemberFunction("addChild",int,const std::string&)>(&Node::addChild)) .addMemberFunction("removeChild",&Node::removeChild) .addMemberFunction("setPosition",static_cast<void(Node::*)(const Vec2&)>(&Node::setPosition)) .addMemberFunction("setPosition",static_cast<void(Node::*)(float,float)>(&Node::setPosition)) .addMemberFunction("setColor",&Node::setColor) .addMemberFunction("setOpacity",&Node::setOpacity) .addMemberFunction("schedule",static_cast<void(Node::*)(const std::function<void(float)>&,float,const std::string &)>(&Node::schedule)));
代码看上去是相当整洁的。
但经过性能测试,却发现在超过 3000 个星星后,性能会出现暴跌,而不是线性降低。
又反复测试了一下,估计问题应该出在 Lua call C++ member function 这里。所以又写了一段代码:
static int lgetNode(lua_State *L) { // node kaguya::ObjectPointerWrapper<Sprite*> *wrap = static_cast<kaguya::ObjectPointerWrapper<Sprite*>*>(lua_touserdata(L,-1)); Node *node = static_cast<Node*>(wrap->get()); lua_pushlightuserdata(L,node); return 1; } static int lsetPosition(lua_State *L) { // node,x,y Node *node = static_cast<Node*>(lua_touserdata(L,-3)); node->setPosition(lua_tonumber(L,-2),lua_tonumber(L,-1)); return 0; } static int lsetOpacity(lua_State *L) { // node,opacity Node *node = static_cast<Node*>(lua_touserdata(L,-2)); node->setOpacity(lua_tointeger(L,-1)); return 0; } lua_State *L = _lua.state(); lua_pushcfunction(L,&lgetNode); lua_setglobal(L,"lgetNode"); lua_pushcfunction(L,&lsetPosition); lua_setglobal(L,"lsetPosition"); lua_pushcfunction(L,&lsetOpacity); lua_setglobal(L,"lsetOpacity");
在 Lua 里,就不再使用对象方法来调用了,而是改成这三个全局函数。测试结果暴涨到 8200 个星星,接近 C++ 70% 的性能了。
总结
cocos2d-x 现在使用的 Luabinding 只能达到 C++ 37.5% 的性能。在更换新 Luabinding 并做相应调整后,可以获得高达 C++ 70% 的性能表现。而这损耗的 30%,其中还有大量的数值运算。要知道拼计算,脚本是肯定比不过 C++ 的。
虽然新的 Luabinding 方案还有很多工作要做,但我有信心最终搞一个比现在快一倍的 Luabinding 出来。