数据加载策略

一般稍具规模的软件往往都少不了配置,游戏软件更是如此。

一个完整个游戏软件的配置数据往往有数MByte甚至更多, 完全加载进同内存往往需要几秒的时间。

几秒钟听起来不长,然而如果是在客户端刚打开时加载,这点延时却时致命性的,因此必须将配置的加载速度优化到足够快。


其中一种办法是在实现细节上做文章。

假设配置表为xml格式,我们可以在打包前优化配置文件的结构。

比如可以在使用xml文件之前,将所有xml文件拼接成一个文件,会提高不少加载速度。这是因为使用一个文件包含所有内容,在加载时会减少许多有关文件操作有关的系统调用。而系统调用是性能杀手。

虽然尽可能的减少了系统调用次数,但是xml解析的开销会占整个加载开销的绝大部分。因此只优化系统调用可能效果并不是非常明显。

如果再进一步,在客户端运行前就将xml解析完成,客户端启动后直接使用解析后的数据将可以显著提高加载速度。

做法就是,向客户端提供的配置文件其实并不再是xml格式的文件了,而是通过工具将xml文件转换成仔细设计过的数据结构。

在客户端加载时不再读取xml,而是直接读取二进制数据,然后在进程中将数据反序列化成将要被使用的数据对象。二进制的解析速度远高于文件的解析速度,尤其是这种二进制数据结构还是经过仔细设计过的。如果偷懒其实还可以直接使用google protocol buffer的序列化数据结构来做数据库存储。


虽然对于一般情况下来说,上面的优化方式就够了,但是我不是很喜欢这种优化方式。

当xml文件的大小继续膨胀时,一定有会有一个临界值是上面的优化手段不能解决的,因此我更希望能从设计上去解决他。

其实从设计上解决xml加载慢的问题,看起来与上面的做法恰好相反。那就是不要将所有xml文件拼接起来(如果是由于特殊原因必须将所有资源文件合并为一个文件,可以采取虚拟文件系统,这里所说的不拼接,目的在于能够随机访问到某个文件的全部内容).

不将xml文件合并的意义就在于程序具有局部性原理,那些业界大牛们依据这一原理创造出来许多可以提高程序运行速度的设施,比如cpu缓存等。

换句话说,我们的程序刚启动时不可能需要所有的xml文件,很可能只是需要其中很少的几个xml文件而已。如果我们单纯的去加载这几个文件,在毫秒级的时间就可以完成。

这样首屏加载速度将会提升到一个质的高度,并且无论你程序增加多少个系统,xml爆增多少,都将不会再影响程序的启动速度。

那么剩下的文件要怎么加载呢,由于把文件分散开了,因此所有文件的整体加载时间必将会延长,这是没有办法改变的事实。但我们依然可以在设计上来加速剩余文件的加载速度。

永远都不要忽视计算机的发展速度,尤其是手机cpu的发展速度,现在随随便便一个手机也都至少4核了,主界面渲染和大部分逻辑仅使用一个线程去处理。

再看一下配置表,由于是只读的,而且模块性良好,天然支持并发,而xml加载速度慢的主要瓶颈在cpu的消耗。

如果我们在客户端启时就启动一个线程池去加载除主界面需要的xml文件之外的所有文件,那么在保证启动速度的情况下,还能显著提高加载速度。这也是单文件配置xml不具有的优点。


为了把这些细节剥离开了,应该把上述操作抽象成为一个config模块。config模块内置线程池负责解析xml文件,将将解析后的数据放入到内存缓存。

此模块提供三个数据访问接口:loadasync、load、drop。

loadasync是用于通知config模块准备加载某配置文件。应用逻辑并不需要等待配置文件的加载完成。loadasync首先过虑出不在缓存中的文件,然后将其发往线程池进行加载,但并不等待文件加载完成,直接返回。

load用于阻塞等待config模块加载完某配置文件。当被调用后,load首先过虑出当前缓存中不存在的配置文件,然后将其发往线程池去解析。当线程池解析完成时,此函数返回。

drop函数用于微调config模块的缓存策略,如果进程使用内存紧迫时,可以根据一些算法(如LRU)清某个模块需要的配置表清理出内存,当需要时再从新载入内存。

同样世界上没有银弹,上述策略并不适用于服务器程序,因为服务器程序启动慢上几s并不会有很大影响,而加上config模块后,除了增加实现复杂度外,并不会有半点好处。还有可能因为数据没有提前加载好,而造成客户端网络请求处理过慢,从而影响服务器程序的并发性能。



发表评论