LoginSignup
17
9

More than 5 years have passed since last update.

RubyをCに変換する話

Last updated at Posted at 2018-12-12

この記事は言語実装 Advent Calendar 2018 13日目の記事です。前日は、raviqqeさんのLLVMでの効率的なUnion型の扱いでした。すっかりLLVM IRが読めなくて愕然としました。alloca命令で返ってくる値は単なる領域を確保されたアドレスだと思ってると、思わぬ最適化をされてびっくりした記憶があります。

さて、私の記事を始めたいと思います。予告編がありますので、読むとより楽しめるかもしれませんし、へたくそなポエムを読まされただけになるかもしれません。

はじめに

 現在、mrubyのJIT2(仮称)というプログラムを作っています。正式な名前がまだ無かったり、レポジトリが昔作ったプログラムに間借りしていたりととてもいい加減なもんです。長いのでここではJIT2とします。JIT2の目的は、

  • とにかくRubyで書いたものを高速で実行する。その為にコンパイル時間やメモリがどれだけかかっても構わない
  • できるかぎりリスキーな技術を使い派手に面白く失敗する。

といったものです。1番目の目的はStalinというSchemeコンパイラの目的に近いでしょうか。
派手に失敗するという目的は、このプログラムが実用を考えていないことを意味します。企業や大学のOSSでは難しい個人ならではの目的と言えるでしょう。派手で面白い失敗は良いアイデアの元です。

JIT2の概要

JIT2はmrubyで書かれたmrubyのコンパイラです。概要はこんな感じです

  • mrubyのVM(RITEVM)のバイトコード命令列をSSA化したような中間コードを持っています。JIT2の本質はこの中間コードです
  • 中間コードレベルで最適化に必要な情報を集めてコード生成するというアーキテクチャを考えています。データフロー解析などに必要な情報はメモリの肥大化を躊躇することなく持たせます。
  • 中間コードを生成するにあたってRITEVMの命令列を仮想実行して、型やデータの寿命などを収集します(仮想実行(Abstract Interpretation))。Rubyでありながら変数やメソッドの型は。基本的に静的に決定しているものとしてコード生成できます。

今の所、型を推定して中間コードを生成するところがある程度のレベルまで完成し、この中間コードの応用例としてC言語に変換するコードを書いています。メソッドの再定義や整数演算のオーバーフロー時の扱いなどをRubyの仕様に忠実に実装するためには最終的には何らかの方法で機械語を直接生成する必要があると考えています。

この記事では型推定については説明しません(興味がある人は多いだろうなとは思いますが)。前に書いた、スライド記事を参照してください。

中間コード

中間コードは静的単一代入(SSA)のような構造をしています。SSAは代入が必ず一回しか現れないコード形式です。SSAを実装するためには

  • 代入は必ず1回だけにするため、複数回代入する場合にはバージョン付きの物にリネームする必要があります
  • SSAでは制御構造の扱いが面倒になります。内部に制御構造を含まないコード列(基本ブロックと呼ばれますが、Rubyのブロックと紛らわしいのでここではNodeと呼びます)に分解する必要があります。命令の変換.png
  • SSAのφ関数とは違い、この中間コードではレジスタが生存していようと、死んでいようと全てのレジスタをNode間で受け渡します(手抜き)。レジスタが生存しているかどうか判定するのはユーザの責任です。

JIT2の中間コードは単なるテキスト列(またはバイト列)ではなく、複数の種類のmrubyのオブジェクトが配列またはリンクリストで接続されているような構造を持っています。そのため、命令列としてテキストに書きだすことは(おそらく)困難でです。
しかし、SSAで重要な面倒なレジスタの名前を定義箇所に応じたバージョン付きの物にリネームする問題がオブジェクトのIDという形で簡単になるというメリットがあります。

中間コードの構造

中間コードは以下のような、Block, Node, INST(命令)と階層構造になっています。これらはすべてmrubyのオブジェクトです。
無題の図形描画.png

また、色々な所にレジスタとありますが、このレジスタもmrubyのオブジェクトです。
レジスタは様々な情報を保持しており、次のような情報をもっています。RITEVMのレジスタだけではなく、インスタンス変数やグローバル変数や配列の要素などにも同じようにレジスタオブジェクトを割り当てています。

  • type 型
  • genpoint このレジスタに代入した命令(単一代入だから常に1つ)
  • refpoint このレジスタを参照している命令

typeの重要性は言うまでもないですが、genpointを使うことで定数畳み込みなどの最適化が容易になるし、refpointを用いることで寿命解析が可能になります。詳しくはC言語への生成の所で説明します。

また、typeの中身である型を表す型オブジェクトには次のような情報をもっています。

  • class_object クラス(Rubyの生のクラスオブジェクト)
  • val 値(あれば)
  • place この型のデータが存在する場所

placeが変わっているというかなにこれ?って思われると思いますが、エスケープ解析を実現するためのしくみです。型情報などを収集するために行う仮想実行で、代入命令やArray#[]=メソッドの実行などの時にplaceに情報を付け加えていきます。placeは場所を表すオブジェクトの集合(実装はHash)で次のようなオブジェクトからなります。

  • レジスタ そのレジスタに格納されている可能性がある(現在実行中のメソッドのRITEVMのレジスタを除く)
  • true グローバル変数などに入るため必ずエスケープする
  • Procの型オブジェクト このProcオブジェクトのクロージャに入っている可能性がある 

placeが空集合の時はエスケープしないと言えます。trueが含まれているときは必ずエスケープします。そうじゃない時は、その親(インスタンス変数ならそのインスタンス)がエスケープするかによります。

C言語への変換

ここでは、具体的に中間コードをCに変換する手順を紹介します。全部紹介するのは無理なので、私が面白いと思ったところを紹介します。

Rubyの型推定の結果をCの型に変換する処理です。大体は自明なことですけど、自明じゃない部分もあります。処理全体はこんな感じです。汚いプログラムで済みません。

    TTABLE = {
      Fixnum => :mrb_int,
      Float => :mrb_float,
      Array => :array,
      NilClass => :nil
    }

    def self.get_ctype_aux(ccgen, inst, reg, tup)
      rtype = reg.type[tup]
      if !rtype then
        # for element of array
        rtype = reg.type[reg.type.keys[0]]
        if rtype.nil? then
          return :mrb_value
        end
      end

      cls0 = rtype[0].class_object
      if rtype.all? {|e| e.class_object == cls0} then
        res = TTABLE[cls0]
        if res then
          return res
        end
      end

      if rtype.all? {|e|
          cls = e.class_object
          cls == TrueClass || cls == FalseClass
        } and rtype.size > 0 then
        return :mrb_bool
      end

      :mrb_value
    end

    def self.get_ctype(ccgen, inst, reg, tup)
      type = get_ctype_aux(ccgen, inst, reg, tup)
      case type
      when :array
        if !is_escape?(reg) then
          uv = MTypeInf::ContainerType::UNDEF_VALUE
          ereg = reg.type[tup][0].element[uv]
          rc = get_ctype_aux(ccgen, inst, ereg, tup)
          if rc == :array then
            :mrb_value
          else
            rc
          end
        else
          :mrb_value
        end
      else
        type
      end
    end

get_ctypeがRubyの型をCの方に変換するメインになるのですが、配列を除くと下請メソッドのget_ctype_auxで行っています。get_ctype_auxの方が簡単なので、はじめにここから説明します。

    TTABLE = {
      Fixnum => :mrb_int,
      Float => :mrb_float,
      Array => :array,
      NilClass => :nil
    }

    def self.get_ctype_aux(ccgen, inst, reg, tup)
      rtype = reg.type[tup]
      if !rtype then
        # for element of array
        rtype = reg.type[reg.type.keys[0]]
        if rtype.nil? then
          return :mrb_value
        end
      end

      cls0 = rtype[0].class_object
      if rtype.all? {|e| e.class_object == cls0} then
        res = TTABLE[cls0]
        if res then
          return res
        end
      end

      if rtype.all? {|e|
          cls = e.class_object
          cls == TrueClass || cls == FalseClass
        } and rtype.size > 0 then
        return :mrb_bool
      end

      :mrb_value
    end

最初の、if !rtype then の部分は闇です。無いと動かない場合がありますが、これが何をしているのか型推定の所で挙げたスライドを全部理解した人なら説明して分かってもらえるかもって話のなので飛ばします。
ここを除くと後は簡単です。単にテーブルを引くだけです。ここで、Arrayのarrayという型、こんな型Cにあったっけ?って思うかもしれません。この型はダミーでget_ctypeで使います。

    def self.get_ctype(ccgen, inst, reg, tup)
      type = get_ctype_aux(ccgen, inst, reg, tup)
      case type
      when :array
        if !is_escape?(reg) then
          uv = MTypeInf::ContainerType::UNDEF_VALUE
          ereg = reg.type[tup][0].element[uv]
          rc = get_ctype_aux(ccgen, inst, ereg, tup)
          if rc == :array then
            :mrb_value
          else
            rc
          end
        else
          :mrb_value
        end
      else
        type
      end
    end

get_ctype_auxで:arrayが返って来た時だけget_ctypeは仕事をします。それ以外は丸投げです。
:arrayが返って来た時は、このレジスタのデータがエスケープするかを判定します。エスケープする場合は配列をヒープに取らないといけないので、有無を言わさずCレベルの型はmrb_value(mrubyでRubyのオブジェクトを格納する型)になります。
エスケープしないならCレベルの配列になるのですが、Cでは配列を宣言するためには要素の型が必要です。JIT2では当然配列の要素の型も推定しているので、それを取り出します。reg.type[tup][0].element[uv]がその場所です。配列の要素を表すレジスタを取り出して、再帰的に型を取り出します。配列が入れ子だった場合はがんばることもできるのですが、ここではがんばらないでRubyの配列を使います。

エスケープするかどうかを判定するis_escape?の定義はこんな感じです。

    def self.is_escape?(reg)
      plist = reg.type.keys.map { |tup|
        reg.type[tup].map {|ty| ty.place.keys}.flatten.uniq
      }.flatten.uniq
      plist.size != 0
    end

小難しそうですが、placeが完全に空ならエスケープしない、そうじゃなければエスケープするって判定しているだけです。もっと精度が高くて面倒くさいコードにすることもできますが、今の所は手抜きバージョンを使っています。

メソッド呼び出し

C言語への変換はRITE VMの命令を変換することで行います。命令毎にどう変換するのかはcgen_c_oprule.rbに定義してあります。しかし、これ見てもらえると分かると思いますが、ほとんどの命令は何もしません。では何もしていないかというとそんなことはありません。
何かをおこなう命令の1つであるメソッド呼び出しをおこなうSEND命令を例にどうC言語に変換するか説明します。
SEND命令の処理は次のようになります。

    def self.op_send_aux(ccgen, inst, node, infer, history, tup)
      name = inst.para[0]
      intype = inst.inreg.map {|reg| reg.flush_type(tup)[tup] || []}
      intype[0] = [intype[0][0]]
      rectype = intype[0][0].class_object
      mtab = MTypeInf::TypeInferencer.get_ruby_methodtab
      rectype.ancestors.each do |rt|
        if @@ruletab[:CCGEN_METHOD][name] and mproc = @@ruletab[:CCGEN_METHOD][name][rt] then
          return mproc.call(ccgen, inst, node, infer, history, tup)
        else
          if mtab[inst.para[0]] and mtab[inst.para[0]][rt] then
            proc = mtab[inst.para[0]][rt]
          else
            next
          end
          nreg = inst.outreg[0]
          ccgen.dcode << gen_declare(ccgen, inst, nreg, tup)
          ccgen.dcode << ";\n"

          utup = infer.typetupletab.get_tupple_id(intype, MTypeInf::PrimitiveType.new(NilClass), tup)
          fname = gen_method_func(name, rt, utup)

          args = inst.inreg.map {|reg|
            reg_real_value(ccgen, reg, node, tup, infer, history)
          }.join(", ")
          ccgen.pcode << "v#{nreg.id} = #{fname}(mrb, #{args});\n"
          minf = [name, proc, utup]
          if ccgen.using_method.index(minf) == nil then
            ccgen.using_method.push minf
          end

          return
        end
      end
      ccgen.pcode << "mrb_no_method_error(mrb, mrb_intern_lit(mrb, \"#{name}\"), mrb_nil_value(), \"undefined method #{name}\");\n"
      nil
    end

まずは、メソッド名、引数の型・レジスタ、レシーバーの型などを使いやすいようにローカル変数に格納します。

      name = inst.para[0]
      intype = inst.inreg.map {|reg| reg.flush_type(tup)[tup] || []}
      intype[0] = [intype[0][0]]
      rectype = intype[0][0].class_object
      mtab = MTypeInf::TypeInferencer.get_ruby_methodt

次にメソッド検索を行います。現状はレシーバーの型が一意に決まる場合のみをサポートしています。そのため、メソッドサーチのコードは生成しません(できません)。
メソッドサーチはこんな感じでコンパイル時に行います。レシーバーの型が分かっているので簡単です。

メソッドが組み込みメソッドかの判定

 if @@ruletab[:CCGEN_METHOD][name] and mproc = @@ruletab[:CCGEN_METHOD][name][rt] then

メソッドがユーザ定義の物かの判定

 if mtab[inst.para[0]] and mtab[inst.para[0]][rt] then

次に引数を組み立てます。引数を組み立て終わればあとはメソッドを呼び出すだけです。JIT2ではメソッドはCの関数の形で定義されます。

          args = inst.inreg.map {|reg|
            reg_real_value(ccgen, reg, node, tup, infer, history)
          }.join(", ")

reg_real_valueってのがレジスタの値がCに変換するメソッドです。こんな定義です。

    def self.reg_real_value(ccgen, reg, node, tup, ti, history)
      rc = reg_real_value_noconv(ccgen, reg, node, tup, ti, history)
    end

    def self.reg_real_value_noconv(ccgen, reg, node, tup, ti, history)
      if reg.is_a?(RiteSSA::ParmReg) then
        if node.enter_link.size == 1 then
          pnode = node.enter_link[0]
          preg = pnode.exit_reg[reg.genpoint]
          return reg_real_value(ccgen, preg, pnode, tup, ti, history)
        end

        if node.enter_link.size == 0 then # TOP of block
          ptype = ti.typetupletab.rev_table[tup][reg.genpoint]
          if ptype and ptype.size == 1 and
              ptype[0].class == MTypeInf::LiteralType then
            return ptype[0].val
          end
        end

        return "v#{reg.id}"
      end

      gins = reg.genpoint
      if !gins then
        return "mrb_nil_value()"
      end
      case gins.op
      when :MOVE
        reg_real_value(ccgen, gins.inreg[0], node, tup, ti, history)

      when :LOADL, :LOADI
        gins.para[0]

      when :LOADSYM
        "mrb_intern_lit(mrb, \"#{gins.para[0]}\")"

      when :LOADNIL
        "mrb_nil_value()"

      when :LOADSELF
        "self"

      when :GETCONST
        gins.para[1]

      when :ENTER
        i = gins.outreg.index(reg)
        reg_real_value(ccgen, gins.inreg[i], node, tup, ti, history)

      when :EQ
        gen_term(ccgen, gins, node, tup, ti, history, gins.inreg[0], gins.inreg[1], :==)

      when :LT
        gen_term(ccgen, gins, node, tup, ti, history, gins.inreg[0], gins.inreg[1], :<)

      when :LE
        gen_term(ccgen, gins, node, tup, ti, history, gins.inreg[0], gins.inreg[1], :<=)

      when :GT
        gen_term(ccgen, gins, node, tup, ti, history, gins.inreg[0], gins.inreg[1], :>)

      when :GE
        gen_term(ccgen, gins, node, tup, ti, history, gins.inreg[0], gins.inreg[1], :>=)

      when :ADD
        gen_term(ccgen, gins, node, tup, ti, history, gins.inreg[0], gins.inreg[1], :+)

      when :SUB
        gen_term(ccgen, gins, node, tup, ti, history, gins.inreg[0], gins.inreg[1], :-)

      when :MUL
        gen_term(ccgen, gins, node, tup, ti, history, gins.inreg[0], gins.inreg[1], :*)

      when :DIV
        gen_term(ccgen, gins, node, tup, ti, history, gins.inreg[0], gins.inreg[1], :/)

      when :ADDI
        gen_term(ccgen, gins, node, tup, ti, history, gins.inreg[0], gins.para[1], :+)

      when :SUBI
        gen_term(ccgen, gins, node, tup, ti, history, gins.inreg[0], gins.para[1], :-)

      else
        "v#{reg.id}"

      end
    end

くそ長いですが、やっていることは分かりやすいと思います。gins = reg.genpointの部分が肝です。これでregを作成した命令(つまりregの値はこの命令の出力)を得ています。これらが、何かで場合分けすることでレジスタの代入,参照を1つスキップできます。
さらに、それらの命令が四則演算や比較であれば、さらに遡って展開できます。また値が定数ならその場で計算してしまいます(eval便利)。この辺がgen_termの仕事で次のようなコードです。

    def self.gen_term(ccgen, gins, node, tup, ti, history, reg0, reg1, op)
      if reg0.is_a?(RiteSSA::Reg) then
        arg0 = reg_real_value(ccgen, reg0, node, tup, ti, history)
        case reg0.type[tup].size
        when 1
          type0 = reg0.type[tup][0].class_object
        else
          type0 = Object
        end
      else
        arg0 = reg0
        type0 = arg0.class
      end

      if reg1.is_a?(RiteSSA::Reg) then
        arg1 = reg_real_value(ccgen, reg1, node, tup, ti, history)
        case reg1.type[tup].size
        when 1
          type1 = reg1.type[tup][0].class_object
        else
          type1 = Object
        end
      else
        arg1 = reg1
        type1 = arg1.class
      end

      if (type0 == Fixnum or type0 == Float) and
          (type1 == Fixnum or type1 == Float) then
        if arg0.class != String and arg1.class != String then
          eval "(#{arg0} #{op} #{arg1})"
        else
          "(#{arg0} #{op} #{arg1})"
        end
      elsif op == :== and
          type0 == type1 then
        if arg0.class != String and arg1.class != String then
          eval "(#{arg0} #{op} #{arg1})"
        else
          "(#{arg0} #{op} #{arg1})"
        end

      else
        op_send_aux(ccgen, gins, node, ti, history, tup)
        nil
      end
    end

今見たら、最後のop_send_auxの部分がバグっていることに気づきました。そんな感じでこのコードの解説はなしです。

配列の生成と参照

配列を生成するところと参照するところが、エスケープ解析の結果を使っていて面白いと(私は)思います。

生成がこんな感じ。

    define_ccgen_rule_op :ARRAY do |ccgen, inst, node, infer, history, tup|
      reg = inst.outreg[0]
      aescape = is_escape?(reg)
      vals = inst.inreg.map {|reg|
        src = reg_real_value(ccgen, reg, node, tup, infer, history)
      }

      dstt = nil
      if aescape then
        dstt = :mrb_value
      else
        dstt = get_ctype(ccgen, inst, inst.outreg[0], tup)
      end

      i = 0
      vals2 = inst.inreg.map {|reg|
        srct = get_ctype(ccgen, inst, reg, tup)
        rc = gen_type_conversion(dstt, srct, vals[i])
        i = i + 1
        rc
      }

      if aescape then
        ccgen.hcode << "mrb_value v#{reg.id};"
        ccgen.pcode << "{\n"
        ccgen.pcode << "mrb_value tmpele[] = {\n"
        ccgen.pcode << vals2.join(', ')
        ccgen.pcode << "\n};\n"
        ccgen.pcode << "v#{reg.id} = mrb_ary_new_from_values(mrb, #{vals.size}, tmpele);\n"
        ccgen.pcode << "}\n"
      else
        type = get_ctype_aux(ccgen, inst, reg, tup)
        if type == :array then
          uv = MTypeInf::ContainerType::UNDEF_VALUE
          ereg = reg.type[tup][0].element[uv]
          etype = get_ctype_aux(ccgen, inst, ereg, tup)
          ccgen.pcode << "#{etype} v#{reg.id}[] = {\n"
          ccgen.pcode << vals2.join(', ')
          ccgen.pcode << "\n};\n"
        end
      end
      nil
    end

aescapeってのがARRAY命令で出来た配列がエスケープするかの情報をもっていて、エスケープするか、しないかで場合分けしています。もし、エスケープするので配列をヒープに取る必要があるのに、初期値がmrb_valueではない場合は型変換をする必要がありますが、型変換コードを生成するのが、gen_type_conversionです。コードはこんな感じ。mrubyのソース読んだことある人は、なるほどそういうことねって思うし、読んだことが無い人は意味不明なコードかと思います。

    def self.gen_type_conversion(dstt, srct, src)
      if dstt == srct then
        return src
      end

      case dstt
      when :mrb_value
        case srct
        when :mrb_int
          "(mrb_fixnum_value(#{src}))"

        when :mrb_float
          "(mrb_float_value(mrb, #{src}))"

        when :mrb_bool
          "((#{src}) ? mrb_true_value() : mrb_false_value())"

        when :nil
          "#{src}"

        else
          raise "Not support yet #{dstt} #{srct}"
        end

      when :mrb_int
        "(mrb_fixnum(#{src}))"

      when :mrb_float
        "(mrb_float(#{src}))"

      when :mrb_bool
        "(mrb_test(#{src}))"

      when :nil
        "mrb_nil_value()"

      else
        raise "Not support yet #{dstt} #{srct}"
      end
    end

配列を作る所がエスケープするかで場合分けしているので、参照するところも場合分けする必要があるわけです。そんなわけで、配列の参照の生成のコードです。

    define_ccgen_rule_method :[], Array do |ccgen, inst, node, infer, history, tup|
      srct = get_ctype(ccgen, inst, inst.inreg[0], tup)
      dstt = get_ctype(ccgen, inst, inst.outreg[0], tup)
      src = reg_real_value(ccgen, inst.inreg[0], node, tup, infer, history)
      nreg = inst.outreg[0]
      ccgen.dcode << gen_declare(ccgen, inst, nreg, tup)
      ccgen.dcode << ";\n"
      idx = reg_real_value(ccgen, inst.inreg[1], node, tup, infer, history)
      if is_escape?(inst.inreg[0]) then
        src = "mrb_ary_ref(mrb, #{src}, #{idx})"
        src = gen_type_conversion(dstt, :mrb_value, src)
      else
        src = "#{src}[#{idx}]"
        if srct == :nil then
          src = "mrb_nil_value()"
        else
          src = gen_type_conversion(dstt, srct, src)
        end
      end

      ccgen.pcode << "v#{nreg.id} = #{src};\n"
      nil
    end

C言語への変換の例

C言語への変換のプログラムはあまり出来ていないので、かぎられた簡単なプログラムしか変換できないのですが、2つほどの変換例を紹介します。

フィボナッチ

def fib(n)
  if n == 1 then
    1
  elsif n == 0 then
    1
  else
    fib(n - 1) + fib(n - 2)
  end
end

MTypeInf::inference_main {
  p fib(35)
}

MTypeInf::inference_mainはまだファイルから読みだして変換する処理を作っていないのでとりあえずのおまじないです。これを変換すると、次のようなコードが生成されます。

mrb_int main_Object_0(mrb_state *mrb, mrb_value self) {
  mrb_int v17;
  mrb_int v19;
 L0:; 
  v17 = fib_Object_1(mrb, self, 35, mrb_nil_value());
  mrb_p(mrb, (mrb_fixnum_value(v17)));
  v19 = v17;
  return v19;
}
mrb_int fib_Object_1(mrb_state *mrb, mrb_value self, mrb_int v135, mrb_value v136) {
  mrb_int v199;
  mrb_int v204;
  mrb_int v211;
 L9:; 
 L11:; 
 L13:; 
   v199 = fib_Object_2(mrb, self, 34, mrb_nil_value());
   v204 = fib_Object_2(mrb, self, 33, mrb_nil_value());
   v211 = (v199 + v204);
 L14:; 
   return v211;
}
mrb_int fib_Object_2(mrb_state *mrb, mrb_value self, mrb_int v135, mrb_value v136) {
  mrb_int v211;
  mrb_int v199;
  mrb_int v204;
 L9:; 
  if ((v135 == 1)) goto L10; else goto L11;
 L10:; 
  v211 = 1;
 L14:; 
  return v211;
 L11:; 
  if ((v135 == 0)) goto L12; else goto L13;
 L12:; 
  v211 = 1;
  goto L14;
 L13:; 
  v199 = fib_Object_2(mrb, self, (v135 - 1), mrb_nil_value());
  v204 = fib_Object_2(mrb, self, (v135 - 2), mrb_nil_value());
  v211 = (v199 + v204);
  goto L14;
}

もともと生成されたCコードはインデントがついていないのですが、読みやすいようにインデントを付けました。また、ヘッダファイルとかmain関数とかは省いてありますので、このままではコンパイルできません。 
fib_Object_1, fib_Object_2がfibメソッドをCに変換したものです。fibはメソッド名、Objectはレシーバのクラス、1とか2が引数の型の組み合わせによるメソッドのバージョン(JIT2ではtupと呼んでいます)です。fibの引数に2バージョンあるの?って思う人がいるかもしれませんが、JIT2の型推定ではfib(35)とfib(n)を別の型として取り扱います。
このことと、定数同士の演算は畳込む最適化から、fib_Object_1であたかも1段インライン化したようなコードになっています。たしか、LLVMもこのようなコードが出ていたと記憶しています(ver 2.5くらいの10年以上前の話ですが)。

ちなみに、mrubyの60~70倍の速度が出ます。

配列

配列の例はJIT2の型推定とエスケープ解析のパワーのデモンストレーションです。

まずは、配列の参照がすべて範囲内で配列そのものがエスケープしない場合です。この場合は明示的にnilを代入していない限り配列の参照でnilが返ってくることが無いしエスケープしない場合は配列はスタックに取ることが出来るので、ほとんどCを手書きしたようなコードが生成されます。

def foo
  a = [1, 2, 3]
  a[0] + a[1] + a[2]
end

MTypeInf::inference_main {
  p foo
}
mrb_int main_Object_0(mrb_state *mrb, mrb_value self) {
  mrb_int v15;
  mrb_int v17;
 L0:; 
  v15 = foo_Object_1(mrb, self, mrb_nil_value());
  mrb_p(mrb, (mrb_fixnum_value(v15)));
  v17 = v15;
  return v17;
}
mrb_int foo_Object_1(mrb_state *mrb, mrb_value self, mrb_value v82) {
  mrb_int v101;
  mrb_int v105;
  mrb_int v111;
 L4:; 
  mrb_int v97[] = {
    1, 2, 3
  };
  v101 = v97[0];
  v105 = v97[1];
  v111 = v97[2];
  return ((v101 + v105) + v111);
}

一方、範囲外の配列の参照がある場合、nilが返ってきます。配列の参照でnilが返る可能性がある場合はCの型は使えずにRubyのオブジェクトの配列として宣言する必要があります。配列の参照のインデックスが定数ではなく変数である場合、現時点では常に範囲外の参照があるものとして取り扱います。
しかし、これではあまりにも厳しすぎるので何とか配列の参照のインデックスが変数でも範囲外の参照が無いことを保障できないか検討しています。例えば、何らかの宣言を取り入れるとか、整数型に範囲の概念を入れるとか考えています。

def bar
  a = [1, 2, 3]
  a[0] + a[1] + a[2] + a[3]
end

MTypeInf::inference_main {
  p bar
}
mrb_value main_Object_0(mrb_state *mrb, mrb_value self) {
  mrb_value v15;
  mrb_value v17;
 L0:; 
  v15 = bar_Object_1(mrb, self, mrb_nil_value());
  mrb_p(mrb, v15);
  v17 = v15;
  return v17;
}
mrb_value bar_Object_1(mrb_state *mrb, mrb_value self, mrb_value v88) {
  mrb_int v107;
  mrb_int v111;
  mrb_int v117;
  mrb_value v123;
 L4:; 
  mrb_value v103[] = {
    (mrb_fixnum_value(1)), (mrb_fixnum_value(2)), (mrb_fixnum_value(3))
  };
  v107 = (mrb_fixnum(v103[0]));
  v111 = (mrb_fixnum(v103[1]));
  v117 = (mrb_fixnum(v103[2]));
  v123 = v103[3];
  mrb_no_method_error(mrb, mrb_intern_lit(mrb, "+"), mrb_nil_value(), "undefined method +");
  return mrb_nil_value();
}

配列がエスケープする場合は、配列はヒープに取る必要があります。この場合は、Rubyのオブジェクトとして配列を取り扱います。この場合、配列の参照もmrubyのAPIを用いるように変換されます。

def baz
  a = [1, 2, 3]
  a[0] + a[1] + a[2]
  a
end

MTypeInf::inference_main {
  p baz
}
mrb_value main_Object_0(mrb_state *mrb, mrb_value self) {
  mrb_int v15;
  mrb_int v17;
 L0:; 
  v15 = baz_Object_1(mrb, self, mrb_nil_value());
  mrb_p(mrb, v15);
  v17 = v15;
  return v17;
}
mrb_value baz_Object_1(mrb_state *mrb, mrb_value self, mrb_value v82) {
  mrb_int v101;
  mrb_int v105;
  mrb_int v111;
 L4:; 
  {
    mrb_value tmpele[] = {
      (mrb_fixnum_value(1)), (mrb_fixnum_value(2)), (mrb_fixnum_value(3))
    };
    v97 = mrb_ary_new_from_values(mrb, 3, tmpele);
  }
  v101 = (mrb_fixnum(mrb_ary_ref(mrb, v97, 0)));
  v105 = (mrb_fixnum(mrb_ary_ref(mrb, v97, 1)));
  v111 = (mrb_fixnum(mrb_ary_ref(mrb, v97, 2)));
  return v97;
}

つまり、Rubyレベルで配列を使うと、その場で一番ふさわしい配列の内部表現を自動的に選んでくれるわけです。

これから

まだ、JIT2でのC生成はほとんどできていません。さしあたっての難関はProcオブジェクト(クロージャ)です。RubyらしいプログラムはProcオブジェクトを多用するので、これは必須です。ある程度できているのですが、Cの型システムではどうやってもできないのでお行儀の悪いこと(void*とか)が必要ですがどうやろうか悩んでいます。
ある程度Cで動くようになったらそれ以上作り込むのは無駄です。次はいよいよ機械語を直接生成する処理を作るわけですが、x86/x86-64/armで動かしたいので良いコード生成系を探しているところです。

明日はtzmfreedomさんの「自作言語のデバッガ実装について」の予定です。

17
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
9