前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >黑科技:魔改TProto优化掉100MB的Lua内存

黑科技:魔改TProto优化掉100MB的Lua内存

作者头像
quabqi
发布2021-10-22 16:18:26
1.6K0
发布2021-10-22 16:18:26
举报
文章被收录于专栏:Dissecting UnrealDissecting Unreal

手机的内存优化几乎是所有手机游戏都会做的事情。像iphone7,iphone8这样的机器,他的CPU非常强悍,但是内存一共就只有2G,真正能给应用使用的安全内存可能就1.1G左右。内存的限制就直接制约着游戏画面的表现,比如不能用过多的的RT,不能用大分辨率贴图,抗锯齿不能使用TAA等太多的因素。像原神这样的游戏,因为用了延迟渲染,为了保证画质更是任性的直接不支持低内存的手机。

而Lua目前在很多游戏开发尤其是手机游戏的开发中被广泛使用,也是因为这个语言本身的特性,比如逻辑简单易修改,解释执行,支持热更等。虽然一般游戏,轻量使用Lua可能内存的占比不高,但在一些非常重度或全部代码都是写在lua的游戏中,lua的启动内存可能就轻松占用上百MB,什么都不做峰值达到300MB以上,所以对lua做内存优化,就是一个非常重要的事情。在前面有专门写一篇lua是怎样占用内存的: Lua数据的内存结构 - 知乎 (zhihu.com)

如果你的游戏也是一个用lua开发的重度游戏,你可能会观察到其中有个结构TProto占用的内存非常夸张,而且这部分会常驻内存,随着项目代码量的增多而增多。那么TProto到底是什么呢?其实就是程序员写的代码,被lua的解析器编译成字节码在内存中的结构。其中code就是对应的代码,Proto是以函数或闭包为单位的。有多少个Proto就相当于是有多少个函数/闭包被加载了。所以,只要函数写的越长,单个Proto就越大,函数或文件越多Proto的数量就会越多。他的内存计算规则如下:

这里可以看到,lua在计算内存时耍了一个小聪明,只是把他认为需要计算的部分加了起来,而其中有一个占用内存比较大块的字段lineinfo,是没有被计算进内存里的

我们可以通过注释看到,这个lineinfo只是调试的时候当前字节码对应在源码中的行号信息。而Instruction本身就只有4字节,调试信息就同样占了4字节。在调试中可以发现,code的内存有多大这个lineinfo的内存就有多大,这对于游戏来说是不太合适的。当然除此外还有一些其他的调试信息,包括source以及locvars等也会占用一些内存。

所以最简单,最暴力的做法,就是全局搜索这个字段,把所有用到的地方都删掉,因为他只是调试信息不会对正常运行产生任何影响。假如你的代码在内存中有200MB,改完后你就会发现内存轻轻松松少了100MB。。。所以,到此为止,本文就可以这样简单愉快的完结撒花了

但这样做的代价,肯定就是lua代码再也看不到报错堆栈了,遇到了异常完全无法定位原因,就像C++没有符号表一样。所以下面就来提出一些方案,能够很好的解决这个问题。

方案1:

也是最简单的改法。我们注意到这里代码行号使用的是int,int的上限是21亿+,但其实应该没有人能把单个lua代码文件写到20亿行的,假如我们把int改为short,那么上限是32767,对于大部分程序来说完全足够用了。

当然用到的地方,只需要改一处,就是下面加载字节码的地方,这个函数在lundump.c中。要把加载进来的int转为short,否则是放不下的。

当然这样的方案,减少的内存肯定不如直接去掉,只能减少一半,但好处就是调试信息还在。

方案2:

其实再仔细观察可以发现,这里行号都是绝对行号,但其实正常的函数长度一般都不会很长,在TProto里还有个字段linedefined,记录了这个函数的开始行号,假如我们把这个字段改为相对行号,假如函数的行数都没有超过256,那理论上还可以把这个short改为uint8(unsigned char)的。在报错打堆栈的时候,再用相对行号加上linedefined即可。这样又可以节省4分之一内存,当然代价是肯定比上面更麻烦了,要在打堆栈的地方还原行号。其实理论上不加linedefined也可以正常运行,只是调试信息友好度相对差一些,只要保证所有程序员都清楚的知道规则就好。另外即使少数函数超过了256行,就只保存低位,报错时发现不对,原行号+256再多看1行就好了。

方案3:

因为还剩了4分支1内存,还有没有办法再压缩一下这部分内存呢?再仔细观察,又可以发现,这里是相对行号,那么可以看到这个数组里面值其实是这样的1,1,1,2,3,3,4...要么和前一个值一样,要么是递增1的。这是因为我们写的代码都是连续的,lua在编译后生成的字节码当然也就是连续的。所以我们就可以把这个代码改为一个BitArray,每一位代表一行,如果相比前一个增加了1行,就设为1,否则为0,这样1字节就可以表示8个字节码的行号。最终内存占用就变成了原来的32分支1。当然代价是在报错或打堆栈的时候要把行号还原回去。这里搜一下lineinfo用到的地方,加上linedefined和当前位之前有多少个1就可以,这里就不再具体说怎么修改了。当然统计多少个1还是有一些快速办法的,比如UE4的数学库就提供了这样的快速函数:

如果支持SSE指令的话那会更快,比如clang下__builtin_popcountll

windows上对应的是_mm_popcnt_u64

方案4:

最后,假如还是一点调试信息都不想存,又还想回复出堆栈信息,该怎么办呢?那么也可以像C++那样,把符号信息离线存成一个符号表,不跟着字节码一起打包对外发布。其实符号表完全不需要单独写,因为最终都是从lundump中读取出来的,只要保留原始字节码,对外发布的是strip后的字节码就好。我们知道lineinfo和code是一一对应的,所以报错的时候只要把code下标记录下来,然后程序员需要根据行号,到对应的符号表上找到对应的行号。当然这种方案是最麻烦的,毕竟要写工具,但肯定是效果最好的,而且安全性相对来说也是最高的,即使游戏程序遭到暴力破解后也拿不到lua的调试行号。

PS:

lua5.4这里也修改了,变成了两个字段,但是内存依然占用很多,所以本文的修改方法还是有参考价值的。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 方案1:
  • 方案2:
  • 方案3:
  • 方案4:
  • PS:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档