作者:benderzhao
方案
常见的内存优化方法有很多,针对不同的场景有不同的解决方案,下面按照由简到繁的顺序一一道来。
字段裁剪
显而易见,把没用的字段干掉,就可以省内存。根据前文的内存计算公式,哪怕只存了一个bool值,占用也是16字节。因此,首先考虑是去掉一些完全没用的字段,其次是去掉一些默认值的字段。
比如游戏里常见的物品,有id、数量、各种属性等。如果出于方便或者可读性,亦或者C 良好的编码习惯,为每个字段都设置一个初始值,那么物品结构就大概长这样:
代码语言:javascript复制local item = {
id = 123123,
count = 1,
property1 = 0,
property2 = 0,
property3 = "",
property4 = false,
}
这种写法property1
到property4
的默认值占用了大量的内存,单个item
从2个Key-Value变为了6个,内存也膨胀了3倍。
比较节省内存的做法是无默认值,在使用时or下即可:
代码语言:javascript复制local property1 = item.property1 or 0
当然,如果使用的地方特别多,比如有上万处地方直接使用了item.property1
,这种改动还是太大。这个后面会讲到还有别的方法。
结构调整
如果不熟悉Lua的实现,雀食会设计出一些常见于C ,但不友好于Lua内存的结构。
还是用物品举例,假设一个玩家有1000个物品,每个物品有一些属性。比较符合直觉的数据结构是这样:
代码语言:javascript复制local items = {
[10001] = {count = 1, property1 = 1},
[10002] = {count = 2, property2 = 4},
[10003] = {count = 4, property4 = 10},
...
[11000] = {count = 3, property1 = 2},
}
使用一个Table
来存储所有的物品,每个Key是物品id,Value是具体的属性。
这种结构浅显易懂,但是有个问题,总共有1000 个Table
,而Table
不同于C 的struct,它是有很大的内存开销的,这种数据结构占用的内存会出乎意料的大,在这里光Table的占用就会有几十KB。
考虑换成另一种结构:
代码语言:javascript复制local items = {
count = {
[10001] = 1,
...
[11000] = 3,
},
property1 = {
[10001] = 1
...
[11000] = 2,
}
...
}
这里把相同的字段放在一起,比如所有的count
是一个Table
,Key是物品id,Value是数量。这种结构与前面的对比,Key-Value的数量是没差别的,但是只有个位数的Table
,对比前面的1000 ,有几个数量级的差距。
当然,改动还是比较大的,但是如果对于这个结构的访问都收敛到物品模块内,对外只提供接口,那就还可以接受。
对于其他结构也是一样,主旨就是减少Table
的使用。
提取公共表
前面字段裁剪提到,如果有一些默认字段不好剔除,比如有上万次使用的地方,挨个去加判断肯定不现实,因此可以考虑提取元表来优化。
还是用物品举例,假设有1000个物品,每个物品有3个属性,绝大部分情况下都是默认值0。
代码语言:javascript复制local items = {
[10001] = {count = 1, property1 = 0, property2 = 0, property3 = 0},
[10002] = {count = 2, property1 = 0, property2 = 0, property3 = 0},
[10003] = {count = 4, property1 = 0, property2 = 0, property3 = 0},
...
[11000] = {count = 3, property1 = 1, property2 = 0, property3 = 0},
}
因为每个物品结构的字段都是一样,且大部分都是相同的值为0,因此我们提取一个元表base
:
local base = {
property1 = 0, property2 = 0, property3 = 0
}
然后将原始数据里与base
内容一样的字段剔除掉,变为:
local items = {
[10001] = {count = 1},
[10002] = {count = 2},
[10003] = {count = 4},
...
[11000] = {count = 3, property1 = 1,
}
再为每个物品设置元表,get不到的字段就去base
里找。set则只在自己的Table
里设置。所有物品共用一张元表。
显而易见,通过共用base
的默认值,很多重复的Key-Value被优化掉了,也就节省了内存。
这种方法适合于结构一致,且有大量相同值的情况。
内存压缩
假如结构不一致,或者字段的值都各不相同,又该如何优化呢?例如我们已经把没用的字段剔除了,现在物品结构长这样:
代码语言:javascript复制local items = {
[10001] = {count = 1},
[10002] = {count = 2},
[10003] = {count = 4},
...
[11000] = {count = 3, property1 = 1,
}
考虑前面的指导思想,就是减少Table
的使用,因此我们可以考虑把Table
序列化为字符串,例如变成:
local items = {
[10001] = "count=1",
[10002] = "count=2",
[10003] = "count=4",
...
[11000] = "count=3,property1=1",
}
这样就少了一大堆的二级的Table
。当然,这种序列化格式还是比较占内存,这里只是举个例子方便理解。实际可以序列化为紧凑的二进制形式。
改为字符串后,要是想访问里面的count
,怎么办?还是设置元表,在使用的时候还原回Table
即可。
而既然都序列化为二进制字符串了,那干脆再调用下lz4压缩下,牺牲一点点CPU换来更高的优化效果。比如变为:
代码语言:javascript复制local items = {
[10001] = "