Ruby Inside。 YARVバイトコード(I)



これ以降の記事では、Ruby MRI 1 1.9で使用される仮想マシンであるYARVのバイトコードについてお話したいと思います。



まず、少し歴史。 Rubyの最初の実装は、最終的にRuby 1.8になりましたが、非常に非効率なインタープリターがありました:コードを読み込むと、完全にメモリに格納された抽象構文ツリーになり、コードの実行はこのツリーの単純な走査でした。 巨大なツリー(ASTが約10メガバイト2を要したRailsについて考えてください)をバイパスするという事実に目を閉じても、プロセッサがコードを適切にキャッシュできないため、メモリ内のリンクはかなり遅くなります。少なくともいくつかの最適化。 また、組み込みのFixnumクラスを含むオブジェクトのメソッドをオーバーライドできる非常に柔軟なオブジェクト指向システムにより、オブジェクトのメソッドを呼び出すことにより算術計算が実行されたことを考慮します(はい、5 + 3はオブジェクトの「+」メソッドと呼ばれます) 5スタックフレームの作成)、Ruby 1.8は一般的なインタプリタプログラミング言語の中で最も遅いものの1つであることが判明しました。



YARV (Yet Another Ruby VM)は、 Sa田浩一が開発し、メインツリーに統合したスタック型仮想マシンであり、これらの欠点のすべてではないにしても修正しました。 コードはコンパクトな表現に変換され、最適化3され、以前よりはるかに高速に実行されます。



ただし、ここでは、他の仮想マシンとの1つの重要な違いがあります。 YARVを生成するバイトコードは保存できますが、ロードできません-再配布可能なバージョンでは、バイトコードローダーが無効になります(ただし、ソースコードにあり、必要に応じてオンにできます)。 公式の理由は検証者の不足ですが、私には思えますが、真実は、このバイトコードは互換性を考えずにいつでも変更を加えることができる内部形式と見なされていることであり、彼らはこの状況を保存しようとしています。



その結果、パフォーマンスを分析したり、代替インタープリターを開発したりする場合、バイトコードへのアクセスは絶対に不可欠であるという事実にもかかわらず、クラスとしてのドキュメントはありません。 YARV Instructionsなどのサイトは、Rubyソースコードから解析された仮想マシン定義ファイルにすぎません。 (バイトコードダンプのヘッダーにある一部のフィールドの存在の意味は、ある日本人のブログの投稿の変数の名前から理解できました。)



同様の状況を部分的に修正したいと思います。 この記事と後続の記事では、Rubyバイトコードデバイスで理解できたことと、それをどのように実践したかを正確に説明します。 一部の機能を理解できなかったとすぐに言わなければなりません。 このような場合、これを個別にマークします。 そのようなフレーズがない場合、これは、実際に受信した情報を検証できたことを意味し、すべてが正常に機能することを意味します。



実際、バイトコードに進みます。 Ruby 4には、任意のテキストをバイトコードにコンパイルできるシステムクラスRubyVM :: InstructionSequenceがあります(私の知る限り、ロードされたプログラムのバイトコードを取得することは不可能です)。 最も単純なケースでは、このクラスのオブジェクトを返すInstructionSequence.compileメソッドと、バイトコードダンプを返すInstructionSequence#to_aメソッドを使用するだけで十分です。



#to_aメソッドはConfiguration over Conventionの原則に従って、オブジェクトを配列に変換する必要があるため、Rubyを知っている読者は既にダンプが配列であるべきだと気付いています。



ここでは小さな余談が必要です。 実装の標準バージョンでは、バイトコードは、その名前が示すように、バイトのシーケンスであり、インタープリターの内部のどこかにあるように見えます。 ただし、標準的な方法で取得できるその表現は、通常のRubyオブジェクト、つまりネストされた配列で構成されるツリーのように見えます。 標準型の最小サブセットのみが含まれます:Array、String、Symbol、Fixnum、Hash(ヘッダーのみ)、nil、true、false。 これは非常に便利です(Rubyスタイル):バイナリデータを解析する必要はありませんが、マジック定数、オペコード番号、トランスレーターの将来のバージョンでの互換性のない変更を考慮することなく、すぐに読み取り可能な表現で作業できます。



そのため、簡単なプログラムのダンプを取得します。

ruby-1.9.2-p136 :001 > seq = RubyVM::InstructionSequence.compile(%{puts "Hello, YARV!"})

=> <RubyVM::InstructionSequence:<compiled>@<compiled>>

ruby-1.9.2-p136 :002 > seq.to_a

=> ["YARVInstructionSequence/SimpleDataFormat", 1, 2, 1, {:arg_size=>0, :local_size=>1, :stack_max=>2}, "<compiled>", "<compiled>", nil, 1, :top, [], 0, [], [1, [:trace, 1], [:putnil], [:putstring, "Hello, YARV!"], [:send, :puts, 1, nil, 8, 0], [:leave]]]









ダンプは、ヘッダーと実際のコードの2つの部分で構成されています。 ヘッダーフィールドを考慮してください。



"YARVInstructionSequence/SimpleDataFormat", 1, 2, 1 , {:arg_size=>0, :local_size=>1, :stack_max=>2}, "<compiled>", "<compiled>", nil, 1, :top, [], 0, []







最初の4つのフィールドは基本的にバイトコードを識別する魔法の値ですが、最後の3つのフィールドもメジャー、マイナー、フォーマットのバージョンです。 (これらは私が日本のブログで見つけたものと同じフィールドです。そして、これは明らかではありません。)



"YARVInstructionSequence/SimpleDataFormat", 1, 2, 1, {:arg_size=>0, :local_size=>1, :stack_max=>2} , "<compiled>", "<compiled>", nil, 1, :top, [], 0, []







5番目のフィールドは、このコードセクション用に作成されるスタックフレームのいくつかのパラメーターを含むハッシュです。 目的:arg_sizeおよび:stack_max、明らかに、と思います。



パラメーター:local_sizeは、理論的にはローカル変数の数を含む必要がありますが、実際には常に1だけ大きくなります。このユニットはコード(compile.c、342)にしっかりと組み込まれています。 最初はselfの値を保存すると思っていましたが、(論理的に考えると)スタックフレームにあります。



"YARVInstructionSequence/SimpleDataFormat", 1, 2, 1, {:arg_size=>0, :local_size=>1, :stack_max=>2}, "<compiled>", "<compiled>", nil, 1 , :top, [], 0, []







次の4つのフィールドには、メソッドの名前(または「メインのブロック」などの擬似名)が含まれています。 ダウンロードされた形式で定義されているファイルの名前(たとえば、「../ something」が必要な場合、このフィールドに「../something」が含まれるブロックが生成されます)。 ファイルへのフルパス(おそらくデバッガ用)と、対応するコードブロックの定義が始まる行。



"YARVInstructionSequence/SimpleDataFormat", 1, 2, 1, {:arg_size=>0, :local_size=>1, :stack_max=>2}, "<compiled>", "<compiled>", nil, 1, :top , [], 0, []







次のフィールドには、コードブロックのタイプが含まれます。 私は値を満たしました:top(トップレベル;メソッドまたはクラスのどちらにも埋め込まれていない「ジャスト」コード)、: block 、: methodおよび:class。



次の値は、Rubyソースコード(vm_core.h、552)で定義されています:top、method、class、block、finish、cfunc、proc、lambda、ifunc、eval。 それらのほとんどはバイトコードには含まれておらず、おそらく動的に割り当てられます。 たとえば、ifunc型のブロックは、渡されたブロックがC関数(vm_insnhelper.c、721)である場合にyieldで作成されます。 他の(cfuncを除く)の目的は現時点では明確ではありません。コードで判断して、ASTタイプのコンパイル中に明確に生成されるラムダタイプのブロックのみを書くことができますが、同時にそれらに会ったことはありません。 おそらく、これは最適化を意味します(まだ行っていません)。



"YARVInstructionSequence/SimpleDataFormat", 1, 2, 1, {:arg_size=>0, :local_size=>1, :stack_max=>2}, "<compiled>", "<compiled>", nil, 1, :top, [], 0 , []







次の2つのフィールドには、ローカル変数のリスト( [:local1, :local2]



ような文字の配列と引数の数。デフォルトの値を持つ引数、または* splatまたは&blockの形式の引数がある場合など) )最後までフォーマットがわからない配列があるかもしれません;関数の呼び出しについて書くとき、それを考慮します。



Bindingクラスを実装するには、実行時のローカル変数のリストがおそらく必要です。これがないと、たとえばREPLを実行できません。



"YARVInstructionSequence/SimpleDataFormat", 1, 2, 1, {:arg_size=>0, :local_size=>1, :stack_max=>2}, "<compiled>", "<compiled>", nil, 1, :top, [], 0, []







最後から2番目のフィールドはキャッチテーブルですが、これはまだ完全な謎のままです。 この神秘的な構造には、例外(catchおよびensure)に関連付けられた構造と、キャッチテーブルにエントリが存在するにもかかわらず、next、redo、retry、breakキーワードの最初の2つのキーワードの実装に何らかの方法で関連するエントリの両方があります、彼らはそれをまったく使用しません。



[

1,

[:trace, 1],

[:putnil],

[:putstring, "Hello, YARV!"],

[:send, :puts, 1, nil, 8, 0],

[:leave]

]







そして最後に、最後のフィールドはコードそのものです。



コードは、一連の命令を含む配列であり、行番号とラベルが散在しています。 要素が数値の場合、これは行番号です。文字の形式がlabel_01の場合、これは遷移が発生する可能性のあるラベルです。それ以外の場合は、命令である配列になります。



[:putstring, "Hello, YARV!"]





命令の最初の要素は常に命令の名前を含むシンボルであり、残りの要素は明らかにその引数です。



仮想マシンの一般的な原則と手順の詳細な説明は次のパートで行います。






1 Matzリファレンス実装

2これは、たとえばここで読むことができます

3 トランスレーターの設定には、 peepholetailcall 、さまざまなキャッシュ、特殊な命令など、約12の最適化があります。

4以下、RubyはRuby MRI 1.9.xを意味します。



PSそして、死の苦痛の下でさえ、私はブラについて一言も書きません...あなたは私が話していることを理解しています。



All Articles