Unity脚本效率评测
对SLua、Tolua、XLua和ILRuntime四个脚本插件进行效率测试,对框架脚本进行选型。
本文项目:https://github.com/cateatcatx/UnityScriptPTest
tolua:https://github.com/topameng/tolua
slua:https://github.com/pangweiwei/slua
xlua:https://github.com/Tencent/xLua
ILRuntime:https://github.com/Ourpalm/ILRuntime
用例版本
Unity5.6.0p3
SLua 1.3.2
Tolua# github 2017/4/25 19:08:37
XLua 2.1.7
ILRuntime 1.11
Lua: luajit-2.1.0-beta2
测试环境
Smartian T2、Win7(64bit)
实验方案
总共设计了17个Test,分别从以下3个方面来考察脚本效率(JIT和非JIT),实验结果取10次的平均值,时间单位为ms。通过实验数据简单分析原因(Lua插件会横向对比,ILRuntime会单独考虑,因为毕竟C#和Lua本身差别较大)。
1. Mono -> Script,Mono调用脚本
2. Script -> Mono,脚本调用Mono
3. Script自身,脚本自身执行效率
Mono -> Script
|
Test11 |
Test12 |
Test13 |
Test14 |
Test15 |
Test16 |
Sum |
Tolua |
186 |
449 |
598 |
407 |
577 |
759 |
2978 |
SLua |
315 |
751 |
901 |
757 |
1253 |
1883 |
5863 |
XLua |
145 |
924 |
1010 |
573 |
1507 |
1929 |
6091 |
ILRuntime |
711 |
422 |
368 |
379 |
397 |
393 |
2672 |
|
|
|
|
|
|
|
|
Tolua(JIT) |
168 |
454 |
592 |
416 |
578 |
826 |
3037 |
SLua(JIT) |
384 |
842 |
956 |
824 |
1328 |
3439 |
7775 |
XLua(JIT) |
189 |
957 |
1047 |
608 |
1540 |
1700 |
6043 |
ILRuntime(JIT) |
1232 |
892 |
903 |
969 |
1175 |
1102 |
6275 |
Lua分析:
-- Test11
function EmptyFunc()
_V0 = _V0 + 1
end
_V0 = 1 -- Test12
_V1 = "12345" -- Test13
_V2 = GameObject.New() -- Test14
_V3 = Vector3.New(1, 2, 3) -- Test15
_V4 = {1, 2, 3} -- Test16
Test11为Mono调用脚本中空方法,Test12~16为测试Mono到脚本中取变量(ILRuntime代码类似,访问类的static函数与变量)。ILRuntime因为没有变量类型的转换所以效率最为优秀(JIT模式下使用的是Mono的反射取值所以会更慢,ILRuntime内部可能对类型变量有缓存,所以比反射快很多),Lua中可以看到Tolua的综合性能尤为突出(所有测试均好于其他Lua)。
对比Tolua和SLua的实现发现,Tolua会尽量减少与C++通信的次数,因为c#与c++通信会有一定效率损耗(参数的Marshaling等),虽然是Mono与Lua通信,但是其中还夹着一层C++,所以Mono与Lua通信的主要优化思路就是减少与C++的通信,从实验数据来看Tolua的这种优化效果是很明显的。
// SLua的函数调用
public bool pcall(int nArgs, int errfunc)
{
if (!state.isMainThread())
{
Logger.LogError("Can't call lua function in bg thread");
return false;
}
LuaDLL.lua_getref(L, valueref);
if (!LuaDLL.lua_isfunction(L, -1))
{
LuaDLL.lua_pop(L, 1);
throw new Exception("Call invalid function.");
}
LuaDLL.lua_insert(L, -nArgs - 1);
if (LuaDLL.lua_pcall(L, nArgs, -1, errfunc) != 0)
{
LuaDLL.lua_pop(L, 1);
return false;
}
return true;
}
// Tolua的函数调用
public void Call()
{
BeginPCall();
PCall();
EndPCall();
}
public int BeginPCall(int reference)
{
return LuaDLL.tolua_beginpcall(L, reference);
}
public int LuaPCall(int nArgs, int nResults, int errfunc)
{
return LuaDLL.lua_pcall(L, nArgs, nResults, errfunc);
}
public void LuaSetTop(int newTop)
{
LuaDLL.lua_settop(L, newTop);
}
对比SLua和Tolua代码的函数调用部分,发现SLua的C++调用多余Tolua两倍左右,所以效率高下立见。变量读取读者可以自行对比,总结就是Tolua通过减少C++调用的方式来优化效率,后期对Lua的进一步优化也要遵循这个思路。
ILRuntime分析:
实验发现解释执行下,ILRuntime的获取变量的效率要明显好于Lua,主要是因为都是C#对象,不需要进行类型转换。ILRuntime的JIT模式其实是用的Mono的,效率反而比解释执行更低,猜测是因为JIT下主要采用反射调用函数和取变量,而ILRuntime的解释器可能内部有缓存(因为没看过实现,都是不负责任猜测)。
Script->Mono
|
Test0 |
Test1 |
Test2 |
Test3 |
Test4 |
Test5 |
Test6 |
Test7 |
Test10 |
Sum |
Tolua |
675 |
797 |
1410 |
354 |
839 |
343 |
407 |
1681 |
915 |
7426 |
SLua |
768 |
640 |
2305 |
466 |
1110 |
408 |
394 |
3379 |
1299 |
10774 |
XLua |
590 |
648 |
8504 |
450 |
775 |
1213 |
695 |
1267 |
845 |
14989 |
ILRuntime |
1152 |
1054 |
4012 |
315 |
998 |
437 |
1026 |
3272 |
1434 |
13703 |
|
|
|
|
|
|
|
|
|
|
|
Tolua(JIT) |
612 |
701 |
7.4 |
357 |
823 |
318 |
37 |
128 |
884 |
3871 |
SLua(JIT) |
732 |
679 |
5.4 |
465 |
1197 |
411 |
10 |
165 |
1307 |
4974 |
XLua(JIT) |
636 |
668 |
8312 |
438 |
767 |
1303 |
734 |
1340 |
911 |
15113 |
ILRuntime(JIT) |
72 |
197 |
84 |
260 |
402 |
33 |
219 |
303 |
77 |
1651 |
Lua分析:
function Test0(transform)
local t = os.clock()
for i = 1,200000 do
transform.position = transform.position
end
return os.clock() - t
end
function Test1(transform)
local t = os.clock()
for i = 1,200000 do
transform:Rotate(up, 1)
end
return os.clock() - t
end
function Test2()
local t = os.clock()
for i = 1, 2000000 do
local v = Vector3.New(i, i, i)
local x,y,z = v.x, v.y, v.z
end
return os.clock() - t
end
function Test3()
local t = os.clock()
for i = 1,20000 do
GameObject.New()
end
return os.clock() - t
end
function Test4()
local t = os.clock()
local tp = typeof(SkinnedMeshRenderer)
for i = 1,20000 do
local go = GameObject.New()
go:AddComponent(tp)
local c = go:GetComponent(tp)
c.receiveShadows=false
end
return os.clock() - t
end
function Test5()
local t = os.clock()
for i = 1,200000 do
local p = Input.mousePosition
--Physics.RayCast
end
return os.clock() - t
end
function Test6()
local Vector3 = Vector3
local t = os.clock()
for i = 1, 200000 do
local v = Vector3.New(i,i,i)
Vector3.Normalize(v)
end
return os.clock() - t
end
function Test7()
local Quaternion = Quaternion
local t = os.clock()
for i=1,200000 do
local q1 = Quaternion.Euler(i, i, i)
local q2 = Quaternion.Euler(i * 2, i * 2, i * 2)
Quaternion.Slerp(Quaternion.identity, q1, 0.5)
end
return os.clock() - t
end
function Test10(trans)
local t = os.clock()
for i = 1, 200000 do
UserClass.TestFunc1(1, "123", trans.position, trans)
end
return os.clock() - t
end
总体效率还是Tolua胜出,其中XLua在Test2中较比SLua、Tolua差出不止一个数量级,主要是因为Tolua和SLua对于Unity的值类型变量做了lua的实现,这种值类型SLua和Tolua中是一个table,而在XLua中是一个Userdata,所以SLua和Tolua在做Test2的时候并没有跟Unity交互(从JIT结果也能看出来,JIT不能处理C函数,所以JIT后Test2效果提升明显),而XLua需要频繁和Unity交互,效率消耗明显。对于对象类型的变量,3种lua处理机制是雷同的,只是内部实现细节不一样而已,细节不再本文讨论范围内,从实验数据上来看,还是Tolua的内部实现更加效率。用好lua+unity,让性能飞起来——lua与c#交互篇,这篇文章对C#与Lua的交互原来有非常详细的说明,虽然插件后续有改进,但是核心思想还是不变的。
ILRuntime分析:
数据上来看ILRuntime解释器的效率还是很高的并不比lua慢太多,但是对于Vector3这种Unity值类型的处理跟lua差距比较大(主要是因为SLua和Tolua中的Unity值类型其实就是table,等于没有跟Unity交互)。ILRuntime还是一个很有潜力的Unity热更解决方案的,毕竟C#配合VS的开发效率还是比Lua高不少的。其中的JIT部分是Mono层的,跟本身的C#代码是没有区别的,不参与对比。
Script自身
|
Test8 |
Test9 |
Sum |
Tolua |
254 |
4246 |
4500 |
SLua |
255 |
4766 |
5022 |
XLua |
311 |
4506 |
4817 |
ILRuntime |
852 |
79048 |
79900 |
|
|
|
|
Tolua(JIT) |
46 |
371 |
417 |
SLua(JIT) |
48 |
414 |
463 |
XLua(JIT) |
40 |
469 |
510 |
ILRuntime(JIT) |
222 |
313 |
536 |
function Test8()
local total = 0
local t = os.clock()
for i = 0, 1000000, 1 do
total = total + i - (i/2) * (i + 3) / (i + 5)
end
return os.clock() - t
end
function Test9()
local array = {}
for i = 1, 1024 do
array[i] = i
end
local total = 0
local t = os.clock()
for j = 1, 100000 do
for i = 1, 1024 do
total = total + array[i]
end
end
return os.clock() - t
end
因为Lua全部使用的是LuaJIT2.1.0B2版本,所以其实脚本自身的效率理论上应该是一致的,从数据上看也差不多。实验结果上主要体现了Lua解释器的速度要明显好于ILRuntime(语言内部实现不一样,勉强对比在一块,毕竟lua是c写的),并且发现LuaJIT对效率的提升也是好几个数量级,虽然LuaJIT很多坑,但是如果能用好还是个优化利器。
总结
综合来看Tolua是现在效率较好的Unity Lua解决方案,后续会对Tolua的内部实现做进一步剖析,从来做进一步的效率优化。