ガベコレも、なんとなくやってることはわかるんだけど、実際のところよくわからない…なので実装して確かめる。
手始めにマーク&スイープ法。
GCを自分で実装、ということでメモリ管理も自分で好きに扱えるように、自分で作る。
というかK&Rのmallocルーチンの管理構造体を外から渡すようにしたもの:
balloc.c #include "balloc.h" typedef union balloc_header Header ;void balloc_init (BAlloc* balc, void * buf, unsigned size) { Header* up; balc->base.s.ptr = balc->freep = &balc->base; balc->base.s.size = 0 ; balc->used = balc->marked = NULL ; up = (Header*)buf; up->s.size = size / sizeof (Header); bfree(balc, up + 1 ); } void *balloc (BAlloc* balc, unsigned nbytes) { Header *p, *prevp; Header *root; unsigned nunits; nunits = (nbytes + sizeof (Header) - 1 ) / sizeof (Header) + 1 ; prevp = root = balc->freep; for (p = prevp->s.ptr; ; prevp = p, p = p->s.ptr) { if (p->s.size >= nunits) { Header *q; if (p->s.size == nunits) { prevp->s.ptr = p->s.ptr; q = p; } else { p->s.size -= nunits; q = p + p->s.size; q->s.size = nunits; } balc->freep = prevp; q->s.ptr = balc->used; balc->used = q; return (void *)(q + 1 ); } if (p == root) return NULL ; } } void bfree (BAlloc* balc, void * ap) { if (ap != NULL ) { Header *bp, *p; bp = (Header*)ap - 1 ; for (p = balc->freep; !(p < bp && bp < p->s.ptr); p = p->s.ptr) { if (p >= p->s.ptr && (bp > p || bp < p->s.ptr)) break ; } if (bp + bp->s.size == p->s.ptr) { bp->s.size += p->s.ptr->s.size; bp->s.ptr = p->s.ptr->s.ptr; } else { bp->s.ptr = p->s.ptr; } if (p + p->s.size == bp) { p->s.size += bp->s.size; p->s.ptr = bp->s.ptr; } else { p->s.ptr = bp; } balc->freep = p; } }
未使用のブロックがリングにつながってる
確保されたメモリはリストにつなぐ
ひとまず、要求されたメモリが確保できなくても gc は起動せず NULL を返すだけ
balloc.h #pragma once typedef long BAllocAlign;union balloc_header { struct { union balloc_header * ptr ; unsigned size; } s; BAllocAlign _align; }; typedef struct { union balloc_header base ; union balloc_header * freep ; union balloc_header * used ; union balloc_header * marked ; } BAlloc; void balloc_init (BAlloc* balc, void * buf, unsigned size) ;void *balloc (BAlloc* balc, unsigned nbytes) ;void bfree (BAlloc* balc, void * ap) ;
テストとして確保する構造体とワーク:
struct Test { int no; }; const int NBUF = 4 ;Test* test[NBUF];
ヒープの初期化:
const int HEAPSIZ = 1 * 1024 ;char heap[HEAPSIZ];int main (int argc, char * argv[]) { BAlloc allocator, *balc = &allocator; balloc_init(balc, heap, sizeof (heap));
確保して、次々に捨てるコード。最終的にバッファに残ったものだけが生存するワークとなる:
const int N = 32 ; for (int i=0 ; i<N; ++i) { int r = rand() * NBUF / (RAND_MAX + 1 ); Test* p = (Test*)balloc(balc, sizeof (Test)); p->no = i; test[r] = p; } dump_meminfo(balc); ... void dump_meminfo (BAlloc* balc) { int freesize, nalloc; nalloc = balloc_get_info(balc, &freesize); printf ("mem: free=%d, #%d\n" , freesize, nalloc); for (int i=0 ; i<NBUF; ++i) { Test* p = test[i]; if (p != NULL ) { printf ("%p: %d\n" , p, p->no); } } } int balloc_get_info (BAlloc* balc, int * pfreesize) { int size = 0 ; int nalloc = 0 ; { Header* p; for (p = &balc->base; p = p->s.ptr, p != &balc->base; ) { size += p->s.size; } *pfreesize = size * sizeof (Header); } { Header* p; for (p = balc->used; p != NULL ; p = p->s.ptr) { ++nalloc; } } return nalloc; }
GCのルートから辿れるものを全てマークして、マークされなかったものをスイープ:
mark_all(balc); sweep(balc); dump_meminfo(balc); return 0 ; }
今回のものは、内部にポインタを持たないので、ルートのみをマーク
void mark_all (BAlloc* balc) { for (int i=0 ; i<NBUF; ++i) { Test* p = test[i]; mark(balc, p); } }
ひとつのワークをマーク。ワークがヒープから取られたものかどうか確認する。そうだったらマーク済みに移す。マークしたかどうかはブロックのサイズの最上位ビットを使う:
#define MARK_FLAG (1<<(sizeof(unsigned) * 8-1)) #define MARK(ptr) ((ptr)->s.size |= MARK_FLAG) #define UNMARK(ptr) ((ptr)->s.size &= ~MARK_FLAG) int mark (BAlloc* balc, void * work) { Header* h = ((Header*)work) - 1 ; Header* pre = NULL ; Header* p; for (p = balc->used; p != NULL ; pre = p, p = p->s.ptr) { if (p == h) { MARK(p); if (pre != NULL ) { pre->s.ptr = p->s.ptr; } else { balc->used = p->s.ptr; } p->s.ptr = balc->marked; balc->marked = p; return TRUE; } } return FALSE; }
回収は、使用済みに残ったもの(=マークされてない)を全て解放。マーク済みのものをマークをはずして使用中に戻す:
void sweep (BAlloc* balc) { Header* p; for (p = balc->used; p != NULL ; ) { Header* q = p->s.ptr; void * work = p + 1 ; bfree(balc, work); p = q; } balc->used = balc->marked; balc->marked = NULL ; for (p = balc->used; p != NULL ; p = p->s.ptr) { UNMARK(p); } }
実行結果:
mem: free=1024, #0 mem: free=512, #32 004286F8: 30 00428748: 25 004286E8: 31 00428798: 20 mem: free=960, #4 004286F8: 30 00428748: 25 004286E8: 31 00428798: 20
なんかすっきりしない…
使用中のワークをマークするためには、アプリ側でどれが gc のルートとなるかをすべて列挙してやらなきゃいけない
確保されてるワークの、どのメンバがポインタかを知ってなきゃいけない
指してるポインタが、GC用のバッファから確保したものかどうかを確認するとすごく時間がかかりそう(O(n^2)?)
ヒープの範囲かどうか調べればマシになるけど、そうするとブロックの先頭以外を指してるポインタがあると誤動作する
確保したメモリの途中を指してると回収されてしまう
malloc ルーチン自体も自作してやらんと非効率的になりそうな
使用中のヒープ全てのポインタを知っている
マークビットをどこかにセットできる
どこかに猫でもわかる簡単なGC実装のサンプルはないでしょうか。