Lua热更新注意事项

4k 词

lua热更新,是当下最成熟、最热门,也是使用最多的热更新方案,单说热更新的话,它是完美的解决方案,但要从性能上说,它其实比C#要差上不少,自然而然地,如何优化性能也就成了开发人员最头痛的问题。
本文讲从一些数据调研出发,结合一些大牛的文章,对lua中性能优化的点进行汇总。

关于热更新

相信有一定经验的游戏从业者都听说过“热更新”,想象一下你在打开农药之后,登录之前总是要检查资源包更新,时不时还要下载一些东西,这其实就是热更新了。与之相对的是整端更新,就是重新下载安装包重新安装那一种。手游嘛,一般要求快速迭代,很多游戏都是一周一个小版本,如果每更一次都要让你下一个整端,你自然不乐意,说不定就要退坑了。在每天有几十款游戏上线的当下,玩家们都被惯坏了,他们可禁不起这么折腾,所以这就是热更新的必要性。

热更新的方案选择

现在市面上常见的热更新方案,无非就那么几种,真要分类的话就两大类,lua热更新和c#热更新,c#热更新本人知之甚少,基于ILRuntime的方式只听说在性能上并不占优势,但好处是c#语言在写代码时比lua爽,如果用c#做服务器的话还能做到客户端服务器统一语言,但终归是新兴起的方案,资料上和完善程度上都不如lua。
lua家族里有ulua、tolua、xlua以及各种各样奇怪的lua,但基本上万变不离其宗,通过lua和c#互相调用的方式实现lua和c#的通信,在底层上要么是基于native lua,要么是基于luajit。

luajit和lua

luajit是lua的高性能版本,其内部实现比较复杂,就不在此讨论了,总之luajit和native lua,几乎无甚区别,只是luajit的性能要高得多。除此之外,luajit目前最新版本是基于lua5.1,目测也很难再更新了,所以如果想要使用lua的新特性,比如原生的位运算,比如原生的int类型,在luajit里基本是做不到了。

常见的热更新方案,ulua、tolua就是基于luajit,xlua默认是基于lua5.3,不过据说可配置或者可以自定义。这里面的取舍,一是看项目类型,二是看方案的必要性。
下面是一组测试数据,主要是针对简单的乘法或者移位运算,从中可以看出原生lua和luajit的差别。

serial language operate count time
1 C# fixed mul 10000000 230ms
2 C# double mul 10000000 34ms
3 C# rshift 10000000 28ms
4 luajit fixed mul 10000000 2600ms
5 luajit double mul 10000000 37ms
6 luajit bit.rshift 10000000 167ms
7 luajit jit mode double mul 10000000 10ms
8 lua native mul 10000000 100ms
9 xLua fixed mul 10000000 5038ms
10 xLua native rshift 10000000 90ms
11 luajit int64 mul 10000000 1500ms
12 luajit ffi-int mul 10000000 1000ms

关于升级Lua5.3对性能的影响

目前luajit官方支持的lua版本是Lua5.1,如果要升级5.3则需要自己修改luajit的源码编译,代价较高。如果直接使用原生lua的话反而会降低性能,因为luajit性能要比原生lua高。
luajit是lua代码的解释器,效率比原生lua要高很多,从表中的对比也可窥见一二(5, 8)。
而luajit在运行时实际上又分为两种模式,即jit模式和interpreter模式。jit模式一般情况下会比interpreter模式快5-10倍。表中对于这种差别也有所体现(5,7)。但要成功jit,需要很多条件,但即使不用jit模式,只在interpreter模式下运行,效率也比原生lua要高很多。
(9)是在xLua中定点数的乘法运算测试,耗时比在tolua中多了一倍。猜测其原因为xLua默认情况下是基于原生的lua5.3,虽然它的位运算效率和其乘除法运算效率相近,但是因为没有jit的高性能,所以总体下来反而变慢了。

引入int类型以及相关位运算的补丁

在tolua中,其实有一个int64的补丁,但是运算效率并不高。如(11)所示。原因仍需调研。
通过ffi调C的int类型做乘法,效率依然不高。如(12)。原因可能是lua和c之间的频繁调用,耗时较多。
找到一个lnum的补丁,看见作者的介绍里,它提供了int32、int64、double、float等数据类型,且运算效率和原生的number类型相近。但是在windows平台编译后,只有double类型可用,尚不知其原因,需要逐步排查。

修改luajit的bit库

目前使用的bit库是基于luajit自带的bit库的,但效率并不高,其运算速度比简单的乘除法要慢得多,目前来看,它是客户端定点数库的主要性能瓶颈,但是在服务器端则不同,因为把lua代码解释成java后,在位运算上时会直接调用java原生的位运算方式,但是因为它把lua中的数据类型抽象成了类,所以在计算时会产生额外的GC,导致性能下降。所以修改luajit的bit库这种方法,并不能提高服务器端的运算效率。

总结

在不能修改luajit重新编译Lua5.3的情况下,使用Lua5.3以提升位运算以及整数运算效率并不现实,无论能否成功进入jit模式,其带来的效率提升都不及luajit。
如果能修改luajit使其支持Lua5.3,无疑是最好的方法,但是代价较高。
修改luajit中的bit库,这种方式暂时不考虑,因为其作用仅体现在客户端,原因上面已经说过了。所以后续主要尝试方向还是前两种,即在lua中添加int32/64类型,这样服务器端可以直接将其解释成java本身的数据类型。

luajit的jit

luajit分为jit模式和interpreter模式,jit模式更快,快到令人发指(与c++接近,甚至更快),但是因为其对平台的高度依赖,导致它在安卓平台上不一定能发挥出PC上的威力,因为,luajit无法保证所有代码都可以jit化,只有在编译时才能知道,而一旦jit失败,其运算速度会骤降百倍。
jit模式的开启方式:

1
2
3
4
if jit then
  jit.on(); --关闭 jit.off()
  jit.flush()
end

至于jit模式究竟能带来多少性能提升,或者其稳定性到底如何,需要在真机上进行大量的测试。
需要注意的是jit模式在ios平台下并不可用。

luajit的ffi

ffi是luajit独有的一个神器,用于进行高效的luajit与c的交互。借助ffi也可以提高luajit和c#交互的性能,原理是利用ffi调用自己定义的c函数,再从c函数调用c#。 必须要注意的是,ffi只有在jit开启下才能发挥其性能,如果是在ios下,ffi反而会拖慢性能。所以使用的时候必须要做好快关。
首先,我们在c中定义一个方法,用于将c#的函数注册到c中,以便在c中可以直接调用c#的函数,这样只要luajit可以ffi调用c,也就自然可以调用c#的函数了

1
2
3
4
void gse_ffi_register_csharp(int id, void* func)
{
  s_reg_funcs[id] = func;
}

这里,id是一个你自由分配给c#函数的id,lua通过这个id来决定调用哪个函数。

然后在c#中将c#函数注册到c中

1
2
3
4
5
6
7
8
9
[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern void gse_ffi_register_csharp(int funcid, IntPtr func);

public static void gse_ffi_register_v_i1f3(int funcid, f_v_i1f3 func)
{
gse_ffi_register_csharp(funcid, Marshal.GetFunctionPointerForDelegate(func));
}

gse_ffi_register_v_i1f3(1, GObjSetPositionAddTerrainHeight);//将GObjSetPositionAddTerrainHeight注册为id1的函数

然后lua中使用的时候,这么调用

1
2
3
4
5
6
7
local ffi = require("ffi")
ffi.cdef[[
int gse_ffi_i_f3(int funcid, float f1, float f2, float f3);
]]

local funcid = 1
ffi.C.gse_ffi_i_f3(funcid, objID, posx, posy, posz)

就可以从lua中利用ffi调用c#的函数了。

lua和c#的交互的注意事项

减少Lua中C# object的引用
避免在Lua和C#之间传递Unity独有的值类型(Vector3、Quaternion)
减少传递bool string object,用int float double替代
频繁调用的函数,参数的数量尽量少
优先使用static函数导出
在lua中只使用自己管理的ID,而不直接引用C#的object

以上结论主要来源于UWA-Blog https://blog.uwa4d.com/archives/USparkle_Lua.html

            <hr style="visibility: hidden;"/>
            
            <hr style="visibility: hidden;"/>