Kashiwa Schemeにライトバリアが付きました

前回のエントリの最後に

面白いのが、スタックからヒープへのコピーという処理は、
スタックをyoung領域、ヒープをold領域とした世代別GCとも見なせる点です。
……と、ここまで書いた時点で、ヒープからスタックへの参照を、
remember setに入れて覚えておく必要がありそうな気がしてきたけど、
そんな処理は書いてないぞ。なんてこった。ライトバリアとか書きたくないぞ。

などと言いましたが、結局ライトバリアを書きました。

そもそも何が問題だったかを簡単に書きます。

(define (g x)
  (lambda (z)
    (set! x (cons z x))
    x))
(define ls (g 1))
(ls 2)
(ls 3)
...

Kashiwa Schemeは基本的にすべてのオブジェクトをスタックに割り当てます。
(ただしシンボルは最初からヒープに割り当てます。)
そのため、上記プログラムでは、(g 1)によって作られるクロージャは、
スタックに割り当てられ、そのアドレスがグローバル変数lsに代入されます。

しばらくしてスタックに対するごみ集めが起こると、先ほどのクロージャはヒープに移されます。
その後に(ls 3)などの評価を行うと、上記のクロージャが呼び出され、
クロージャの保持する変数xに『新しいコンス』が代入されます。
新しいコンスはもちろんスタックに割り当てられるわけです。

このようにして、ヒープからスタックへの参照が生まれます。
この状態でスタックに対するごみ集めが起こると、
上記のクロージャはルートセットに含まれないため、
コンスがごみとして回収されてしまいます。
これはどう考えてもまずいわけです。

で、解決策はいくつかありまして、一番簡単なのは、
スタックに対するごみ集めのたびにヒープ全体を舐めることです。
これは確実な上、簡単に書くことができますが、
ごみ集めにかかる時間が明らかに増大します。

別の解決策として、ヒープからスタックへの参照を記憶する方法があります。
ヒープからスタックへの参照を覚えておき、それをルートセットに加えることで、
回収漏れを防ぐというやり方です。これだとごみ集めの時間はあまり増えません。

ヒープからスタックへの参照の集合をremember setと呼ぶことにします。
ごみ集めを正しく行うためにはremember setを正しく作る必要があります。
ヒープからスタックへの参照がいつ生まれるかというと、set!やset-car!などの、
破壊的代入が行われる時のみです。
それらの場所で、必要があればremember setに参照を加えればいいわけです。
書き込みを行う場所で見張るため、この処理をライトバリアと呼びます。

remember setにも作り方が色々あります。
一つは単純にアドレスを1つずつ覚える方法。
この方法だと、ヒープからスタックへの参照の数だけメモリが必要です。
例えば、対象となる参照が1M個ある場合、remember setは4MB必要です。
(アドレスは32bitとして計算してます)

もう一つの方法は「おおよその場所」を覚える方法です。
ヒープを例えば512bytesごとのブロックに区切り、
ブロックの中にヒープからスタックへの参照が(1個以上)あるかを記憶します。
remember setのサイズは参照の数ではなくヒープサイズに比例します。
参照の有無は1bitあれば表せるので、ヒープが1GBあっても、
remember setはたったの256KBで十分なわけです。
(K=1024, M=1024K, G=1024Mで計算しています)

さてさて、どちらの方針が有利でしょうか。
対象となる参照の数が非常に多い場合は、おおよその場所を覚える方がいいでしょう。
しかし、おおよその場所しか覚えない場合は、
スタックに対するごみ集めの時に「実際の場所」を探すために
余計なスキャンを行う必要があります。
対象となる参照の数が少ない場合はアドレスを1つずつ覚える方がいいでしょう。

さて、Schemeといえば、破壊的代入を嫌う文化があります。
そのため、対象となる参照の数は十分に少ないと仮定していいでしょう。多分。
そんなわけでアドレスを1つずつ覚える方針を取ることにしました。
これを書きたかっただけなのに説明がえらく長くなってしまった。

Leave a Reply