Ypsilon vm boot 時のコード埋め込み

Scheme で書かれたコンパイル済みのコンパイラC++ に埋め込む形で Mosh は書かれている。しかしながら2つほど問題がある。

  • g++ でビルドするときに遅い(環境によってはコンパイルが終わらない)
  • コードを取得するときに重い
    • 静的に決まらない部分の初期化コードなどが遅いと思われる。

の2つ。


以前 shiro さんからアドバイスいただいた通り pair を静的に割り当てればこれらの問題は改善できそうだが、ちょっと面倒かもしれない。そこで ypsilon のコードを読んでこのあたりをどう解決しているか見てみる。
src/vm0.cpp にそのあたりの処理がある。

#include "../heap/bootimage.code"
#include "../heap/coreimage.code"

これらのファイルを見てみると完全に静的に埋め込まれたバイナリである。

static const uint8_t s_bootimage[211187] = {
0xa,0x23,0x21,0x66,0x61,0x73,0x6c,0x30,0xa,0xc5,0xf,0x80,0x8a,0x70,0x75,0x73,

実際に s_bootimage が使われるのは VM::boot() の中。

scm_bvector_t bv = make_bvector_mapping(m_heap, (void*)s_bootimage, sizeof(s_bootimage));
m_bootport = make_bytevector_port(m_heap, make_symbol(m_heap, "bootimage"), SCM_PORT_DIRECTION_IN, bv, scm_true);
scoped_lock lock_port(m_bootport->lock);
while (true) {
    reset();
    scm_obj_t obj = reader_t(this, m_bootport).read(NULL);
    if (obj == scm_eof) break;
    m_pc = obj;
    prebind(m_pc);
    run(false);
}

ふむ。バイナリを bytevector にマッピングして、そこから read したオブジェクトを run で eval しているように見える。
ということはこのバイト列は、テキスト情報をそのままバイナリに埋め込んだものではないか?と推測できる。
これを裏付けるように、デバッグ時には .vmi 形式のファイルを読み込み read しているのだが

  #if USE_DEBUG_BOOT
        {
            char load_path[] = "heap/debug-boot.vmi";
            m_bootport = make_file_port(m_heap, make_string_literal(m_heap, load_path), SCM_PORT_DIRECTION_IN, 0, SCM_PORT_BUFFER_MODE_BLOCK, scm_true);
            printf(";; loading \"%s\"\n", load_path);
            fflush(stdout);
            scoped_lock lock_port(m_bootport->lock);
            while (true) {
                reset();
                scm_obj_t obj = reader_t(this, m_bootport).read(NULL);
                if (obj == scm_eof) break;

vmi ファイルは

((push.const . \x2E;list?)
 (push.gloc.of list?)
 (subr.gloc.of set-top-level-value! 2 "./boot/first-load.scm" . 7171)

と完全に S 式で書かれている。


ここまで仕様が理解できたと思ったが、よく見ると debug-boot.vmi と s_bootimage は内容が違う。
s_bootimage は bootimage.vmi と同じ内容。 bootimage.vmi は

#!fasl0
....;push.const

インストラクション列の表現方法が違う。エンコードされているっぽい。fasl.cpp/fasl.h を見たが、どうやら正解の模様。主にサイズがコンパクトになる利点があるかな。
ということでまとめると Ypsilon はコンパイル済みコードを S 式で表現しており、それをエンコードしてヘッダファイルとして静的に埋め込む。 VM の boot 時にその領域を参照しデコードして、その場で eval する。

比較

g++ でのコンパイル速度は Ypsilon 方式は静的なので圧倒的に高速。
read のコストは

  • pair などの割当コストは同じと考える
  • Mosh では
    • list->vector が余計にかかる
  • Ypsilon では
    • デコードのコストがかかる。(tagベースの分岐だから parse ほどは時間はかからない)

実際に core/boot のロード速度(evalも含む)を計測したら 40msec くらいだった。一方 Mosh は load だけで (eval 除く) 70msecほど。
ただ boot に必要なコードのサイズがそもそも違うのかもしれないのでなんとも。
g++ コンパイル速度以外ははそこまで変わらない。ということかな。

FASL?

FASt Loading の略らしいよく使われる方法っぽい.