Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RubyVM読んでみた。

242d711e8cebfb2cbcd25f7e0e26a337?s=47 ocha-
April 19, 2014

 RubyVM読んでみた。

242d711e8cebfb2cbcd25f7e0e26a337?s=128

ocha-

April 19, 2014
Tweet

Other Decks in Programming

Transcript

  1. RubyVM読んでみた。 Kawamoto (ocha-) Oedo Ruby Kaigi 04 2014/04/19

  2. 自己紹介 github.com/ocha- お茶 RHGの翻訳(残ってた分)やりました いろいろソース読むのと英語が趣味 です

  3. 身の上話 Ruby歴がたぶん6、7年くらい Ruby歴 = プログラム歴 Ruby On Rails 使うためにRuby始めました 独学

    あまり専門的なことはわかりません
  4. RHGの翻訳プロジェクト Since 2006 rubyforge.org/projects/rhg github.com/ruby-hacking-guide/ruby-hacking- guide.github.com 経緯が少々複雑です 詳しくはMLをrubyforge.org/pipermail/rhg-discussion

  5. Ruby Hacking Guide 青木峰郎さん 伝説の本 超オススメ あえていうなら特に第2部が好き

  6. ソースを読む目的 読みたいから! 知りたいことがある 具体的 & 実用的な実装を通していろいろ学べる できれば何かオープンソースでできること見つけていき たい

  7. 今日のテーマ どんなかんじのこと調べてるかっていう個人的な話します

  8. 知りたいこと RubyVM GC GVL

  9. 一番最初の疑問点 バイトコードって実際どういう状態な のでしょう

  10. バイトコードの利点 メモリ上で並んでるとまとめてとってこれる(キャッシュが つかえる) 最適化手法の研究が蓄積されている 詳しくは2007年ごろのインタビュー記事がいろいろ オススメは「まつもと×笹田、Ruby 1.9を語る」

  11. それで ASTで表現されてていたデータをどういった風に一列に 並べるのか 最適化って実際どういうことをやるのか

  12. たぶんソースを読めばわかるは ず

  13. データ構造 RHGによれば、「まずデータ構造か ら」

  14. データ構造 それっぽい構造体や名前がいろいろあるので とりあえず把握 iseq ? insns ? rb_iseq_t, iseq_link_element, INSN

    cfp->iseq ? iseq->iseq ?
  15. rb_iseq_t でかい /* method.h */ typedef struct rb_iseq_struct rb_iseq_t; /*

    vm_core.h */ struct rb_iseq_struct { /***************/ /* static data */ /***************/ enum iseq_type { ISEQ_TYPE_TOP, ISEQ_TYPE_METHOD, ISEQ_TYPE_BLOCK, ISEQ_TYPE_CLASS, ISEQ_TYPE_RESCUE, ISEQ_TYPE_ENSURE, ISEQ_TYPE_EVAL, ISEQ_TYPE_MAIN, ISEQ_TYPE_DEFINED_GUARD } type; /* instruction sequence type */ rb_iseq_location_t location; VALUE *iseq; /* iseq (insn number and operands) */ VALUE *iseq_encoded; /* encoded iseq */ unsigned long iseq_size; const VALUE mark_ary; /* Array: includes operands which should be GC marked */ const VALUE coverage; /* coverage array */ /* insn info, must be freed */ struct iseq_line_info_entry *line_info_table; size_t line_info_size; ID *local_table; /* must free */ int local_table_size; /* sizeof(vars) + 1 */ int local_size; union iseq_inline_storage_entry *is_entries; int is_size; rb_call_info_t *callinfo_entries; int callinfo_size; ...
  16. rb_iseq_t 特にそれっぽいとこ /* method.h */ typedef struct rb_iseq_struct rb_iseq_t; /*

    vm_core.h */ struct rb_iseq_struct { ... VALUE *iseq; /* iseq (insn number and operands) */ VALUE *iseq_encoded; /* encoded iseq */
  17. LINK_ELEMENT (iseq_link_element *) な next, prev で、なんかリ ンクリストっぽい typedef struct

    iseq_link_element { enum { ISEQ_ELEMENT_NONE, ISEQ_ELEMENT_LABEL, ISEQ_ELEMENT_INSN, ISEQ_ELEMENT_ADJUST } type; struct iseq_link_element *next; struct iseq_link_element *prev; } LINK_ELEMENT;
  18. iseq_insn_data とかいろいろ 最初にLINK_ELEMENTがあるから全部 LINK_ELEMENT typedef struct iseq_link_anchor { LINK_ELEMENT anchor;

    LINK_ELEMENT *last; } LINK_ANCHOR; typedef struct iseq_label_data { LINK_ELEMENT link; ... } LABEL; typedef struct iseq_insn_data { LINK_ELEMENT link; ... } INSN; typedef struct iseq_adjust_data { LINK_ELEMENT link; ... } ADJUST;
  19. AST --> バイトコード の変換を見ていけば使われ方がわ かるはず

  20. 具体例 # simple.rb x = 1 x + 2

  21. 入力と結果

  22. AST( ruby --dump parsetree ) # @ NODE_SCOPE (line: 2)

    # +- nd_tbl: :x # +- nd_args: # | (null node) # +- nd_body: # @ NODE_BLOCK (line: 1) # +- nd_head: # | @ NODE_DASGN_CURR (line: 1) # | +- nd_vid: :x # | +- nd_value: # | @ NODE_LIT (line: 1) # | +- nd_lit: 1 # +- nd_next: # @ NODE_BLOCK (line: 2) # +- nd_head: # | @ NODE_CALL (line: 2) # | +- nd_mid: :+ # | +- nd_recv: # | | @ NODE_DVAR (line: 2) # | | +- nd_vid: :x # | +- nd_args: # | @ NODE_ARRAY (line: 2) # | +- nd_alen: 1 # | +- nd_head: # | | @ NODE_LIT (line: 2) # | | +- nd_lit: 2 # | +- nd_next: # | (null node) # +- nd_next: # (null node)
  23. insns (ruby --dump insns) バイトコードの様子(最適化後) == disasm: <RubyVM::InstructionSequence:<main>@simple.rb>=============== local table

    (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, keyword: 0@3] s1) [ 2] x 0000 trace 1 ( 1) 0002 putobject_OP_INT2FIX_O_1_C_ 0003 setlocal_OP__WC__0 2 0005 trace 1 ( 2) 0007 getlocal_OP__WC__0 2 0009 putobject 2 0011 opt_plus <callinfo!mid:+, argc:1, ARGS_SKIP> 0013 leave
  24. 最適化しない # Usage: ruby disasm.rb file_to_dump.rb iseq = RubyVM::InstructionSequence.compile_file ARGV.first,

    false print iseq.disasm
  25. 結構シンプルになります == disasm: <RubyVM::InstructionSequence:<main>@simple.rb>=============== local table (size: 2, argc: 0

    [opts: 0, rest: -1, post: 0, block: -1, keyword: 0@3] s1) [ 2] x 0000 putobject 1 ( 1) 0002 setlocal x, 0 0005 getlocal x, 0 ( 2) 0008 putobject 2 0010 send <callinfo!mid:+, argc:1, ARGS_SKIP> 0012 leave
  26. とりあえず 最適化による変化を後回しに ast --> 最適化前 の流れをみることにする

  27. ast は作ったとして (RHG参照)

  28. rb_iseq_new_main この時点でバイトコードの配列が返ってくると 期待してました /* ruby.c process_options */ iseq = rb_iseq_new_main(tree,

    opt->script_name, path);
  29. rb_iseq_new_with_bopt_and_opt rb_iseq_tのallocate地点になります /* iseq.c */ static VALUE rb_iseq_new_with_bopt_and_opt(NODE *node, VALUE

    name, VALUE path, VALUE absolute_path, VALUE first_lineno, VALUE parent, enum iseq_type type, VALUE bopt, const rb_compile_option_t *option) { rb_iseq_t *iseq; VALUE self = iseq_alloc(rb_cISeq); GetISeqPtr(self, iseq); iseq->self = self; prepare_iseq_build(iseq, name, path, absolute_path, first_lineno, parent, type, bopt, option); rb_iseq_compile_node(self, node); cleanup_iseq_build(iseq); return self; }
  30. tree --> iseq 特にみたいとこ rb_iseq_compile_node(iseq, tree);

  31. rb_iseq_new_with_bopt_and_opt /* iseq.c */ static VALUE rb_iseq_new_with_bopt_and_opt(NODE *node, VALUE name,

    VALUE path, VALUE absolute_path, VALUE first_lineno, VALUE parent, enum iseq_type type, VALUE bopt, const rb_compile_option_t *option) { rb_iseq_t *iseq; VALUE self = iseq_alloc(rb_cISeq); GetISeqPtr(self, iseq); iseq->self = self; prepare_iseq_build(iseq, name, path, absolute_path, first_lineno, parent, type, bopt, option); rb_iseq_compile_node(self, node); /* <-- Here! */ cleanup_iseq_build(iseq); return self; }
  32. 基本情報 RHG (Ruby 1.8以前) で nodeをたどりつつ実行だったの ですが nodeをたどりつつバイトコードに変換になります

  33. ast 最初はNODE_SCOPE # @ NODE_SCOPE (line: 2) # +- nd_tbl:

    :x # +- nd_args: # | (null node) # +- nd_body: # @ NODE_BLOCK (line: 1) # +- nd_head: # | @ NODE_DASGN_CURR (line: 1) # | +- nd_vid: :x # | +- nd_value: # | @ NODE_LIT (line: 1) # | +- nd_lit: 1 # +- nd_next: # @ NODE_BLOCK (line: 2) # +- nd_head: # | @ NODE_CALL (line: 2)
  34. rb_iseq_compile_node (extremely simplified) nodeがNODE_SCOPEだった場合 /* compile.c */ VALUE rb_iseq_compile_node(VALUE self,

    NODE *node) { DECL_ANCHOR(ret); rb_iseq_t *iseq; INIT_ANCHOR(ret); GetISeqPtr(self, iseq); ... iseq_set_local_table(iseq, node->nd_tbl); iseq_set_arguments(iseq, ret, node->nd_args); ... COMPILE(ret, "scoped node", node->nd_body); ... ADD_INSN(ret, iseq->compile_data->last_line, leave); ... return iseq_setup(iseq, ret); }
  35. わかったこと(1) バイトコード配列になる前にもう1つ形態があるらしい バイトコードになるのは NODE_SCOPEのnd_bodyから treeで表現されていたすべてが一列に並んだデータに変 換されているわけではなかった ast --> バイトコード列 より

    ast --> rb_iseq_t
  36. ret を作るところ見ていきます DECL_ANCHOR(ret); INIT_ANCHOR(ret); COMPILE(ret, "scoped node", node->nd_body); ... return

    iseq_setup(iseq, ret);
  37. COMPILE, COMPILE_ /* compile node */ #define COMPILE(anchor, desc, node)

    \ (debug_compile("== " desc "\n", \ iseq_compile_each(iseq, (anchor), (node), 0))) /* compile node, which is popped when 'poped' is true */ #define COMPILE_(anchor, desc, node, poped) \ (debug_compile("== " desc "\n", \ iseq_compile_each(iseq, (anchor), (node), (poped))))
  38. iseq_compile_each 何度も呼ばれます ノードたどりつつret作られていきま す

  39. iseq_compile_each: NODE_BLOCK nd_headをCOMPILE_してnd_nextをたどる case NODE_BLOCK:{ while (node && nd_type(node) ==

    NODE_BLOCK) { COMPILE_(ret, "BLOCK body", node->nd_head, (node->nd_next == 0 && poped == 0) ? 0 : 1); node = node->nd_next; } if (node) { COMPILE_(ret, "BLOCK next", node->nd_next, poped); } break; }
  40. iseq_compile_each: NODE_DASGN_CURR x = 1, nd_valueのCOMPILEが先 case NODE_DASGN_CURR:{ int idx,

    lv, ls; COMPILE(ret, "dvalue", node->nd_value); debugp_param("dassn id", rb_str_new2(rb_id2name(node->nd_vid) ? rb_id2name(node->nd_vid) : "*")); if (!poped) { ADD_INSN(ret, line, dup); } idx = get_dyna_var_idx(iseq, node->nd_vid, &lv, &ls); if (idx < 0) { rb_bug("NODE_DASGN(_CURR): unknown id (%s)", rb_id2name(node->nd_vid)); } ADD_INSN2(ret, line, setlocal, INT2FIX(ls - idx), INT2FIX(lv)); break; }
  41. iseq_compile_each: NODE_LIT 1 case NODE_LIT:{ debugp_param("lit", node->nd_lit); if (!poped) {

    ADD_INSN1(ret, line, putobject, node->nd_lit); } break; }
  42. ADD_INSN insn (+ operands) なLINK_ELEMENT 作って ADD_ELEM #define ADD_INSN(seq, line,

    insn) \ ADD_ELEM((seq), (LINK_ELEMENT *) new_insn_body(iseq, (line), BIN(insn), 0)) #define ADD_INSN1(seq, line, insn, op1) \ ADD_ELEM((seq), (LINK_ELEMENT *) \ new_insn_body(iseq, (line), BIN(insn), 1, (VALUE)(op1))) #define ADD_INSN2(seq, line, insn, op1, op2) \ ADD_ELEM((seq), (LINK_ELEMENT *) \ new_insn_body(iseq, (line), BIN(insn), 2, (VALUE)(op1), (VALUE)(op2)))
  43. ADD_ELEM こちらは関数、elemつなげてます /* * elem1, elem2 => elem1, elem2, elem

    */ static void ADD_ELEM(ISEQ_ARG_DECLARE LINK_ANCHOR *anchor, LINK_ELEMENT *elem) { elem->prev = anchor->last; anchor->last->next = elem; anchor->last = elem; verify_list("add", anchor); }
  44. ret できたとして ANCHOR -- INSN -- INSN -- INSN --

    ...
  45. iseq_setup(iseq, ret) ret はさらに最適化されて最終的な並びになります するとiseq->iseq に必要なスペースのサイズがわかりま す iseq->iseq を具体的にallocateして値をいれていきます

  46. iseq_setup いろいろ最適化でretの内容かわっていきます static int iseq_setup(rb_iseq_t *iseq, LINK_ANCHOR *anchor) { /*

    debugs("[compile step 2] (iseq_array_to_linkedlist)\n"); */ if (compile_debug > 5) dump_disasm_list(FIRST_ELEMENT(anchor)); debugs("[compile step 3.1 (iseq_optimize)]\n"); iseq_optimize(iseq, anchor); if (compile_debug > 5) dump_disasm_list(FIRST_ELEMENT(anchor)); if (iseq->compile_data->option->instructions_unification) { debugs("[compile step 3.2 (iseq_insns_unification)]\n"); iseq_insns_unification(iseq, anchor); if (compile_debug > 5) dump_disasm_list(FIRST_ELEMENT(anchor)); } if (iseq->compile_data->option->stack_caching) { debugs("[compile step 3.3 (iseq_set_sequence_stackcaching)]\n"); iseq_set_sequence_stackcaching(iseq, anchor); if (compile_debug > 5) dump_disasm_list(FIRST_ELEMENT(anchor)); } ...
  47. iseq_setup 見たいとこ static int iseq_setup(rb_iseq_t *iseq, LINK_ANCHOR *anchor) { ...

    ... iseq_set_sequence(iseq, anchor);
  48. iseq_set_sequence (1) リストをたどって必要なサイズを計算し ます static int iseq_set_sequence(rb_iseq_t *iseq, LINK_ANCHOR *anchor)

    { ... list = FIRST_ELEMENT(anchor); k = pos = 0; while (list) { switch (list->type) { case ISEQ_ELEMENT_INSN: { iobj = (INSN *)list; line = iobj->line_no; pos += insn_data_length(iobj); k++; break; }
  49. iseq_set_sequence (2) 必要なサイズを割り当てします generated_iseq = ALLOC_N(VALUE, pos); ... ... iseq->iseq

    = (void *)generated_iseq; iseq->iseq_size = pos;
  50. iseq_set_sequence (3) リストをたどって値をいれていきます while (list) { switch (list->type) { case

    ISEQ_ELEMENT_INSN: { ... iobj = (INSN *)list; ... operands = iobj->operands; insn = iobj->insn_id; generated_iseq[pos] = insn; ... for (j = 0; types[j]; j++) { ... case TS_VALUE: /* VALUE */ { VALUE v = operands[j]; generated_iseq[pos + 1 + j] = v;
  51. わかったこと(2) バイトコード列とは [insn, operand, insn, operand, operand, insn] みたいな並びらしい

  52. わかったこと(3) バイトコードのリンクリスト(ret)が iseq_setup 内でよばれる iseq_set_sequence で バイトコード配列(generated_iseq)に変換されて iseq->iseq にセットされる

  53. iseq->iseq 完成! なんとなく基本的な流れと内容がわかったので 次は 実行される流れを追ってみたり いろいろなケースで試してみたり 最適化on/offを試してみたり

  54. ちなみに iseq->iseq_encoded は iseq->iseq 内のinsnが変 換済みのもので、どちらが実行に使われるかは 環境によると思われます。 struct rb_iseq_struct {

    ... VALUE *iseq; /* iseq (insn number and operands) */ VALUE *iseq_encoded; /* encoded iseq */
  55. オススメ Ruby と mruby を同時に見ていく 基本思想は近い(はず) 比べながら見ていくのが面白い 例えば、、

  56. mruby の ast --> bytecode 入力と結果 (mruby -v simple.rb) mruby

    1.0.0 (2014-01-10) NODE_SCOPE: local variables: x NODE_BEGIN: NODE_ASGN: lhs: NODE_LVAR x rhs: NODE_INT 1 base 10 NODE_CALL: NODE_LVAR x method='+' (112) args: NODE_INT 2 base 10 irep 0x225c6f0 nregs=4 nlocals=2 pools=0 syms=1 reps=0 000 OP_LOADI R1 1 001 OP_MOVE R2 R1 002 OP_ADDI R2 :+ 2 003 OP_STOP
  57. RubyVM バイトコード比較してみる == disasm: <RubyVM::InstructionSequence:<main>@simple.rb>=============== local table (size: 2, argc:

    0 [opts: 0, rest: -1, post: 0, block: -1, keyword: 0@3] s1) [ 2] x 0000 putobject 1 ( 1) 0002 setlocal x, 0 0005 getlocal x, 0 ( 2) 0008 putobject 2 0010 send <callinfo!mid:+, argc:1, ARGS_SKIP> 0012 leave
  58. mruby の ast node: mrb_ast_node /* AST node structure */

    typedef struct mrb_ast_node { struct mrb_ast_node *car, *cdr; uint16_t lineno, filename_index; } mrb_ast_node;
  59. MRI の ast node: RNode /* node.h */ typedef struct

    RNode { VALUE flags; VALUE nd_reserved; /* ex nd_file */ union { struct RNode *node; ID id; VALUE value; VALUE (*cfunc)(ANYARGS); ID *tbl; } u1; union { struct RNode *node; ID id; long argc; VALUE value; } u2; union { struct RNode *node; ID id; long state; struct rb_global_entry *entry; struct rb_args_info *args; long cnt; VALUE value; } u3; } NODE;
  60. mruby の NODE_WHILE /* (:while cond body) */ static node*

    new_while(parser_state *p, node *a, node *b) { return cons((node*)NODE_WHILE, cons(a, b)); }
  61. mruby は irep->iseq irep->iseq は大きめに確保して前からいれてくようです 最適化もいれながらやってきます 最後にぴったりサイズになります

  62. mruby: scope_new 大きめ確保 static codegen_scope* scope_new(mrb_state *mrb, codegen_scope *prev, node

    *lv) { ... static const codegen_scope codegen_scope_zero = { 0 }; ... *p = codegen_scope_zero; ... p->irep = mrb_add_irep(mrb); scope_add_irep(prev, p->irep); p->rcapa = 8; p->irep->reps = (mrb_irep**)mrb_malloc(mrb, sizeof(mrb_irep*)*p->rcapa); p->icapa = 1024; p->iseq = (mrb_code*)mrb_malloc(mrb, sizeof(mrb_code)*p->icapa); p->irep->iseq = p->iseq; ...
  63. mruby: genop 前からいれてきます static inline int genop(codegen_scope *s, mrb_code i)

    { if (s->pc == s->icapa) { s->icapa *= 2; s->iseq = (mrb_code *)codegen_realloc(s, s->iseq, sizeof(mrb_code)*s->icapa); ... } s->iseq[s->pc] = i; ... return s->pc++; }
  64. mruby: scope_finish ぴったりサイズになります static void scope_finish(codegen_scope *s) { mrb_state *mrb

    = s->mrb; mrb_irep *irep = s->irep; ... if (s->iseq) { irep->iseq = (mrb_code *)codegen_realloc(s, s->iseq, sizeof(mrb_code)*s->pc); irep->ilen = s->pc; ...
  65. これからみたいこと知りたいこと 引き続き RubyVM と mruby Rubinius の LLVM 引き続き GC、GVL

    笹田さんのRGenGCの解説
  66. ありがとうございました!