2011年4月13日 星期三

Haskell 與 State Monad

昨天試著用 HaskellState Monad,花了一點時間才寫出一個簡單的猜數字遊戲。感覺要在 Functional Programming Language 寫出 State 的概念比較困難...

import Control.Monad.State
import System.IO
import System.Random

gameLB, gameUB :: Integer
gameLB = 0
gameUB = 100

data GameState = GameState { steps :: Integer ,
                             lb :: Integer,
                             ub :: Integer }

initialState :: Integer -> Integer -> GameState
initialState l u = GameState { steps = 0, lb = l, ub = u }

updateState :: Integer -> Integer -> GameState -> GameState
updateState ans guess (GameState { steps = s, lb = l, ub = u }) =
    case (compare ans guess) of
      EQ -> GameState { steps = s + 1, lb = ans, ub = ans }
      LT -> GameState { steps = s + 1, lb = l, ub = min (guess - 1) u }
      GT -> GameState { steps = s + 1, lb = max l (guess + 1), ub = u }

range :: GameState -> (Integer, Integer)
range (GameState { lb = l, ub = u }) = (l , u)

stepCount :: GameState -> Integer
stepCount (GameState { steps = s }) = s

gameSession :: Integer -> StateT GameState IO Integer
gameSession ans =
    do (l, u) <- gets range
       lift $ putStr $ "Guess number (" ++ show l ++ ".." ++ show u ++ "): "
       lift $ hFlush stdout
       str <- lift $ getLine
       let num = read str
       modify (updateState ans num)
       case (compare ans num) of
         EQ -> gets stepCount >>= return
         LT -> gameSession ans >>= return
         GT -> gameSession ans >>= return

main :: IO ()
main = do rand <- getStdRandom (randomR (gameLB, gameUB))
          s <- evalStateT (gameSession rand) (initialState gameLB gameUB)
          putStrLn $ "Steps used: " ++ show s

備註:上面的程式用 ghc --make Guess.hs 就可以編譯了(有用到 mtl package

2011年3月12日 星期六

libffi 的範例

在 C/C++ 當中,函式的型別必需在編譯期固定下來才可以正確的呼叫。所以 Function Pointer 都要記錄 Function 的 Signature,簡單地說下面二個 Function Pointer 是不同的型別:

void (*func0)();
void (*func1)(int);

然而在很多情況下,我們只能在執行期決定我們要呼叫的函數。最常見的例子就是有提供 Native Function Call 的 Interpreter。例如:Python (C API)、Java (JNI) 等等。然而這種功能如果只能使用 C 語言實作,會變得非常龐雜、沒效率而且失去一般性。所以有了 libffi 的產生。

libffi 的全名是 Foreign Function Interface,他本身是用組合語言撰寫,不過他提供了一個 Portable 的 API。我們只要呼叫這些 API 就可以進行動態的函式呼叫(這和 Function Pointer 不同。如前所述,Function Pointer 必需在 Compiler Type 決定函式的型別。而 libffi 可以在執行期決定 Callee 的型別)。許多有名的計劃都是 libffi 的使用者,例如:CPythonOpenJDKDalvik VM 等等。

以下是簡單的範例程式:

#include <stdio.h>
#include <stdlib.h>

/* libffi 的 header */
#include <ffi.h>

/* 一些亂七八糟的函式 */
int func0() {
  printf("%s()\n", __func__);
  return 0;
}

int func1(int p0) {
  printf("%s(%d)\n", __func__, p0);
  return p0;
}

int func2(int p0, int p1) {
  printf("%s(%d, %d)\n", __func__, p0, p1);
  return p0 + p1;
}

/* 函式查詢表。也可以使用 hash table、binary tree 來實作,
   不過作為一個簡單的範例直接使用 array 就好了。 */
void *func_table[] = {
  (void *)func0, (void *)func1, (void *)func2,
};

int main(int argc, char **argv) {
  int i;

  /* 決定我們要使用哪一個函式 */
  if (argc < 2) {
    fprintf(stderr, "USAGE: %s parameter-count\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  int parameter_count = atoi(argv[1]);

  if (parameter_count > 2) {
    fprintf(stderr, "WARNING: Parameter count is greater than 2.\n");
    parameter_count = 2;
  }

  /* 讀取函式的參數 */
  int values[2] = { 0 , 0 };

  for (i = 0; i < parameter_count; ++i) {
    if (scanf("%d", &values[i]) != 1) {
      static char const *const postfix[] = {
        "th", "st", "nd", "rd", "th",
        "th", "th", "th", "th", "th"
      };

      fprintf(stderr, "Unable to read %d%s parameter\n",
              i + 1, postfix[(i + 1) % 10]);
      exit(EXIT_FAILURE);
    }
  }

  /* 使用 libffi 呼叫指定的函式 */
  ffi_cif cif;
  ffi_type *args[] = { &ffi_type_sint, &ffi_type_sint };
  void *ptr_values[] = { &values[0], &values[1] };

  /* 準備 ffi 所需的資料結構 */
  if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, parameter_count,
                   &ffi_type_sint, args) == FFI_OK) {
    int sum = 0;

    /* 執行函式呼叫 */
    ffi_call(&cif, func_table[parameter_count], &sum, ptr_values);

    printf("return value: %d\n", sum);
  } else {
    fprintf(stderr, "Unable to initialize FFI structure.\n");
    exit(EXIT_FAILURE);
  }

  return EXIT_SUCCESS;
}

2011年2月12日 星期六

幫 C 語言加上 Garbage Collector

前幾天讀 Dragon Book 的時候突發奇想,何不自己實作 Conservative Garbage Collector?

一般來說 Tracing Garbage Collector 分成以下三個步驟:
  1. 先從 call stack 或者 global variables 的 references (或者 pointers) 蒐集 object 的位址作為 root set,並標記 root set 當中的 object。
  2. 檢視 root set 當中的 object。如果 object 也包含 reference,而且這個 reference 指向的 object' 沒有被標記過,就標記並檢視 object'。(不斷遞迴直到沒有新的 object 被標記)
  3. 清除並回收沒有被標記的 object。
然而為了有效率地計算 root set 與走訪被標記的 objects,通常我們會需要編譯器加入一些額外的資訊。而且程式語言必需是 type-safe 的!著名的例子如 Java 或者是 Haskell、Common Lisp 等等。而 C/C++ 就不適合當 Garbage Collector 的目標語言,因為 C/C++ 支援 Pointer Arithmetic、Union Type 等語言構件,使得我們幾乎不可能在 C/C++ 之上實作 (Accurate) Garbage Collector。

不過 Conservative Garbage Collector 的做法不太一樣。Conservative Garbage Collector 的想法是:我不去分辨誰是 Pointer Type 誰是 Integer Type,只要「看起來」像是 Pointer 的 bit pattern,我就把他當成 Pointer。這有一個好處:我們不需要編譯器的幫忙也可以寫 Garbage Collector,所以可以和其他的程式碼混用;然而也有一個壞處:有時候 Conservative Garbage Collector 過於保守,以至於無法回收一些可以回收的記憶體。以下我就以一個小程式展現 Conservative Garbage Collector 的基本概念。

首先我們的第一個問題是如何找到 root set?我們可以取 main 函式 argc 的位址,另外在 gc_cleanup_ 函式隨意傳入一個整數,再取該整數的位址。所有還可能會用到的 pointer 就會在這二個整數之間:

void *stack_top;

void gc_init(int *argc_ptr) {
  stack_top = argc_ptr;
}

void gc_cleanup_(int stack_indicator) {
  void *stack_bottom = &stack_indicator; 
  /* Stack 的有效範圍就介於 [stack_bottom, stack_top] 之間 */
}

int main(int argc, char **argv) {
  gc_init(&argc);
}

當然我們還必需考慮 alignment 的問題,所以要把 stack_top 與 stack_bottom 對齊到 sizeof(void *) 的某倍。這可以用以下二個巨集來達成:

#define FLOOR_ALIGN_TO_WORD(X) \
    (((uintptr_t)(X)) / sizeof(void *) * sizeof(void *))

#define CEIL_ALIGN_TO_WORD(X) \
    (((uintptr_t)(X) + sizeof(void *) - 1) / \
        sizeof(void *) * sizeof(void *))

接著我們要準備一些空間來當我們的 Heap,所以我們在 gc_init 之中加入一些程式碼:

static char *heap_begin = NULL;
static char *heap_free = NULL;
static char *heap_end = NULL;

/* Initialize the internal data structure of the
   garbage collector */
void gc_init(int *argc_ptr) {
    /* We assume that argc is the variable in the
       stack having the highest address. */
    stack_top = (void **)CEIL_ALIGN_TO_WORD(argc_ptr);

    /* Allocate the heap using mmap. */
    heap_begin = (char *)mmap(0, GC_HEAP_SIZE,
                              PROT_READ | PROT_WRITE,
                              MAP_PRIVATE | MAP_ANONYMOUS,
                              -1, 0);

    if (!heap_begin || heap_begin == MAP_FAILED) {
        fprintf(stderr,
                "Unable to allocate the heap (size=%lu).\n",
                (unsigned long)GC_HEAP_SIZE);
        exit(EXIT_FAILURE);
    }

    heap_free = heap_begin;
    heap_end = heap_begin + GC_HEAP_SIZE;
}

接著我們需要一些資料結構用以管理我們分配出去的記憶體。做為一個簡單的範例,我只用了一個陣列依序記錄分配出去的記憶體,我的陣列會從 heap_end 開始往 heap_begin 的方向生長。

當然這是一個很沒有效率的作法,比較有效率的方法是使用類似 buddy system 之類的資料結構來管理 heap。

enum {
    ALLOC_STATUS_UNKNOWN,
    ALLOC_STATUS_TOUCHED,
    ALLOC_STATUS_REFERRED,
    ALLOC_STATUS_DEALLOCATED,
};

typedef struct alloc_record_ {
    char *addr;
    size_t size;
    unsigned int status;
} alloc_record;

為了操作 alloc_record 資料結構,我寫了三個函式,其功能分述如下(程式碼就不再贅述):

static alloc_record *find_alloc_record(char *addr) {
  /* 給定一個位址,找出它是屬於哪一次 allocation */
}

static void insert_alloc_record(char *addr, size_t size) {
  /* 在 alloc_record 的尾端加上一筆記錄 */
}

static void *allocate_deallocated_block(size_t size) {
  /* 重新把 deallocated 過的位址配置給不同的人 */
}

接下來是 Malloc 的程式碼。我們先嘗試直接從 heap_free 配置記憶體,如果記憶體不夠,再嘗試使用別人 deallocate 的記憶體。如果以上二者都沒有用,我們就必需進行 Garbage Collection。(備註:gc_cleanup(); 即 gc_cleanup_(0);)

/* Allocate a block with given bytes. */
void *gc_malloc(size_t size) {
    /* Make sure that all of returned address is aligned */
    size = CEIL_ALIGN_TO_WORD(size);

    /* Try to allocate if free space available */
    if (is_free_space_avail(size)) {
        char *result = heap_free;
        heap_free += size;
        insert_alloc_record(result, size);
        return result;
    }

    /* Try to allocate from deallocated address. */
    void *result = allocate_deallocated_block(size);
    if (result) {
        return result;
    }

    /* Try to collect garbage and retry. */
    while (gc_cleanup()) {
        void *result = allocate_deallocated_block(size);
        if (result) {
            return result;
        }
    }

    return NULL;
}

當然有 Malloc 就要有 Free,不過值得注意的是:為了提高 Conservative Garbage Collector 的回收率,減少「應該 deallocate 而沒有 deallocate」的情況,我們應該要抹除整個 object 與傳入的 Pointer

/* Deallocate and wipe the allocated block. */
static void deallocate(alloc_record *record) {
    /* Mark this block as deallocated */
    record->status = ALLOC_STATUS_DEALLOCATED;

    /* Wipe the memory block for cleaning the possible
       pointer address */
    memset(record->addr, '\0', record->size);
}

/* Deallocate the given address and wipe the pointer. */
void gc_free_(void **addr_ptr) {
    alloc_record *record = find_alloc_record(*addr_ptr);
    if (!record) {
        fprintf(stderr, "Memory corrupted\n");
        return;
    }

    deallocate(record);
    *addr_ptr = NULL;
}

最後就是最關鍵的 Garbage Collection 的程式碼 gc_cleanup

/* Garbage collection and deallocate unused memory. */
size_t gc_cleanup_(int stack_indicator) {
    alloc_record *records =
        (alloc_record *)heap_end - alloc_record_count;

    size_t i, j;

    /* Mark alloc record as UNKNOWN */
    for (i = 0; i < alloc_record_count; ++i) {
        if (records[i].status != ALLOC_STATUS_DEALLOCATED) {
            records[i].status = ALLOC_STATUS_UNKNOWN;
        }
    }

    /* Scan the stack */
    void **stack_bottom =
        (void **)CEIL_ALIGN_TO_WORD(&stack_indicator);
    assert(stack_top != NULL);
    assert(stack_bottom <= stack_top);
    scan_and_touch(stack_bottom, (void **)stack_top);

    /* Scan the heap */
    scan_touched_objects();

    /* Deallocate and reset the status */
    static unsigned int gc_count = 0;
    static char const sep[] =
    "-------------------------------------------------------";

    fprintf(stderr, "%s\n", sep);
    fprintf(stderr,
            "GARBAGE COLLECTION ROUND #%u\n", ++gc_count);

    size_t deallocated_count = 0;
    for (j = alloc_record_count, i = j - 1; j > 0; --i, --j) {
        if (records[i].status == ALLOC_STATUS_UNKNOWN) {
            fprintf(stderr,
                    "  Reclaim [addr: %p, size: %lu]\n",
                    records[i].addr,
                    (unsigned long)records[i].size);

            deallocate(&records[i]);
            ++deallocated_count;
        }
    }

    fprintf(stderr, "%s\n\n", sep);

    return deallocated_count;
}

/* Scan for address between [begin, end) */
static void scan_and_touch(void *begin_, void *end_) {
    void **begin = (void **)FLOOR_ALIGN_TO_WORD(begin_);
    void **end = (void **)FLOOR_ALIGN_TO_WORD(end_);

    for (; begin < end; ++begin) {
        char *addr = (char *)*begin;

        if (addr >= heap_begin && addr < heap_free) {
            alloc_record *record = find_alloc_record(addr);

            if (record && record->status ==
                    ALLOC_STATUS_UNKNOWN) {
                record->status = ALLOC_STATUS_TOUCHED;
            }
        }
    }
}

/* Scan the touched objects */
static void scan_touched_objects() {
    alloc_record *records =
        (alloc_record *)heap_end - alloc_record_count;

    size_t count, i;

    do {
        count = 0;
        for (i = 0; i < alloc_record_count; ++i) {
            if (records[i].status != ALLOC_STATUS_TOUCHED) {
                /* Not in touched state, skip it. */
                continue;
            }

            /* Mark as referred. */
            records[i].status = ALLOC_STATUS_REFERRED;

            /* Scan this object */
            scan_and_touch(
                (void **)(records[i].addr),
                (void **)(records[i].addr + records[i].size));

            count++;
        }
    } while (count > 0);
}

以上程式碼就是一個具體而微的 Conservative Garbage Collector,完整的程式碼與測試程式可以從這裡下載:conservativegc.h , conservativegc.c , test_tree.c , test_many.c 。當然這個 Conservative Garbage Collector 還少了很多東西:包括計算 static storage 的 root set(在 Linux 之下可以檢查 etext 與 end 二者之間的值),還有更有效率的 allocation policy 等等。

備註:如果你真的有在 C/C++ 使用 Garbage Collector 的需求可以參考 libgc,一個由 BoehmDemersWeiser 等前輩撰寫 (他們是提出 Conservative Garbage Collector 的重要前輩),並被移植到多個平台的 Conservative Garbage Collector 函式庫。