c++ 多线程 垃圾回收器_如何写一个C++垃圾回收器(一)

最近因为工作缘故,通过为一款Unity手游做性能优化,接触到一个写的很好,也是在实际中应用非常广泛的一个C++垃圾回收器————BoehmGC。这里直接引用来自维基百科的词条

Boehm-Demers-Weiser garbage collector,也就是著名的Boehm GC,是计算机应用在C/C++语言上的一个保守的垃圾回收器,可应用于许多经由C/C++开发的项目,同时也适用于其它运行环境的各类编程语言,包括了GNU版Java编译器运行环境,以及Mono的Microsoft .NET移植平台。

我之前对垃圾收集领域的经验,主要来自自研引擎的GC模块,类似Unreal Engine里基于UObject的垃圾回收。和BoehmGC相比,虽然两者原理都是差不多(无非是对二十年前基本都已成型的垃圾回收算法的实现),但毕竟只作用于一个大型系统中一个相对小而核心的模块,和BoehmGC这种系统级的库在工程规模、实现难度、代码质量上还是有着不小的差距。

当然,具体到游戏引擎这个case,具体使用哪个方案,仍然要看引擎的设计理念和技术框架。比如我们的自研引擎和UE4,因为是从底层架构到逻辑代码都基于C++实现,所以可以用一个相对轻量级的GC实现(实际上窃以为,GC即便没有也并非无法接受)。而Unity选择了对开发者相对更友好的C#语言,在底层采用BoehmGC这种相对重量级的框架也无可厚非。

本文的主旨并不在于讨论这种设计上的取舍,所以我们还是尽快进入正题————从BoehmGC源码学习如何写一个C++垃圾回收器吧!

BoehmGC中的内存分配

看到这里,你可能会有点儿不解,我们不是要谈怎么写内存回收器么,为什么要扯内存分配呢?

其实,垃圾回收和内存分配本就是一体两面,是垃圾回收器设计的时候就会考虑到的问题。接管应用的内存分配,也就拥有了“哪些内存可能需要归还给系统”这样的重要信息。

作为一个重量级的基础组件库,BoehmGC的使用方法非常简单,只需要把系统函数malloc替换为GC_malloc即可,之后你就完全不用管何时free的问题。在小型项目里,你甚至可以直接

#define malloc(n) GC_malloc(n)

然后再不用管free,BoehmGC自会帮你打理好一切。

既然全盘接管了内存分配,那就必须做到以下两点,才能称得上是合格的分配器 1. 分配的效率要高 2. 尽量避免内存浪费,避免碎片化等

BoehmGC的内存分配架构

在整个内存分配链的最底部,BoehmGC通过平台相关接口来向操作系统申请内存(可能是malloc, sbrk, 或者mmap等)。为了提高效率会根据配置,每次批量申请4K的倍数大小,除了用户能使用的内存之外,还有BoehmGC内部维护的数据结构(通过GC_scratch_alloc分配)。

分配器的核心是一个分级的结构,BoehmGC把每次申请根据内存大小归类成小内存对象(Small Object)和大内存对象(Large Object),这点和STL的分配器也比较相似。归类的依据具体来说就是,

  • 不超过PageSize/2,也就是2048字节的对象为小内存对象
  • 大于PageSize/2的对象为大内存对象
//heap block定义了一个页,大小为4K的倍数

对于Large Object,向上取整到4K的倍数大小,直接以整数个hblk的形式给出。而Small Object则会先申请一个hblk出来,而后在这块内存上进一步细分为Small Objects,形成free-list。

BoehmGC的内存管理策略

为了尽量减少碎片化和加速分配,BoehmGC在设计上就做了一些限制,充分体现了“物以类聚”的思想。

  • 首先,GC管理的对象有一个最小的“粒度”,即Granule。32位上这个值是8字节,64位则是16字节。在64位环境下,即使用户申请的内存是10个字节,也会被向上调整到16字节。
  • 一个在用的hblk如果不是属于一个large object,那就是容纳了若干个等大小的small object。

对于有一定内存分配器实现经验的开发者来说,以上两点应该都比较熟悉了,不过BoehmGC把这种“物以类聚”的设计贯彻落实得更加彻底。

  • 对于大内存对象(large object),按照对应的hblk数,把他们归类到若干个freelist中。具体的做法可以参考GC_hblk_fl_from_blocks和GC_allochblk_nth。
  • 当大内存对象被垃圾回收的时候,会尝试把相邻的hblk合并,减少内存碎片。
  • 对于小内存对象的大小分档,也不是完全按照Granule的等差数列来决定。有些临近的大小会被优化合并掉,比如系统当前有很多1024字节的闲置块,但申请1008字节的小内存对象仍然可能miss。此时用1024字节的块可能是更好的选择,适当的合并临近的block size可以优化内存分配效率。这块的做法可以参考GC_init_size_map和GC_extend_size_map。

BoehmGC的代码实现

对于很多工程问题来说,算法或者大体的思路,大家可能都是相差不多,最终的品质很大程度上取决于代码实现的功力,而BoehmGC的代码就展现了这番功力。

GC_arrays

BoehmGC把全局数据结构放在GC_arrays这个全局变量里,并定义了各个字段的快速访问方式。

struct 

二级页表

每个hblk对应有一个元信息(hblkhdr),里面存放了object类型、大小等信息,也包含GC所需要的字段比如mark数组等。

struct 

BoehmGC管理所有的内存分配用到一个小技巧,因为64位地址空间是非常大的,如果用数组来管理header,那这个数组占用的内存我们可承受不起。这种情况下我们一般使用hash表,在BoehmGC里就是GC_arrays里的GC_top_index。

BoehmGC采用二级页表的结构,首先会按照内存地址的高42位计算得到hash表中的数组index。看下bottom_index的定义就知道为什么是42位了(64-10-12,2**12=4096是hblk的大小)

typedef 

而计算hdr的代码为了性能考虑,也多用宏和内联函数来实现,

/*LOG_HBLKSIZE = 12, BOTTOM_SZ = 10*/

总结

在3A游戏引擎这种大工程里,一般都不会直接用操作系统自带的内存分配函数例如malloc等。标准做法是写一套自己的内存分配类/函数接管所有内存分配,所以BoehmGC的源码其实也是一个非常好的学习例子。

下一篇来学习BohemGC的垃圾回收实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值