Ribbon: スクリプトの起動が遅いのをなんとかしたい
とりあえず適当なスクリプト(普通のSchemeインタプリタなら500msくらいで終わる)を起動すると:
FIXME: [0] yielded exact zero?
New addition?? [vmfetchcode]
(ARGS: ("-yuniroot" "/home/oku/repos/ribbon-integ/yuni" "-runtimeroot" "/home/oku/repos/ribbon-integ/yuniribbit-proto" "iter0.sps"))
(ARGS: ())
/* ★★ ここで時間掛かる1 ★★ */
(STARTING...: "iter0.sps")
(READBUF: 0)
(READDONE: 512)
(READBUF: 512)
(READDONE: 512)
/* ★ ここはネイティブ化したので一瞬 */
(READDONE: 11)
(READBUF: 0)
(READDONE: 11)
/* ★★ ここで時間掛かる2 ★★ */
(MAKESYM: check-finish #t)
(MAKESYM: check-equal #t)
(LOCAL: %%yunitest-mini-proc-fail! (yunitest mini))
(LOCAL: %%yunitest-mini-failed-forms (yunitest mini))
(RENAME: cons (yunitest mini))
/* ★ 以降もインタープリトなので一瞬 */
real 0m7.118s
user 0m7.031s
sys 0m0.061s
(ちなみにReaderを移行する前は12秒だったので3秒ほど短縮はしている。)
2箇所で時間が掛かっているように見える。どこかにO(N)が有りそうだからprintを足していって、どこに時間掛かってそうか特定しないといかんな。。特に時間が掛かっているのは2なので、そっちを先に見よう。。
ensure-library-loaded!
1 = 最初に時間が掛かっているポイントは、フロントエンドの初期化でライブラリをロードしているところだった。
これはフロントエンド側の ensure-library-loaded!
に至る。
これは普通のSchemeコードだからインタプリタ側を最適化するしかないな。。Schemeの大半をSchemeで実装している都合上、標準ライブラリである (yuni scheme)
をimportするだけで大量のライブラリをロードしてしまう。
2 = ライブラリロード
いやこれどうしようも無いな。。全てのプログラムが依存するライブラリ (yuni scheme)
を特別扱いしてライブラリの絶対数を減らすのが唯一の策なんじゃないだろうか。
Cセーフシンボルへの変換を止めてみる
Ribbonでは将来的にCソースコードを出力することを見越して、ライブラリは内部でCシンボルとしても正当なシンボル名で処理するようにしている。
これをno-opにしてみた。 (define (csafe str) str)
。
real 0m7.103s
user 0m7.015s
sys 0m0.061s
↓
real 0m4.078s
user 0m4.000s
sys 0m0.046s
減り方がえぐいな。。これは事前計算できるんだし事前計算した方が良いね。。リリースビルドなら2秒切るしこれで良い気もしてきた。
でも (yuni scheme)
のバンドル化をすれば、Cシンボルの導出回数自体も減るので効果という意味では何ともだな。
ライブラリのバンドル、マージ
開発中は複数に分かれているライブラリを1つに纏めてしまうこと自体は理に叶っている。実際npmで提供されるようなJavaScriptのライブラリはライブラリであってもbundlerで纏めてしまうことが一般的になされている。
というわけで、 (yuni scheme)
を構成するライブラリは 個別には import できない ルールにしてみるのが良いのではないか。。従来は、 (yuni scheme)
はyuniのライブラリの一部である (yuni miniread *)
に依存していた。
サブライブラリ化
ただし、全てのライブラリを単純にマージしてしまうと、 VMのプリミティブに直接アクセスしたいようなケースで困ってしまう。 Ribbonの場合は (rvm-primitives)
ライブラリで記述されているプリミティブはスクリプト中から直接呼出せるようにしている。
スクリプトから (import (rvm-primitives) ...)
できる状況はキープしなければならない。サブライブラリを宣言できるようにし、サブライブラリのexportは保存する。つまり、ライブラリ (yuni scheme)
のバンドルには2つのSchemeライブラリ (yuni scheme)
と (rvm-primitives)
が含まれているように見せる必要がある。( (yuni scheme)
は (rvm-primitives)
に依存しているため、 (yuni scheme)
のimport時に纏めてimportできるように合成してしまうのが合理的と言える。)
ライブラリバンドルへの変更
ライブラリバンドルは以下の vector
のリストとなっている:
idx | sym | 内容 |
---|---|---|
0 | libname | ライブラリ名(リスト) |
1 | libsym | ライブラリシンボル |
2 | import* | インポート対象のライブラリシンボルリスト |
3 | imports | インポート対象のライブラリ(リスト) |
4 | exports | エクスポートしているシンボルのリスト |
5 | prog | ★ リストにする: 実行コードのVM命令列 |
6 | macnames* | マクロ名のリスト |
7 | maccode | ★ リストにする: マクロ変換コードのVM命令列 |
8 | aliassym | ★ 追加: エイリアス先のライブラリシンボル または #f |
aliassymが偽の場合のインポート処理は:
- importsに含まれるライブラリを全てロードする
- prog および maccodeに含まれるVM命令列を全て実行する (= exportsに設定されているシンボルがVM内に定義される)
- 名前空間にexportsやmacnamesを追加する
という処理になる。aliassymがシンボルである場合は、シンボルが指すライブラリを代わりにロード(stage)し、その後名前空間の処理(import)を行う。
stage / importの分離
ライブラリのVM命令列を実行済かどうかを示す stage と、プログラムの名前空間に追加済かどうかを示す import にライブラリのロードを分割する。
超早くなるけどimplicit exportをどうすんのか問題
これかなり高速化する。。もうこれで普段使いできそうだな。
real 0m1.676s
user 0m1.609s
sys 0m0.046s
ただ、単純にライブラリをマージするだけだと、implicitなexport -- マクロ内でinjectされる暗黙の参照を処理できないことがわかった。とりあえず面倒だけど手動でそういうシンボルを持っているライブラリを追加することで対策している。
今回mergeするのは (yuni scheme)
だからSchemeの標準ライブラリで使われるものだけ指定すれば良いけど、一般的なライブラリをmergeしないといけない場合は困るな。。
とりあえず実装
地味に unquote
が壊れていたのを直した。。
とりあえずyuniのテストは普通の処理系と遜色ない速度で実行できるようになったので、そろそろyunibaseに登録しても良いかもしれない。