Skip to content

Add call-with-composable-continuation experimentally#1259

Closed
Hamayama wants to merge 6 commits into
shirok:masterfrom
Hamayama:partcont_exp_v4
Closed

Add call-with-composable-continuation experimentally#1259
Hamayama wants to merge 6 commits into
shirok:masterfrom
Hamayama:partcont_exp_v4

Conversation

@Hamayama
Copy link
Copy Markdown
Contributor

<<本件は、急いでマージする必用はありません。>>

  • call-with-composable-continuation と
    call-with-non-composable-continuation を追加しました。

  • resetChain をなくそうとしたのですが、私にはちょっと無理でした。
    partialChain とリネームして、今までと同様に使っています。
    promptTag の検索に対応しました。
    (ScmPromptData の情報を使おうとしたのですが、
    どうもいろいろなルートで部分継続が起動されるので、
    ScmPromptData の dynamicHandlers 等を使うと、
    めちゃくちゃな結果になるようでした。一部だけ切り取ったり
    つないだりしたら良いのかもしれませんが、ちょっとよく分かりませんでした)

  • Scm_VMReset() は、Scm_VMCallWithContinuationPrompt() に統合しました。
    また、Scm_VMCallPC() の後半部分は、Scm_VMAbortCurrentContinuation() に統合しました。
    このため、Scm_VMCallWithContinuationPrompt() と
    Scm_VMAbortCurrentContinuation() の処理は、元の処理より複雑になっています。
    ただ、これによって partcont.scm の reset/shift は、
    SRFI-226 の本文にある定義に近い形にできました。

  • Fix reset/shift and eval combination problem v2 #972
    Change vm error handling #1242 は、
    本 PR に統合しました。

  • 現状、冗長そうな関数があります。

    • copy_ccont() (save_cont_1() のコードのコピー)
    • apply_rec_with_tag() (apply_rec() のコードのコピー)
    • Scm_ApplyRecWithTag() (Scm_ApplyRec() のコードのコピー)
  • 現状、promptTag に未対応の関数があります。

    • with_error_handler()
    • Scm_VMCallCC()
  • new_ep() 内で save_cont() するようにしたので、少し遅くなっているかもしれない。
    (これをしないと、いろいろなところで対策が必要になりややこしかった)

  • 部分継続のループ使用でメモリ使用量が増えていく件ですが、
    どうも Scm_VMCallPC() の ep->cont で継続全体を捕まえているのが原因のようでした。
    Scm_VMCallPC() の ep->cont には、切り取った継続をセットするようにしたら、
    変な条件を入れなくても、メモリ使用量が一定になりました。
    ただ、そのために ep->cont を途中までコピーする関数 (copy_ccont_frames() と copy_ccont())
    を作ったので、パフォーマンスは悪くなったかもしれない。

<テスト結果>
(1) make check ==> OK

(2) Gauche-effects の effects.scm で、
*use-native-reset* を #t にして、各サンプルを実行 ==> OK

(3) 以下のメモリリークのテスト ==> OK
(出典 : http://okmij.org/ftp/continuations/against-callcc.html#memory-leak )
(これは (use gauche.partcont-meta) だとメモリリークします)

(use gauche.partcont)
(define (leak-test1 identity-thunk)
  (let loop ((id (lambda (x) x)))
    (loop (id (identity-thunk)))))
(leak-test1 (lambda () (reset (shift k k))))

(4) Kahua の nqueen を実行 ==> OK

(5) https://practical-scheme.net/wiliki/wiliki.cgi?Gauche%3ABugs#H-2dgngv
の pcdemo10.scm を実行 ==> OK

@Hamayama
Copy link
Copy Markdown
Contributor Author

ちょっと心配になったので、ENSURE_SAVE_CONT() を戻しました。
(これもよく分かっていないのですが…)

テスト結果は同じです。

@Hamayama
Copy link
Copy Markdown
Contributor Author

今の作りだと、複数の promptTag があった場合に、
うまく動かないケースがあるようです。

ひとつの原因は、部分継続の実行中に、
別な promptTag の区切りがあった場合、
無視して続行する必要があるのですが、
現状だと、BOUNDARY_FRAME があるため、
無条件に user_eval_inner を脱出してしまいます。
これは直すのが難しそうです。

@Hamayama
Copy link
Copy Markdown
Contributor Author

Hamayama commented Apr 25, 2026

  • reset-at と shift-at を追加し、
    複数の promptTag があるケースでも動作するように修正しました。

  • (ただ、全体的に複雑すぎて、マージは厳しいかなという気がしています・・・)

  • 今回やってみて分かったことですが、基本的な考えとして、
    「ep (escapePoint) をつかまえてそこに戻る」というのではなく、
    「ep には prompt までを切り取った継続を保存し、
     その継続の起動時には、現在の継続に切り取った継続を追加する」
    というような考えになるようです。

  • そして、フル継続と部分継続の違いは、継続の起動時の処理にあり、
    ・部分継続ならば、単に現在の継続に切り取った継続を追加する
    ・フル継続ならば、現在の継続から直近の prompt までを削除してから、
     切り取った継続を追加する。
     (なので一部の継続がなくなって goto 文っぽさが出てくる)
    ということになるようです。

  • ただ、これを素直に実装すると、
    継続の切り取りや継続の追加の際に、継続のリストを丸々コピーしなくてはならないので、
    遅くなるような気がします。
    (これは2個のリストをつなげるときに、
    片方のリストは丸々コピーしないといけないというのに似ています。
    コピーをさぼると共有している他のプログラムにひどい影響が出る・・・
    今回も修正した箇所でそれに苦労しました)

  • (例えば、chibi-scheme のリポジトリに「Provide delimited continuations」という
    プルリクエストがあるのですが、
    ctak が 4 倍遅くなったためマージされていないようです)

  • あとは、現状の Gauche の call/cc が
    「ep (escapePoint) をつかまえてそこに戻る」
    タイプになっていることが挙げられます。
    継続を追加するタイプの実装に変えた場合、
    どのくらい既存のプログラムが動かなくなるのか、
    また、共存させることは可能なのか、というあたりも気になります。

  • テスト結果は同じです。

@shirok
Copy link
Copy Markdown
Owner

shirok commented Apr 25, 2026

私の認識もだいたい同じです。escaoePointをキープしているのも、できるだけ継続フレームのコピーを避けたいという目論見があります。継続フレームをひとつのリンクトリストで管理することになると、部分継続を呼び出した時に再コピーが必要になります。そこで、継続フレームチェイン自体はprompt-tagで終端されるぶつ切りのリストで持っておいて、escapePointがそれらの継続の断片を「つなぐ」役割にできないかなと (ひとつの継続チェインが終端に達したら、現在のescaoePointから続きの継続チェインを取ってきて続行する、というイメージです。(Gasbichler & Sperber論文で"Meta continuation" と言ってるやつに相当)

@Hamayama
Copy link
Copy Markdown
Contributor Author

一応、その考え方でうまくいかなかったことを共有しておきます。

まず、

epcont = vmcont -> prev1 -> prev2 -> prompt -> prev3 -> prev4 ->

のように epcont = vmcont として継続フレームをそのまま保存した場合、
prev3 以後が GC されず、多量のメモリリークが発生しました。
(今まで if 文を入れたりして、特定のケースだけは対策していたのですが、
根本原因はこれでした・・・
(partcont-meta.scm がメモリリークするのも、これが原因と思う))

このため、

epcont = vmcont -> prev1 -> prev2 -> prompt -> NULL

のように切断したところ、メモリリークはなくなったのですが、
今度は他の epcont の保存分まで切断されてしまい。
大変分かりにくい不具合になった。

このため、

     vmcont  -> prev1  -> prev2  -> prompt  -> prev3 -> prev4 ->
epcont = vmcontCP -> prev1CP -> prev2CP -> promptCP -> NULL
のようにまるまるコピーする必要がありました。

また、継続の起動時についても、
最初は epcont の終端を書き換えて現在の継続の先頭につなぐようにしていたのですが、
これだと同じ継続を 2 回実行した場合に、
前回の方の継続のつなぎ先まで、今回の継続になってしまい、
これも大変分かりにくい不具合になった。
このため、継続の起動時についても、
epcont の内容をまるまるコピーしてから現在の継続につなぐようにしています。

結局、これらは、共有しているリストの途中を書き換えたリストがほしくなったときに、
その要素より前の部分をまるまるコピーしないといけないという、
一般的な話なのかと思います。

対策としては、(Copilot によると)
永続データ構造(persistent vector / HAMT / finger tree 等)
を使うという方法があるらしいですが・・・

@shirok
Copy link
Copy Markdown
Owner

shirok commented Apr 26, 2026

詳しく見てみないとわかりませんが、私のアイディアは「継続リストは書き換えない」です。
上の2段目のアイディアで、プロンプトの先は常にNULLにします。
で、escapePointのリストからなる「メタリスト」が各継続リストの断片を保持します。つまりリストが2階層になります。
「現在のメタリストの頭」と「サブリストの頭」を対で持っていれば、通常のリストとインタフェース的には等価にできます。
(partcont-meta.scmは継続リストの断片でなく全継続を保存しちゃうので、内部的にはちょいと違いますね。)

永続データ構造を使うというのは「リストを書き換えずに再利用したい」という時の一般的な方法ですが、上の2階層リストは今回の用途に特有のやり方になっています。

@Hamayama
Copy link
Copy Markdown
Contributor Author

なるほど、理屈は分かったような・・・
Racket の control.ss や srfi-226 の参照実装に、
metacontinuation-frame というのがありますが、似たような感じでしょうか。
ジャンプするときに実行される
 winders ( = dynamicHandlers ? )
 handler ( = abortHandler ? )
などもそこに格納されているようです。
(しかし、ここまで複雑なものを入れるほどの価値があるんかな。
理論の検証に使われるくらいであれば、
部分継続はコピーありで遅くても良いような気もしますが・・・
そもそもそんなに速度変わるのかな?)

@shirok
Copy link
Copy Markdown
Owner

shirok commented Apr 26, 2026

すごく長期的な希望としては、通常の継続も軽くしたいんですよね。可能な限りコピー自体を減らしたい。今もまだ残ってるGAUCHE_SPLIT_STACKフラグとかはその一部です。

今は継続も環境もキャプチャ時にヒープに移しますが、呼び出しのエクステントより長く生き延びないと無駄です。コンパイラが頑張ってエスケープ解析をしてアロケーション方法を変えるというのがひとつありますが、Gaucheではコンパイラはあまり重くしたくない。継続、環境のキャプチャ時にスタックのそこから下をポップしないヒープ第0世代として扱って、スタックが足りなくなったりグローバルGCが走るタイミングで必要な分だけコピーする、というのが基本的なアイディアです。が、第0世代ヒープを指すポインタをスタックGCのタイミングで書き換えないとならないのが大変。

@Hamayama
Copy link
Copy Markdown
Contributor Author

  • いろいろ整理したりデバッグして、
    動作はかなり Racket に近くなったと思います。

  • ep->contType を追加して、継続の種類を (ep->cstack == NULL ではなく)
    ep->contType で判別できるようにしました。

  • テスト結果は同じです。

  • call/cc は既存の動作のままです。ただし promptTag は指定できます。
    (call/cc (lambda (k) ... ) tag)

  • call-with-non-composable-continuation は、
    Racket の call/cc の動作になっています。
    (部分継続 + 起動時は直近のプロンプトまでの継続を削除してから追加)

@Hamayama
Copy link
Copy Markdown
Contributor Author

Hamayama commented May 1, 2026

少し見直したいので一度閉じます。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants