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的缓存,以及垃圾回收处理
  • 整个配置表的垃圾回收

posted on 2022-11-08 10:58  marcher  阅读(877)  评论(0编辑  收藏  举报

导航