前回の更新から1ヶ月経ってることに今気づいてびっくりしました
1ヶ月の間何もしてなかったわけじゃなく、実はもうScheme処理系も結構出来上がってるんですね
じゃあなんで1ヶ月更新してなかったんだよ!!っていうと、まあ単に忙しかったのもありますが、Scheme処理系がちゃんと完成させられるのか自信がなかったからです(言い訳
途中まで記事書いといて、この仕様じゃ詰むってことが発覚したりしたら怖いんでね、、、とりあえず末尾最適化は実装し終わり、call/ccもまあなんとかなりそうなんで今処理系に採用してる方針で記事にしていこうと思います
ちなみにですが、学校ではguileというScheme処理系が使われていますが、僕の作った処理系はguileの50倍遅いです!!!
まあわかりやすさ重視ということで目を瞑ってやっていきましょう…
ガベージコレクション
まずはガベージコレクションを考えていこうと思います
SchemeではC言語のように自分で明示的にmalloc/freeせずとも変数を用意できますが、この背景には自動でメモリ管理をしてくれるガベージコレクションというものがあるわけです
ガベージコレクションには様々なやり方があります
Copying方式
seesaawiki.jp
ざっくりいうと、メモリプールを2つ用意しておいて、ガベージコレクションするたびに使うメモリプールを切り替える方針です
強みとしては、GCするたびにオブジェクトが詰めて配置されるので、フラグメンテーションが発生しないという点ですかね
上のサイトに書いてある通り多くのScheme処理系に使われているらしいので最初はCopying GCを採用していましたが、アルゴリズムの性質上GCするたびにオブジェクトの位置が変わってしまうので、扱いには少し注意が必要になります
Mark & Sweep方式
seesaawiki.jp
とても直感的なアルゴリズムなので割と単純に実装できます
ルートオブジェクトから参照しているオブジェクトを辿っていってマークをつけていくマークフェイズと、マークのついていないオブジェクトの後処理を行うスイープフェイズの2つから成ります
今回処理系にはこちらを採用しました
理由としては、Copying方式と違いオブジェクトのアドレスは変わらないため途中でGCが行われても気にせず作業を続行できるので色々楽だからです
あと、本来Mark & Sweepはフラグメンテーションが弱点ですが、確保したいオブジェクトの型を統一してしまえばこの問題を回避できるからです
Obj型の配列にしてしまえばフラグメンテーションも何もないからですね
Reference Counter方式
seesaawiki.jp
(このサイトありがたすぎる)
参照カウンタ方式はスマートポインターなどで使われているので僕としては一番馴染み深いです
各オブジェクトに参照カウンタというものを付属させ、参照するたびに+1、参照をやめると-1していき、0になったら解放するという戦略です
僕のイメージですが、これをC言語で実装するのはC++と違ってクラスもないので結構めんどくさそう
あと循環参照といった、参照カウンタ特有の問題もついて回るのでなかなか難しい方式だと思います
コードの概略
今回はMark & Sweep GCを使い、処理系に登場するオブジェクトを全てObj型というものに統一することにしました、なかなか脳筋なやり方です*1
Obj型の構成は
typedef struct Object{ enum TYPE type; Data data; struct Object* child; struct Object* next; short gc_mark; }Obj;
こんな感じになっています、GCではchildとnextをどんどん辿っていく感じです
このObj型はScheme中の数字や文字列やシンボル、関数などありとあらゆるものを代入できるようになっているので、Mark & Sweep GCはObj型の配列を管理すればよく都合がいいわけです
まずMark & Sweep GCはObj型をたどるためのルートオブジェクトが必要なので、初期化すると共にルートオブジェクトをセットする関数を用意します
void initGC(int _size) { size = _size; pool = malloc(sizeof(Obj[size])); for (int i = 0; i < size; i++) { pool[i].type = UNDEFINED; pool[i].child = NULL; pool[i].next = NULL; pool[i].gc_mark = 0; } allocPtr = 0; }; void setRootObj(Obj* _root) { root = _root; }
size…メモリプールのサイズ
pool…メモリプール
allocPtr…メモリの現在地
root…ルートオブジェクト
です
オブジェクトを確保するたびにallocPtrを進めていって、allocPtrがメモリプールの端まで来たらGCを行います
Obj* newObj(enum TYPE type, Data data) { if (pool == NULL) return NULL; Obj* obj = NULL; int flag = 0; for (; allocPtr < size; allocPtr++) { if (pool[allocPtr].gc_mark == 0) { flag = 1; break; } } if (flag == 0) { runGC(); for (; allocPtr < size; allocPtr++) { if (pool[allocPtr].gc_mark == 0) { flag = 1; break; } } if (flag == 0) return NULL; } obj = pool + allocPtr; obj->type = type; obj->data = data; obj->child = NULL; obj->next = NULL; obj->gc_mark = 1; return obj; }
allocPtrを進めつつgc_markが0になっているところを探す、もし見つからなかった場合はrunGC()でGCを行い、再度探してみる
確保できる場所が見つかったらObj型の初期化をする
って流れです
GCを行う関数ではマークフェイズとスイープフェイズを行う必要があります
static void runGC() { for (int i = 0; i < size; i++) pool[i].gc_mark = 0; allocPtr = 0; if (root == NULL) return; // mark markObj(root->child); // sweep sweepObj(); }
これstaticがついてるのはrunGC関数はgc.cファイルでしか使わないからです
まんまコードの通りですが、一度gc_markを全て0にしたらmarkObj関数でルートから辿れるオブジェクトをマークしたのち、sweepObj関数でマークのついていないオブジェクトの後処理を行います
static void markObj(Obj* obj) { if (obj == NULL) return; if (obj->gc_mark) return; obj->gc_mark = 1; markObj(obj->child); markObj(obj->next); if (obj->type == ENV_MEMORY) markObj(obj->data.p_env); } static void sweepObj() { for (int i = 0; i < size; i++) { if (pool[i].gc_mark == 0) { if (pool[i].type == STRING || pool[i].type == SYMBOL || pool[i].type == PROCEDURE || pool[i].type == BUILT_IN) { if (pool[i].data.name != NULL) { free(pool[i].data.name); pool[i].data.name = NULL; } } pool[i].type = UNDEFINED; } } }
(ちょっと説明めんどい所があるのでそこは飛ばす(^^;)
markObjでは自分をまずマーク、次に自分の子(child)と隣(next)をmarkObjに渡すことで再帰的にやってます
sweepObjではメモリプールを端から見てってgc_markが0になっているとこの後処理をしてます
後処理ってなんだよ!!って感じですが、この処理系では文字列などもObj型に突っ込むためにObj型のdataという変数がなかなか無茶をしてくれています
Data型はunion(久しぶりに使った…)で、例えば文字列だったらdata.nameがchar*になっているわけです
で、data.nameにはmallocを使って文字列のメモリを確保しているので、後処理ではそれのfreeを行なっています
なんかGCなのに非効率ですが、Obj型で統一した代償ですね
だいたいMark & Sweep GCの主要な部分はこんな感じです
概略とか言いつつコードそのまま載せちゃいましたが、やってることはコードの通りですね
はじめに突貫で作った処理系はGCを搭載していませんでしたが、それだと流石にきつかったです
次は処理の流れとルートオブジェクト以下の説明をすると思います!