资源管理系统的初步想法及其它问题(续)

显存资源管理
    上次说到自己做显存资源管理,也就是说全部采用 D3DPOOL_DEFAULT 方式创建设备资源,当显存耗尽时,引擎使用 LRU 或者 MRU 算法来释放显存资源。这几天翻了翻国外网站,也看了一下主流的 3D 引擎的资源创建方式,大部分都倾向于采用托管的设备资源管理(D3DPOOL_MANAGED),理由有这么几点:
1.D3D 或者 Driver 对资源的管理更加高效,因为是在操作系统的内核模式下,所以当恢复资源时一定会比用户模式下更快。
2.由于 Managed 的含义是指托管给 D3D/Driver,则 D3D/Drvier 一定会根据自己的最优化方案来进行设备资源的管理。这一点要比自己做管理要灵活得多,自己管理的无论多么高效,总是不能放之四海而皆准,在不同的硬件平台下可能会产生完全不同的结果。
3.资源的管理结构简单、清晰,这是显而易见的。
    基于以上几点,调整引擎的设备资源管理方案,除了一些必要的 Default 资源外,其余全部以托管的方式来创建,这样引擎不必管理设备相关的资源,当显存不够时,Driver 会自动清除一定的 Managed 资源;当渲染不在 GPU Local memory 的资源时,Driver 会自动 Upload 到 GPU 中,整个过程对引擎完全透明,流程清晰简单。不过还是有必要跟踪显存使用情况,为调试性能提供参考数据。
File Mapping
    我之前关于 FM 的 MapViewOfFile 函数的理解是错误的。我以为在 MapViewOfFile 时操作系统会分配一段内存并将映射的文件数据拷贝到内存中,实际上并非如此,当 MapViewOfFile 时系统仅仅是做了内存地址的映射,并没有作任何文件数据的读取,但是当使用 MapViewOfFile 返回的内存指针访问/修改数据时,才会产生真正的文件读取操作。另外,使用 FM 不一定比读取普通文件更快,经过测试发现:当读取的数据在操作系统分配粒度(64k)以下时(包括64k),会有较明显的性能提升,但是当超过分配粒度大小时,性能会下降,读取的数据越大,性能下降的越厉害。对此种现象目前我没有找到任何解释答案,初步怀疑操作系统本身有缓存机制。最后测试 33k+ 的文件,总计 1.5G 以上,平均读取性能要快 30% 左右。
文件系统
    在原有方案中修改包数据存储时将包中所有数据都读入内存,再存入硬盘,这样效率太低,虽然可以多次修改一次保存来提高效率,但是如果包更新时(比如网络游戏更新游戏资源)对于大文件的数据包,存储的时间还是难以让人忍受的,更大的问题是在长时间的存储过程中发生异常情况的几率会变大,发生异常时会造成整个包无法读取,影响用户感受。所以要采取一定的策略提高包保存的性能。
    目前采取的策略:新增数据时,通过文件结构信息获取最适合的空间进行存储。在修改数据时,首先判断修改后的数据大小是否和原数据匹配,如果大于原数据,则将数据保存在文件尾处;删除数据时直接将文件结构信息中的文件信息删除,原有数据不作任何改动。
    实际上这是一种 Append 方式的数据更新机制,势必会造成包中存在碎片,如果不经整理,包会越来越大,碎片会越来越多,因此,需要增加碎片整理机制,整理包的算法基本如下:通过文件结构信息获取最适合的空间安排,从前到后的进行数据迁移,保证不会进行多余的数据拷贝。接口提供整理级别,整理大小,整理范围等参数。
引擎的线程模型及资源管理的线程同步策略
    初步打算将引擎的渲染作为单独线程,引擎的场景图更新放在另一线程,可以放在游戏逻辑主线程。渲染线程中使用 Command buffer 来处理所有请求,包括:设备资源创建请求、设备资源创建完成、渲染请求等等。设备资源数据的 Upload 由设备资源 Upload 线程完成。内存资源的创建由 IO 线程完成。这样引擎最多会同时存在 4 个线程:场景图线程、渲染线程、设备资源 Upload 线程,IO 线程。
    当画完线程处理流程图之后我才发现,需要同步的地方太多了,虽然锁中完成工作并不复杂,但是频繁的锁定会严重影响性能,因为在锁定的同时会使其它线程挂起(这一点在多核平台下后果会更严重,会造成其它核心挂起),严重降低 CPU 的使用率(因为其它线程在等待),不能完全并行。更为重要的是可能会造成死锁,虽然可以从设计上避免这个问题,但还会存在潜在的危险。一开始我首先想到的是 LockFree,这是一种乐观同步机制:当修改数据时,假定没有并发操作发生,在真正提交数据时如果和之前的期望数据不一致,则继续循环直到一致为止。实际上 LockFree 还是基于 CPU 的原子操作 CAS ,这个指令很多 CPU 都已提供,x86 还提供了 CAS2 语义的指令,但是 LockFree 最困难的地方在于内存管理,当数据被修改时也可能被读取,所以不能立刻销毁数据对应的内存,但何时销毁就是个比较麻烦的事情了。由于资源管理线程基本上是一个生产者和一个消费者,并且访问是顺序的,所以可以使用 RingBuffer 来完成同步,这也是一种 LockFree 同步机制,不同的是它无需采用 CAS,因为两个线程不会修改同一数据(但是会访问同一数据,所以还要做重试机制)。
此条目发表在DreamEngine 3D 图形引擎开发分类目录。将固定链接加入收藏夹。

5 Responses to 资源管理系统的初步想法及其它问题(续)

  1. shi说道:

    老大,64k是一个内存page,超过之后变成多个page,如果文件不连续,mapping到内存就不连续,所以就慢吧?file mapping一向就慢吧,性能的提升主要在于一部分经常用到的resource已经在内存page中以后,速度就会快起来,而且不用自己操心哪些文件常用要常驻内存,哪些可以释放掉,整个地图maping起来,内存管理让os kernel去做。。。

  2. 说道:

    被映射的文件数据是连续的,所以不应该是这个问题造成的。因为 FM 使用虚拟内存技术实现,所以还是有可能跟这个 64k 内存页大小有关。其实我在读取数据之后就立刻释放掉 mapping,如果不释放还是会占用内存,而且在读取数据之后,引擎自己也会离散的保存特定内存数据。现在看来,用 FM 来加速读取也许并不是最好的选择…

  3. shi说道:

    是了,FM的运行效率一直都不是上乘之选吧,只能说给开发者方便了。。。

  4. wu说道:

    用Fiber可以完全不用同步,省去很多麻烦。

  5. liu说道:

    哥们, 宝贝女儿回上来了吧? 宝贝女儿长得很精神。
    快过年了,提前给你拜个年吧。

发表评论 取消回复