给 C 实现一个垃圾收集器

原文转自云风的Blog:http://blog.codingnow.com/2008/06/gc_for_c.html


给 C 实现一个垃圾收集器

粽子节假期,欧洲杯开战。为了晚上不打瞌睡,我决定写程序提神。这三天的成果就是:实现了一个 C 用的垃圾收集器。感觉不错。

话说这 C 用的垃圾收集器,也不是没人做过,比如 这个 。不过它用的指针猜测的方法,总让人心里不塌实,也让人担心其收集的效率。

我希望做一个更纯粹的 gc for C/C++ 模块,接口保持足够简单。效率足够的高。三天下来,基本完成,正在考虑要不要放到 sourceforge 上开源。等过两天彻底测试过再做打算(或许再支持一下多线程收集)。

下面列一下设计目标和实现思路。

首先,采用标记清除的 gc 策略,这是目前公认的最有效的 gc 方案。远强过用引用计数的 boost::smart_ptr 那种。

接口保持足够简单,没有太多多余的东西需要使用者留意。

最重要的是效率,除了收集过程外,所有的 api 调用都要求是近似 O(1) 的时间复杂度。


先谈谈我对传统的标记清除的 gc 算法实现的一些看法。大多数实现中,都需要对 gc 模块分配出来的内存做特殊处理,在内存的头上放一些链接数据和预留标记位。IMHO ,当内存使用量较大,大过物理内存的量时,这种方案会导致收集过程异常缓慢。因为标记的过程需要访问几乎所有的内存块,这会导致大量的虚拟内存交换。就是说,无论你是否立即需要内存块里的数据,在收集过程中,每个内存块都需要碰一下。如果还包括设置标记的话,甚至需要改写虚拟内存中的数据。

我希望改进这一点,也就是说,那所有 gc 相关的数据集中在一起,整个收集过程,除了最终释放那些不再使用的内存外,不会碰用户数据块的内存。

gc 最重要的一点,就是要对堆栈上的数据进行关联。在收集发生时,堆栈上所有临时分配出来的内存块都不应该被释放掉。C 语言本身不提供堆栈遍历的特性,所以要想个自然的方案让用户可以方便的做到这点。

在用户的调用栈上,每个调用级上,临时分配的内存都被自然挂接在当前级别的堆栈挂接点上,一旦调用返回,当前级别的所有临时内存块都应该和根断开。当然,如果内存块作为返回值出现的话,需要保留。在 C 里,我们需要给每个函数的入口和出口都做一个监护,保证 gc 的正确工作。(如果是 C++ ,要稍微方便一点,在函数进入点设置一个 guard 对象即可)因为这个监护过程会非常频繁,对其的优化是重点工作。


最终,我的 gc 库暴露了 5 个 api 供用户使用:

void * gc_malloc(size_t sz, void (*free)(const void *));
void  gc_link(const void *parent, const void *prev, const void *child);
void gc_enter();
void gc_leave(const void *value, ... );
void gc_collect();

要申请内存时,可以调用 gc_malloc 申请 sz 大小的内存。free 函数指针可选。它提供一个机会,在内存真正释放之前做一些事情。

gc_link 用于建立内存块之间的联系。可以让 child 指针依赖 parent 指针。既,child 的生命期不会短于 parent 。这个 api 还可以取消 prev 和 parent 之间的联系。parent prev child 中任何一个都可以传空指针。当parent 为空时,child 挂接到根上。这通常用于维系全局变量的生命期。gc_link 保证 prev 在堆栈上有一次临时的引用。

gc_enter 和 gc_leave 当配对使用,放在一个函数或一段语句块的入口和出口处。夹在 enter 和 leave 之间的 gc_malloc申请的内存块,生命期不会超过临近的 leave 指令。除非在 gc_leave 的参数中指明需要延长生命期。gc_leave 可以带多个指针,只需要最后一个以 0 结束。这通常用于函数的返回值。

gc_collect 用于垃圾收集,它可以在任何时机调用,把和根没有关联的内存块全部释放掉。堆栈上(没有闭合的 enter / leave 对)的所有 gc_malloc 分配的内存块都会被自动挂接在根上;用户也可以用 gc_link 主动挂接(parent 传 0)。


这套接口设计的应该是足够简洁了。用户只需要自己描述对象和对象之间的关系(使用 gc_link),别的不用太操心。

如果使用 C++ 可以进一步的封装,重载赋值操作符来做到这些。而 C 也可以定义一个宏来辅助(注意宏的一些问题,比如重复计算)。比如:

static void 
eval(void *parent,void **node,void *child)
{
    gc_link(parent,*node,child);
    *node=child;
}

#define EVAL(obj, prop, value) eval( (obj), & ((obj)-> ## prop), (value))

struct tree {
    struct tree *left;
    struct tree *right;
};

struct tree *
new_node()
{
    struct tree *n=(struct tree *)gc_malloc(sizeof(*n),0);
    memset(n,0,sizeof(*n));
    return p;
}

struct tree *
foo()
{
    struct tree *t;
    gc_enter();

    t=new_node();
    EVAL(t,left,new_node());
    EVAL(t,right,new_node());

    gc_leave(t,0);

    return t;
}

上面这个 foo 函数演示了 gc 模块的基本用法:构造了一个节点 t ,以及另外两个临时节点连接到 t 的 left 和 right 两个成员上。最后把 t 返回。


下面谈一下优化:

为了让用户数据块和关联数据分离,所以模块内部实现的时候,将指针映射到了内部 id 上,这里使用了一个 hash map 。这样,可以使用 id 保持相互关联的信息。

对象之间的关联信息是一个图结构。图的边的构建和变动复杂度较大。在实现时,做了一个 cache ,在 gc_link 的时候,不直接增删边。而是缓存对图变更的请求,并在缓冲期间合并一些操作。例如,一些临时的关联信息,可能因为周期很短,在 collect 发生前就已经解除关联,其操作就会被抵消掉。

cache 了大量操作后,对操作进行排序。批量修改图的边时,也可以减少大量的运算。(内部数据结构对图的每个节点的孩子采用一个有序数组保存,比较适合批量增删)

gc_enter 和 gc_leave 是优化的重点。因为这个调用最为频繁。而 gc_collect 发生较少,对象频繁进出堆栈,不需要重复挂接。

采用另一个 cache ,直接保存内存指针,甚至可以不做 hash map 查询(映射到内部 id ),只到 collect 发生时再一次计算。临时对象存在于堆栈上时,是一个树结构,而非图结构的关联关系(每个堆栈上的调用级是一个树节点)。这也有利于优化处理。


整个实现代码只用了 600 多行,但是却写了三个晚上。主要是为了提高处理效率(时间和空间效率),设计了一些精巧的数据结构,控制起来非常麻烦,写起来也很是小心。这次完成后,就可以替换掉去年实现的一个不太地道的 gc 模块了。当时的那个需要依赖一个单根的类树,用起来要麻烦的多。

如果日后开源的话,还有一些事情要做:代码需要更规范,补上更详细的测试代码,以及支持 64 位系统等。


6 月 10 日补充:

今天把它在 google code 上开源了,用的 BSD 的许可协议。第一个版本还很 dirty 。没有怎么测试,可能还有许多 bug 。

有兴趣的同学可以在这里用 svn check out (尚未 release ,没做下载包) :

http://code.google.com/p/manualgc/

ps. 我的英文很滥,注释和说明可以无视。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,二值贝叶斯分类器是一种基于贝叶斯定理的分类器,它假设每个特征都是二元的,即只有两个取值,比如真和假。下面是一个简单的二值贝叶斯分类器的实现: 1. 收集数据:收集一些已经标注好类别的数据,比如垃圾邮件和非垃圾邮件。 2. 准备数据:将数据转换为二元特征,比如将文本中的单词转换为是否出现的二元特征。 3. 计算先验概率:计算每个类别的先验概率,即 P(c),其中 c 表示某个类别。 4. 计算条件概率:计算每个特征在每个类别下的条件概率,即 P(x|c),其中 x 表示某个特征,c 表示某个类别。 5. 计算后验概率:对于新的样本,计算它属于每个类别的后验概率,即 P(c|x),并选择后验概率最大的类别作为预测结果。 下面是一个简单的 Python 实现: ```python import numpy as np class BinaryNaiveBayes: def __init__(self): self.priors = None self.likelihoods = None def fit(self, X, y): n_samples, n_features = X.shape self.priors = np.zeros(2) self.likelihoods = np.zeros((2, n_features, 2)) for c in [0, 1]: X_c = X[y == c] self.priors[c] = len(X_c) / n_samples for i in range(n_features): self.likelihoods[c, i, 0] = np.mean(X_c[:, i] == 0) self.likelihoods[c, i, 1] = np.mean(X_c[:, i] == 1) def predict(self, X): n_samples, n_features = X.shape posteriors = np.zeros((n_samples, 2)) for c in [0, 1]: likelihoods_c = self.likelihoods[c, np.arange(n_features), X] posteriors[:, c] = np.log(self.priors[c]) + np.sum(np.log(likelihoods_c), axis=1) return np.argmax(posteriors, axis=1) ``` 其中 X 表示训练数据的特征矩阵,y 表示训练数据的标签。fit 方法用于训练模型,predict 方法用于预测新的样本的类别。在训练模型时,我们计算了每个类别的先验概率和每个特征在每个类别下的条件概率。在预测时,我们计算了每个类别的后验概率,并选择后验概率最大的类别作为预测结果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值