Lua配置表优化-二进制方案
需求
随着项目组越来越大,Lua配置表内存占用越来越大,峰值超过60M。这导致在1G内存设备想达标就很困难了,可以说是必须解决的问题。
优化思路
把Lua配置表转成二进制自动格式,导出到Lua成一个Userdata,通过给userdata一个特别设计的Metatable,使它访问起来和普通的配置表没有差异。但是实现方向有几点需要考虑:
索引
优点
支持了索引,就避免读取整个表的需求,可以直接通过索引得到需要的配置行,同时也可以支持内存映射
缺点
主要是所有配置表读取的地方,以前遍历方式要改成索引,就需要修改使用的地方。这对已经开发多年的项目很不友好了。
内存映射
优点
对于大表,可以不一次把配置表加载进来
缺点
1. 小的表,比完整加载慢。
2. 表头的一些内存无法释放
原理
内存减少来源于:
1. Lua中单个值(TValue)是用12个字节表达的,而使用double nan trick的技巧,可以使用8字节来表达
2. Lua中Table需要支持读写,包含数组,Hash部分和其他信息,它是不紧凑的。而配置表是只读的,只需要支持读操作,可以使用 更紧凑的格式
3. 公共值提取,减少了对象个数
实现细节
综合考虑下,支持索引和内存映射,因为改成非索引和内存映射方式也比较容易。
实现流程
- 修改导表工具,配置表导出成lua后,读取导出的Lua文件,得到table对象,提取公共值,修改table对象
- 调用C的方法,把table转成自定义的格式文件
- 写入文件头
- 提取所有的字符串,排序后写入文件。
- 序列化table成紧凑的二进制格式,写入文件。
- 读取自定义的格式文件,检查和原表是否一致
提取公共值
在lua中实现,遍历table,把table序列化成string(注意tostring的唯一性),
- 如果在查找表中没有,就以string为key,table值为value存入lut表中。同时递归遍历子表
- 如果查找表中有,就把value指向lut中对应的value
自定义格式设计
基础类型CValue
union{
double d;
union{
long n;
struct{
struct
{
union{
uint32 refPos;
uint32 strIndex;
uint32 keyNum;
};
union{
uint16 b;
struct{
union{
uint8 keyIndex;
}
uint8 tt;
};
};
}
int16 t;
};
};
}
类型细节
上边是小端字节序,大端字节序自行对应修改。用来表示的类型如下:
- 浮点数,d表示.其中nan用0x7FF0000000表示和lua的不同。
- 整数n表示,注意n的范围只有-2ⁿ-2ⁿ-1,其中n=50。 此时t=0x7FE0-0x7FE3。实际存储的值和n也需要转换
- bool, b表示,1=true,0=false。2=nil 此时 t=0x7FE0-0x7FE4
- 引用类型。此时 t=0x7FE0-0x7FE8.
- 字符串。此时tt = 0,strIndex表示字符串编号
- 表。此时tt= 0x80,refPos表示引用对象的偏移
- table类型。此时 t=0x7FE0-0x7FEC
- 固定key的hash,此时 tt=0,keyIndex表示固定key的索引
- 数组。此时tt = 0x40-0x4F,keyNum表示数组长度
- 普通类型的hash 此时 tt=0x80,keyNum表示hash里key的个数
文件格式
-
字符串区
字符串数量|字符长度1|字符内容1|字符长度2|字符内容2|... -
固定key区
固定key数量(ushort)|偏移1(ushort)|偏移2|...|字符串数量1(uchar)|字符下标11|字符串下标12|...|字符串数量2|字符下标21|字符下标22
固定key数量表示总共有几组。
偏移1表示对应的字符串数量1的开始地址
字符串数量1表示该固定key组的key数量
字符下标11表示改key组的第一个key在字符串区的索引
以{k1=1,k2=2,k3=3}为例
固定key数量是1
偏移1是4
字符串数量1是3
字符下标11是0,表示k1的字符串编号
字符下标12是1,表示k2的字符串编号
字符下标13是2,表示k3的字符串编号
偏移表示每组的开始 -
索引区
-主键区
CValue包含类型和长度。主键是整数或者字符串。
-整数
整数按大小排序后存入。可能是char, short, int32,int64。再连续存入对应的偏移(uint32)
CValue|整数1|整数2|整数3|...|偏移1|偏移2|偏移2|...
- 字符串
按字符串区的下标,排序后存入。再连续存入对应的偏移(uint32)
CValue|下标1|下标2|下标3|...|偏移1|偏移2|偏移2|...
-索引区
支持多个多级索引。多级索引只支持字符串。多级索引用到的字符串驱虫后最多支持到32个不同,可以估算下,能支持至少10个三级索引。单个多级索引最多支持8个key值。索引用到的key对应的值,必须是字符串,整数,浮点数,bool,nil。不能是table(有需求后,可以支持空表,或者其他特殊表)
- 头部
先把所有的索引用到的字符串去重,按下标偏序后存入。
各个索引用到的字符串根据上边的索引区字符串下标,能得到唯一的uint32值,把值偏序后再存入,再存入偏移1
字符串数量(char)|字符串下标1(uint32)|字符串下标2|...|多级索引个数(char)|索引值1(uint32)|索引值2|偏移1(short)|偏移2
-索引查找区
多级索引能得到索引区字符串下标。先按照第1个标,把配置表对应的值去重后存入数量,再排序后存入值。再存入对应下一个标的起始偏移。下一个标和第1个标规则一样。最后一个标的存放规则不一样。不再存入下一级标偏移,而是符合索引的,所有结果集(行)的数量累加值。再存入结果集每一项的偏移。结果集每一项必然是个表。
1级标值数量(uint32)|1级标值1(CValue)|1级标值2|...|2级标偏移1(ushort)|2级标偏移2|...
2级标值数量(uint32)|2级标值1(CValue)|2级标值2|...|3级标偏移1(ushort)|3级标偏移2|...
...
n级标值数量(uint32)|n级标值1(CValue)|n级标值2|...|结果集数量累加值1(uint32)|结果集数量累加值2|...|结果集偏移1(uint32)|结果集偏移2|。。。 -
数据区
配置表内容,就是一个二维表。存入规则下边序列化详细介绍。 -
内存映射
如果支持内存映射,要把字符串表单独到1个文件。因为必然要全读入,且字符串要构造Lua里字符串对象后,就不再需要了。可以考虑多个配置表,合并字符串表,这个要根据实际情况来定。为了支持需不需要内存映射,所以上边所有的偏移都是指的对应于目标区的偏移,而不是整个文件开头的偏移。
序列化
table以外的类型
都可以用一个CValue来表示
table类型
先存储1个CValue对象,根据类型存储各不一样:
- 固定key的hash,只用存储value,存储和数组一样
- 数组
- 空数组 tt= 0x40
- 纯字符串的数组 tt= 0x41,直接存keyNum*uint32
- 纯数字数组
- 【-128-127】 tt= 0x42,直接存keyNum * char
- 【-65536-65535】 tt= 0x43直接存keyNum * short
- 【-2ⁿ-2ⁿ-1】, tt= 0x44,其中n=31直接存keyNum * int
- 【-2ⁿ-2ⁿ-1】, tt= 0x45,其中n=50直接存keyNum * long
- 纯bool数组 tt= 0x43,直接存keyNum/8;
-数字,字符串, bool, nil混合 tt= 0x44,直接存keyNum * CValue
-各种类型混合 tt= 0x45,直接存keyNum*uint32,再依次根据各个value存储即可。前边的uint32表示的真正value的偏移
- hash。只支持数字和字符串的hash。先连续存KeyNumCValue表示所有的key,再存KeyNumuint32。uint32表示的真正的value偏移。连续存key是为了方便二分查找。
访问封装
读
为了访问起来像1个常规的table,需要定制1个metatable。table外的返回比较容易,主要是table类型的。
-
固定key的hash,根据keyIndex,再二分查找得到索引,接下来读取和数组一样
-
数组。
- 空数组,直接返回nil
- 纯字符串数组,根据下标得到字符串索引,再返回对应的字符串
- 纯数字数组。根据下标和类型直接得到值
- 纯bool数组。根据下标和类型直接得到值
- 数字,字符串, bool, nil混合 根据下标和类型直接得到CValue
- 各种类型混合, 先拿到偏移,再根据偏移读取CValue
-
hash
先二分查找得到偏移,再根据偏移读取CValue
CValue返回
- 非table类型
读取到真正的类型,对应返回即可。 - table类型
返回userdata,存放整个配置表Config*,偏移
遍历
- next函数,pair函数,#函数,ipair函数
覆盖原来的实现,发现是个userdata,且userdata是配置表生成的,实现的对应的逻辑。
卸载
- 内存映射下,考虑只unmap内容,保留字符串表,
- 整个配置表对象随着垃圾回收释放
注意
- 处理double nan trick下小端字节序和大端字节序差异
- 处理table类型返回的userdata的缓存,以及垃圾回收处理
- 整个配置表的垃圾回收