Ribbon: クロージャの循環参照をどうにかしたい

リファレンスカウントをちゃんとやってるのにメモリリークする理由は、クロージャの循環参照だった。これはSwiftのような他の言語でも問題になる:
一旦何らかのヒューリスティックを導入して緩和できないか検討する。

最小コードを作る
- リークする
(define (apply-dummy0 f)
(letrec ((x (lambda ()
(none 0))))
(x)))
- リークしない
(define (apply-dummy0 f)
(let ((x (lambda () ;; ★ letrecをletにするとリークしなくなる
(none 0))))
(x)))
実際には letrec
はマクロとして実装されていて、
(define (apply-dummy0 f)
(let ((x #f))
(set! x (lambda () (none 0)))
(x)))
と等価であり、この let
と set!
の組合せでもリークは発生する。

Visual Studioを使ったメモリリークの追跡
今回最小コードを作るにはVisual Studio 2022のメモリデバッガを使用した。
スナップショット間の比較機能を使用すると、
ExClose
(クロージャを作成するnative側関数) で作成されたオブジェクトがリークしていることが観察できる。

良いアイデアが出ない
まぁちゃんとしたGCに頼れば当然解決するわけだけど、さすがに循環参照がそこまで出ることは想定外なのでなんとかしたい。現状だとただのループで絶対に循環参照が出ることになる。
今回のケースに限って言えば、Swiftの例のように弱参照にしてしまうことで解決することは可能だが、これを一般に適用できるかは何とも言えない。プログラマーが明示的に弱参照になっていることを意識しなければならないので、暗黙にやってしまうのは安全でない(はず)。ただ、実は今回のような named-let ケースは絶対に安全なんではないかという気がしてならない。。
定数伝搬を実装して、 set!
自体を消してしまうという手は考えられる。こうすれば参照自体が作られなくなるので循環参照の問題は解決する。(実際には、コードシーケンスの内部で循環参照が発生するが、コードシーケンスは通常解放されないので問題にならない) コンパイラで (set! ... (lambda ...))
と (set! ... (quote ...))
のパターンを定数の set!
とみなし、これらは定数の伝播と見做してコード上で事前に置き換えてしまえば元々の set!
は削除できる。
Clojureの loop
〜 recur
https://clojure.org/reference/special_forms#recur のようなプリミティブをサポートしてクロージャ自体を作らないようにするということも考えられる。 ...が、流石に言語にまで手を入れてしまうのは考えものと言える。。
... いったん普通にGCを実装して、その後定数伝搬によって letrec
の set!
を消し込むのが良いかな。普通の循環参照ケースを救うにはGCしか無いので、どっちみちGCは必要だし。