Elisp 標記-清除算法簡介

標記-清除(mark-sweep)算法
    Emacs Lisp最早使用的就是標記清除算法. 算法分爲"標記"和"清除"兩個階段.
1) 首先標記出所有正在使用的對象.
2) 回收那些所有未被標記的對象,並清除掉標記.
    該算法的缺點是:
1) 效率問題, 標記和清除的效率並不高.
2) GC運行時, 正常程序必須停下來, 降低實時性.

下面以elisp中的cons單元的爲例來釋放標記-清除的算法.
elisp中的所有對象是 lisp_object.
typedef lisp_object {
    unsigned int val;
    unsigned int type:7;
    unsigned int mark:1;
} lisp_object;
其實lisp_object是個聯合體(union), 這裏只描述lisp_object中的GC相關的部分.
type    : 表示該lisp_object的類型, 有符號,函數,cons,或者字符串等等.
val     : 表示該lisp_object的值, 一般來說是相應類型的指針.
mark    : mark-sweep算法中使用, 表示該對象是否被標記. 未被標記的對象代表着不會被引用到,要回收.

cons的結構體相對比較簡單點,就包含car和cdr兩個對象.
struct lisp_cons {
    lisp_ojbect car, cdr;
};

瞭解了cons和lisp_object對象結構之後, 我們還需要知道cons的內存申請,標記和回收.
一. cons的內存申請步驟:
  1)查看空閒的cons鏈表, 爲了節省空間把car當做鏈表指針. 如果不爲空的話,則直接從空閒鏈表中取出一個cons單元
  2)如果空閒鏈表爲空, 則從cons塊中申請一個cons單元. 一個cons塊有500個cons單元, 每個塊形成一個鏈表.
cons函數實現如下:
lisp_object Fcons(lisp_object car, libp_object cdr) {
    lisp_object val;

    if (cons_free_list) {
        // 設置lisp_object的類型爲cons, 值爲cons_free_list的指針
        XSET(val, Lisp_Cons, cons_free_list);
 
        // cons_free_list 指向鏈表的下一個地址
        cons_free_list = (struct Lisp_Cons *) XFASTINT (cons_free_list->car);
    } else {
        // cons_block_index 爲cons塊使用的索引, cons_block爲當前正在使用的cons塊
        if (cons_block_index == CONS_BLOCK_SIZE) {
            struct cons_block *new = (struct cons_block *) malloc (sizeof (struct cons_block));
            if (!new) memory_full ();
            new->next = cons_block;
            cons_block = new;
            cons_block_index = 0;
        }
        XSET (val, Lisp_Cons, &cons_block->conses[cons_block_index++]);
    }
    XCONS (val)->car = car;
    XCONS (val)->cdr = cdr;
    return val;
}
   

二. cons塊的標記. lisp的GC要標記出所有可達的對象, 就必須記錄那些全局的對象.根據全局對象對其他對象的引用鏈來標記出所有的引用對象, 這樣那些申請的,而沒被標記出的對象就是可回收的. 下面來看一下標記函數如何來標記cons對象的.
void mark_object(lisp_object *objptr) {
    switch (XGCTYPE (obj))
    case Lisp_Cons : {
        struct Lisp_Cons *ptr = XCONS (obj);
        if (XMARKBIT (ptr->car)) break;
        XMARK (ptr->car);
        mark_object(&ptr->car);
        mark_object (&ptr->cdr);
        break;

    /* other object */
    case ....:
        break;
    }
}
    
三. 標記出所有cons的可達對象之後,就需要掃描所有的被申請的cons對象, 並把那些未標記的cons對象添加的cons空閒鏈表中.
void sweep_cons(void) {
    struct cons_block *cblk;
    int lim = cons_block_index;
    int num_free = 0, num_used = 0;

    cons_free_list = 0;
 
    // 掃描所有的空閒塊
    for (cblk = cons_block; cblk; cblk = cblk->next) {
    int i;
        // 掃描空閒塊中的所有cons單元, 查看其是否被標記
    for (i = 0; i < lim; i++) {
        if (!XMARKBIT (cblk->conses[i].car)) {
            XFASTINT (cblk->conses[i].car) = (int) cons_free_list;
            num_free++;
            cons_free_list = &cblk->conses[i];
        } else {
            num_used++;
            XUNMARK (cblk->conses[i].car);
        }
        }
    lim = CONS_BLOCK_SIZE;
    }

    // 統計使用和未使用的cons單元數量
    total_conses = num_used;
    total_free_conses = num_free;
}

至於elisp其他類型的對象也依此類推, 不過是標記的地方不一樣了而已. 總體來說標記-清除算法還是比較簡單的, 不過越是簡單的東西越是基礎, 掌握的這個算法之後我們就可以進階其他的改進GC算法了.
本文參考了Emacs-18.59的代碼, 舊版本的Emacs代碼可以從這個網站上下載: ftp://ftp.gnu.org/old-gnu/emacs/.
之所以選擇舊版本代碼閱讀, 是因爲舊版本的代碼比較少,功能簡單, 核心思想卻於新版本差不多.




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章