各种垃圾收集的随便笔记

Posted on 四月 11th, 2010 in 笔记 | 4 Comments »

GC多好多好就不多说了。没银弹嘛,什么好处什么坏处都了解下就好 TvT

引用计数

“穷人的垃圾收集”。实现简单,可以在对象失去引用时立即收集,不过对付不了循环引用。“立即收集”容易给人一种“高效”的印象,其实正相反,引用计数中的每个mov指令后面都必须跟着几条额外指令,引用的局部性还不好。跟各种GC算法比起来,引用计数的效率是最低的 :(

CPython是引用计数加备用的mark-sweep,(好像如果一开始就使用GC的话就没必要引入那套复杂的内存池机制了?

mark-sweep

从根(寄存器、局部变量和全局变量等)开始,遍历所有可访问的对象并标记之;再遍历所有变量,将所有没mark的对象收集。

C\C++中的GC实现大约是扫描bss段和堆栈什么的,找到全局变量和局部变量。C\C++中你不知道一块数据是指针还是一个纯洁的整数,所以只能用个is point to heap来猜它是不是指针,如果这个整数恰好是一块内存的地址,就只能“保守地”当它是指针mark掉了。保守式的GC中比较有名的就是Boehm的GC(貌似很靠谱!)。

停止-复制

这个需要额外一倍的内存空间。先将一切活动停止,遍历堆中所有可以访问的对象并将其拷贝到另个堆中,并将将原先的指针统统指向新的堆中对应的地址,完毕后继续程序的执行。

停止复制的同时也紧缩了内存,因而不存在内存碎片的问题(malloc/free在整理堆中碎片时候的各种搬运不容小觑),性能上好像要优于mark-sweep。缺点就是对空间浪费比较大(一倍啊一倍~)。

分代收集

遍历所有对象时候的停滞会比较厉害。想办法把停滞缩短到人类察觉不到吧…就有了分代收集和增量收集 TvT

分代收集基于这样的假设:

1.对象越新,生存期越短;
2.对象越老,生存期越长;
3.少有指向新对象的老对象

就把堆分成n块,对应n代的对象(三代就差不多吧)。再加个记忆表什么的记下指向新对象的老对象,以记忆表中的老对象以及活跃的变量等做根,重点照顾最新的这代就行了。

.net那CLR好像是分代的…缺点是经过多次收集,较老的代会积攒比较多的垃圾。

增量收集

增量收集好像是记住上次遍历到了什么地方,然后每次遍历一点,最后集中收集。

有个三色标记:
白色:未被访问的对象;
灰色:已经被访问过,子对象还未访问;
黑色:已经被访问过,子对象也已被访问;

然后就是性质:
当没有灰色对象时,所有的白色对象都是垃圾;
不会有黑色对象指向白色对象;
每个灰色对象都位于收集器的队列中(作为下次收集的根);

Fleurer’s Stack VM

Posted on 二月 3rd, 2010 in 备忘 | 14 Comments »

http://github.com/Fleurer/fsvm
尝试用C写的堆栈机,好像烂尾了 – -!

只是个运行时,无视了语法分析。随便写的东西也没什么规划,编写的时候就郁闷不知道哪部分该归分析器那部分该归vm,也不知道现有的部分能不能真用到解释器上,于是华丽地烂尾~只实现了20来条指令,可以递归可以闭包,不过不能算乘法…囧,很简陋啦~

好像是第一次写C,不会make就先凑合了rake – -! 对C不熟悉,满地的Segmentation Fault对我们只会用printf调试的菜鸟太残酷了…用了那个保守式的gc库Boehm GC,面对满地的malloc而无free不清楚泄漏起来会怎样…囧

本来是对C++那套OO无爱,想单用struct和函数也可以OO么。于是用了C,然后就后悔了:我不想重新实现hashmap之类的东西,C++那stl多好…T_T。倒也找到了个C的泛型库khash,不过宏终究不如模板来的好看…现在想来,信息学奥赛acm中用C的那些同学做题的时候都是自己实现一遍各个数据结构么?

拿段伪代码:

def main:
     sum(10)
 
def sum(i):
     if (i==0) : return 0;
     else: return(i+sum(i-1));

放到fsvm下大约是这样:

int test_rec(){
    Op op_main[]={
        OP_PUSH_NUM, 10, 
        OP_PUSH_CONST, 0, //"sum"  
        OP_PUSH_VAR, 
        OP_CALL, 1,
        OP_RET
    };
    Op op_sum[]={
        OP_PUSH_CONST, 0, //"i"
        OP_PUSH_VAR, 
        OP_POP_TMP, 0, //store i
        OP_PUSH_TMP, 0, //push i
        OP_PUSH_NUM, 0, 
        OP_EQ,
        OP_NOT, // i!=0?
        OP_BRANCH, 3, 
            OP_PUSH_NUM, 0,
            OP_RET,
        //else
            OP_PUSH_TMP, 0, 
            OP_PUSH_NUM, 1, //1
            OP_SUB, 
            OP_PUSH_CONST, 1, //"sum"
            OP_PUSH_VAR, 
            OP_CALL, 1,   //sum(tmp[0]-1)
            OP_PUSH_TMP, 0, 
            OP_PRINT_STACK, 
            OP_ADD, //tmp[0]+sum(tmp[0]-1)
        OP_RET
    };
    Env *env=fnew_env(NULL); 
 
    Proto *p_main = fnew_proto(op_main, 0);
    fset_const(p_main, 0, fstr("sum"));
    Func* f_main=fnew_func(p_main, env);
    Obj o_main=ffunc(f_main);
 
    Proto* p_sum=fnew_proto(op_sum, 1);
    fset_const(p_sum, 0, fstr("i"));
    fset_const(p_sum, 1, fstr("sum"));
    Func* f_sum=fnew_func(p_sum, env);
    Obj o_sum=ffunc(f_sum);
 
    fbind_var(env, "sum", o_sum);
 
    fio_puts(fcall(0, f_main));
    return 0;
}

创建函数的那几个函数我自己也看着别扭…不过写C还是老实点好 >_<

typedef unsigned long VALUE;

Posted on 一月 19th, 2010 in 笔记 | 8 Comments »

因为swdpress.cn的死掉严重打击积极性,好久没更新了。此文纯凑数。

ruby中除了Fixnum,其余所有类型的值都是引用传递。读《ruby hacking guide》时看到如下的定义:

typedef unsigned long VALUE;

同lua那union+tag不一样,ruby中所有类型的值都存放在一个VALUE中,而没有tag指明其类型。如果是Fixnum,就把数值直接放在VALUE里;如果是其它类型,则存放其地址(unsigned long和C中指针类型的长度一致)。不过万一作为Fixnum的VALUE指向的地址与其他对象有重叠怎么办?它又怎么区别数值和地址呢?

这用到了一点tricks,C的struct在内存中都是以4字节对齐,因此ruby中所有对象的地址都偶数。在表示Fixnum时,ruby就将C的int值左移一位再加一,使其看起来总是个奇数,这样就不会与ruby的其它对象有重叠。

 123  #define INT2FIX(i) ((VALUE)(((long)(i))<<1 | FIXNUM_FLAG))
 122  #define FIXNUM_FLAG 0x01
(ruby.h)

判断一个VALUE是Fixnum还是地址,只需判断VALUE是奇数还是偶数就行了。

表示true,false和nil是这样:

 164  #define Qfalse 0        /* Ruby's false */
 165  #define Qtrue  2        /* Ruby's true */
 166  #define Qnil   4        /* Ruby's nil */
(ruby.h)

都是偶数。像刚才说的,它们不会被当作是地址了么?这就用到了另一个trick:进程虚拟地址空间的前一部分都是不可访问的。因而0,2,4的地址上不会存在ruby对象。

PS: 感谢FX大大提醒,这套方法有个标准的名字叫做“tagged pointer” :)