RHGの逆襲 第10回

ラクリナックス社にて。いつも会場の提供ありがとうございます。

第10回のテーマは「コンパイルと最適化」。Ruby 1.9 ではスクリプトはパースされて一旦構文木(AST)を経由して VM の命令列にコンパイルされます。なのでここで言うコンパイル構文木のノードを辿って命令列に変換することをいいます。

それから命令列はよくバイトコードと呼ばれますが、Rubyの命令語は VALUE 列だからとか、ささださんがなにか抵抗があるとかいう事情により iseq (多分 instruction sequence)と言うようです。

ではいつものように箇条書きで。

  • まずは型。
    • 命令語自体は VALUE
    • 命令列は rb_iseq_t
      • 命令列はメソッド、クラス/モジュール定義、ブロックなどスコープごとと、rescue節など毎に作られる。
      • なので引数の情報が含まれる。ちなみに 1.9 では rest 引数が拡張されている。
      • GC からの保護用の mark_ary
      • self は、iseq 自体が Ruby のオブジェクトとして wrap されているので、その VALUE を持っている。
      • compile_data->compile_data_storage はコンパイル時に使用するメモリチャンクマネージャみたいなやつ。
  • compile.c が今回の本丸
  • コンパイル時に一次利用される型、マクロ等
    • LINK_ELEMENT(struct iseq_link_element)
      • コンパイルの途中では一旦 linked list のデータ構造で若干抽象的な命令列が作成されます。その要素の基底型です。実際にはこれを継承した LABEL(struct iseq_label_data) や INSN(struct iseq_insn_data)が繋がれます。LABEL はマーカとして一旦挿入されるだけでコンパイル結果には残りません。
    • LINK_ANCHOR(struct iseq_link_anchor)
      • linked list 毎にダミーのheadとtailポインタをまとめたもの。
    • DECL_ANCHOR と INIT_ANCHOR
      • LINK_ANCHOR を定義して、空のリストを表現するように初期化するマクロ
    • ADD_TRACE, ADD_LABEL, ADD_INSN, ADD_INSN1...
      • LINK_ANCHOR に要素を追加するマクロ。ADD_INSN1 とかは命令語のオペランド数毎にマクロがある。
  • gdb でコールツリーを辿りながら実演。以下のような感じ。
  • parse_option
    • rb_iseq_new_main
      • rb_iseq_new_with_opt
        • rb_iseq_new_with_bopt_and_opt (ブロックオプション付き?)
          • prepare_iseq_build
            • klass = 0 は ObjectSpace.each_object から隠す work around
            • set_relation cref_stack などを継承してる。
          • rb_iseq_compile_node <- ここからコンパイル開始。深くなるので一旦リセット
  • rb_iseq_compile_node
    • iseq_compile_each
      • 以下再帰。実質的には iseq_compile_each 関数がコンパイルの中心的な関数。
      • 再帰には2つあって、条件分岐などで条件部と本体部などNODEをそれぞれコンパイルしてからつなげる場合と、メソッド定義のように新規の iseq を作る場合。
      • これは後日調べましたが、poped 引数は「expr や stmt の値が使用されていない」時に渡されて、主に最後にスタックから値をpopする命令を追加するようになっています。ただし副作用のないリテラルや defined? ではそもそも命令列に追加されないという場合があります(後述)
    • iseq_setup
      • 各種 optimization (後述)
      • iseq_set_sequence
        • insn のバイト長のメタデータ構築。insn は(オペランドもあるので)長さが可変なので一度リストをなめて長さとラベル位置の確認をしています。
        • 配列に詰める。これでついに命令列である VALUE 配列ができあがり。
  • 最適化について。
    • 不要なジャンプ命令を除去する peephole optimization。
    • 特化命令、命令融合、オペランド融合。
    • スタックキャッシングというやつはよく読めなかった。
  • defined? のコンパイルがちょっとおもしろい
    • defined? には任意の文が渡せます。
    • defined_expr が専用の関数
    • リテラルの類やローカル変数(ローカル変数はパース時に決定されているのでわかるんです)の場合は、ただの putstring 命令にコンパイルされます。
      • この時 poped 引数が Qtrue だと、結果が使用されないので何も命令を出力しません。
    • その他のNODEの場合、例えばグローバル変数インスタンス変数、クラス変数、定数などの場合は実行時に検索するしかないので、専用の defined 命令を作ります。
    • 配列リテラルの場合は、各要素について再帰的に defined_expr が呼ばれます。
  • おまけとしてよしおかさんが oprofile の使いかたの解説と、ruby の make test-all した時の2次キャッシュミスする場所をさがして prefetch 命令をインラインアセンブラで埋め込んで性能改善してみる実例を発表

今回はちょっと発表側もやりました(defined? のところ)。当日とっさに思い付いたネタなのでさわださんが発表している間せっせとコードリーディングしてました。なので命令融合の実演のところとかメモが不足。
あと ust でみてみたら声もターミナルの字も小さすぎた。まあそれでも思ったほど緊張してコチコチにはならなかったし、最初はこんなものか。もっとスクリーンとか参加者の様子をみる余裕ができるといいんでしょうきっと。

次回は 2/14 で「YARVの命令ディスパッチと実行コンテキスト」だそうです。