Gauche:boxに関する覚書

Gauche:boxに関する覚書

Shiro(2014/01/28 22:38:59 UTC): SRFI:111になったboxの実装について。

Gauche内部では既にboxオブジェクトを使っている。set!されているローカル変数はコンパイル時にboxに置き換えられている。概念的には、こういうコードが:

(let ([x val0])
  ...
  (set! x val1)
  ... 
  x
  ...)

こんなふうになってる:

(let ([x (box val0)])
  ...
  (set-box! x val1)
  ...
  (unbox x)
  ...)

この置換を行うと、環境フレーム自体はimmutableになるため、何かと都合が良いのだ。

ところがこいつを実装した時にちょっとサボって、xの初期化時こそbox操作を挿入するものの、 set!と参照については出力インストラクションを変えるのではなく、LSETやLREFインストラクション 自体に「対象がboxだったらそれぞれset-box!およびunbox操作を行う」という 機能を入れてしまった。コンパイラを変えるより手っ取り早かったから。

さて今になって、 box srfiをサポートするには、boxオブジェクトのAPIをSchemeから見えるようにするだけで いいや、と思ってやってみたんだが、このLSETやLREFによる暗黙のboxの扱いが干渉してうまく いかない。boxオブジェクトそのものが欲しいのに勝手に中身が取り出されたりしてしまう。

しまったなあ。

正しい方法は、set!されてるlvarに関してはLREFの後にUNBOXインストラクションを挿入することと、 LSETの操作を事実上set-box!にすること (LSETの対象となるlvarは全てbox化されてるはずだから、 set-box!として扱って良い)、なんだけど、いくつか障害に当たってしまった。

LSETの先がboxでないケースが残ってた

letrecで束縛される変数の値はクロージャであることが非常に多いので、 (それぞれ個別にCLOSUREインストラクションを発行して作るかわりに)まとめてクロージャを 作成するLOCAL-ENV-CLOSURESインストラクションが発行される。

値がクロージャでない変数についてはLOCAL-ENV-CLOSURESでは#<undef>にしておいて、 本体内で値を計算してset!するのだけれど、このset!は「一回限り」行われる操作のため mutableな扱いになっていなかった。

あと、これは見落としというかバグで、仮引数にset!してる場合にそれをbox化するのを忘れてた。

まあ後者は直すとして、LOCAL-ENV-CLOSURESについては一度set!したら後は immutableなわけで、box化するのも無駄なように思える。

「LOCAL-ENV-CLOSURESと対で使う、環境スロットの初期化のためのインストラクション」 を新設すべきかも。

以前のGaucheでプリコンパイルされたコードとの互換性

暗黙のboxを入れたのは0.9.2くらいだったと思うんだけど、それから 今までのGaucheでプリコンパイルされたコードは、 「束縛時にBOXインストラクションが入っているけど参照時はLREFがunboxしてくれることを期待」 となっている。

LREFの暗黙unboxを落としてUNBOXインストラクションを入れる、とした場合、 以前のプリコンパイル済みバイナリでは値を期待してるコードで#<box>オブジェクトが 見えることになっちゃう。

LREFには触らずにおいて、暗黙のunboxを行わないLREFインストラクションを 新設すれば互換性は保たれるけれど、LREF系は複合インストラクションがたくさんあるので 冗長なインストラクションが大量に増えてしまう。

プリコンパイルはまだ公式な機能ではないので、「precomp使ってた人は 再コンパイルしてね」でもいいかな。genstubの方は影響ないし。

実装メモ

現在のBOXインストラクションはVAL0の値をboxして、結果をVAL0に残す。通常のローカル変数では 値をVAL0に置いてからPUSHするので、BOXをPUSHの前に挿入するだけで良い。

仮引数にset!しているケースの場合は、既に環境フレーム中に値があって、それをBOXで 置き換えないとならない。一度VAL0に持ってきてBOXして戻す、というのも無駄な感じなので、 指定の環境フレームスロットを直接BOXで置き換えるインストラクションがあった方が良さげ。

とはいえそのためだけにインストラクションを増やすのも何だし、BOXインストラクションの パラメータは今使ってないからここにフレームスロット番号を入れてやればいいかな。

と思ってやってみたんだが、ビルド時に問題が。本体のビルドはこういう手順なわけだが:

  1. インストール済みコンパイラでターゲットのソースをコンパイルして、インストラクションのリストを得る
  2. その結果をターゲットのvminsn定義に従ってバイナリに変換

インストール済みコンパイラはBOXをパラメータ無しのインストラクションとして生成するんだけど、 2.でターゲットバイナリを生成する時にターゲットのvminsn仕様はBOXにパラメータを要求するんで エラーになる。

つまりインストラクションパラメータを変えるような変更は簡単にできないということか。

今回だけ別インストラクションを設けるなどして回避してもいいんだが、 将来また引っかかりそうなので、バイナリ生成ルーチンの方を変えて 「インストラクション仕様に変更」に柔軟に対応できるようにしたほうがいいな。

仕切り直し

いよいよLREFの仕様を直してみた (暗黙のunboxを行わず、コンパイラが別途出すUNBOXインストラクションでunboxを行う)。だが動かない。

考えてみたら、ターゲットGaucheをコンパイルするビルドGaucheは0.9.3.3で、 UNBOXインストラクションを出さず、LREFが暗黙のunboxをすることを当てにしている。 だからターゲットGaucheのランタイムでLREFが暗黙のunboxを行わないと、Gauche本体の コードの実行がおかしなことになるのだ。

間に「UNBOXインストラクションを出すがそれはnopで、LREFで暗黙のunboxを行う」という 中間バージョンを挟めば移行できる。

しかしビルド過程を変えるとなるとそれはそれで一仕事だなあ。もっとも一度変えてしまえば こういう複数ステージビルドには利点があって、新たに入れた最適化をGauche本体にも 適用できる。

0.9.4を中間バージョンとして、0.9.5で完全移行、というのが順当だが、その場合 boxをSchemeに見せるのは0.9.5までお預けになる。それと、「UNBOXが正しく出ていること」 を確認するには0.9.4コンパイラで0.9.5候補をビルドしないとわからないので、検証が面倒。 不完全なまま0.9.4を出しちゃうと、それを直すためにさらに中間バージョンが必要になるし。

もう一つの選択肢は、0.9.4に限り「暗黙のunboxを行う特別なLREF系インストラクション」を 新設し、0.9.3.3が出すLREFをそれにマップする。0.9.4コンパイラはこの特別なインストラクションを 出さないので、この特別なインストラクションは「0.9.3.3でコンパイルした0.9.4Gauche本体」 のみに含まれる。0.9.4コンパイラは「暗黙のunboxを行わないLREF」とUNBOXインストラクションを 出すので、0.9.4のテストスイートで「UNBOXが正しく出ていること」の検証も可能。 デメリットは、「特別なLREF系インストラクション」が大量にあることで、 一時的とはいえ気持ち悪い。

さてどうしようかな。

(2014/01/31 05:40:57 UTC): 結局、ソースツリーが最終的に綺麗になることを重視して、 「0.9.4ではUNBOXを出すがnopで暗黙のunboxをキープ / 0.9.5で完全切り替え」という 方針を採用。テストに関してはスクリプト書けばいいだけだし。

0.9.4時点のまとめ

0.9.4出したので、結局どうなったかをまとめておく。(2014/08/30 17:30:07 UTC)

More ...