Rubyがコードをどのように解釈するかを見てみましょう
新しいRubyMagicの記事へようこそ!今回は、Rubyがコードをどのように解釈するか、そしてこの知識をどのように活用できるかを見ていきます。この投稿は、コードがどのように解釈されるか、そしてこれがどのようにコードの高速化につながるかを理解するのに役立ちます。
シンボル間の微妙な違い
Rubyでの文字のエスケープに関する以前のRubyMagicの記事には、改行のエスケープに関する例がありました。
以下の例では、プラスの+
を使用して、2つの文字列を複数の行にまたがる1つの文字列として組み合わせる方法を示しています。 記号またはバックスラッシュ付き\
。
"foo" +
"bar"
=> "foobar"
# versus
"foo" \
"bar"
=> "foobar"
これらの2つの例は似ているように見えますが、動作はまったく異なります。これらの読み取り方法と解釈方法の違いを知るには、通常、Rubyインタープリターの要点を知る必要があります。または、Rubyに違いを尋ねることもできます。
InstructionSequence
RubyVM::InstructionSequence
の使用 クラスRubyに、与えたコードをどのように解釈するかを尋ねることができます。このクラスは、Rubyの内部を垣間見るために使用できるツールセットを提供します。
以下の例で返されるのは、YARVインタープリターによって理解されるRubyコードです。
YARVインタープリター
YARV(Yet Another Ruby VM)は、Rubyバージョン1.9で導入されたRubyインタープリターであり、元のインタープリターであるMRI(MatzのRubyインタープリター)に代わるものです。
インタプリタを使用する言語は、中間のコンパイル手順なしでコードを直接実行します。これは、Rubyが最初にプログラムを最適化された機械語プログラムにコンパイルしないことを意味します。このプログラムはC、Rust、Goなどの言語をコンパイルします。
Rubyでは、プログラムは最初にRuby VMの命令セットに変換され、その後すぐに実行されます。これらの手順は、RubyコードとRubyVMで実行されているコードの中間ステップです。
これらの手順により、構文固有の解釈を処理しなくても、RubyVMがRubyコードを理解しやすくなります。これは、これらの命令を作成するときに処理されます。命令シーケンスは、解釈されたコードを表す最適化された操作です。
Rubyプログラムの通常の実行中は、これらの命令は表示されませんが、それらを表示することで、Rubyがコードを正しく解釈したかどうかを確認できます。 InstructionSequence
を使用 YARVが実行する前に、どのような種類の命令を作成するかを確認することができます。
Rubyインタープリターを構成するすべてのYARV命令を理解する必要はありません。ほとんどのコマンドはそれ自体で話します。
"foo" +
"bar"
RubyVM::InstructionSequence.compile('"foo" + "bar"').to_a
# ... [:putstring, "foo"], [:putstring, "bar"] ...
# versus
"foo" \
"bar"
RubyVM::InstructionSequence.compile('"foo" "bar"').to_a
# ... [:putstring, "foobar"] ...
実際の出力には、後で説明するもう少しセットアップコマンドが含まれていますが、ここでは"foo" + "bar"
の実際の違いを確認できます。 および"foo" "bar"
。
前者は2つの文字列を作成し、それらを結合します。後者は1つの文字列を作成します。これは、"foo" "bar"
を使用することを意味します "foo" + "bar"
で3つではなく、1つの文字列のみを作成します 。
1 2 3
↓ ↓ ↓
"foo" + "bar" # => "foobar"
もちろん、これは私たちが使用できる最も基本的な例ですが、Ruby言語の細部がどのように大きな影響を与える可能性があるかについての良いユースケースを示しています:
- より多くの割り当て:すべてのStringオブジェクトが個別に割り当てられます。
- より多くのメモリ使用量:割り当てられたすべてのStringオブジェクトがメモリを消費します。
- ガベージコレクションが長くなる:すべてのオブジェクトは、短命であっても、ガベージコレクターによるクリーニングに時間がかかります。割り当てが多いほど、ガベージコレクション時間が長くなります。
分解
もう1つの使用例は、ロジックの問題のデバッグです。以下は簡単な間違いであり、大きな影響を与える可能性があります。違いを見つけることができますか?
1 + 2 * 3
# versus
(1 + 2) * 3
Rubyを使用して、この少し複雑な例の違いを見つけることができます。
このコード例を分解することで、Rubyに実行中のコマンドのより読みやすい表を出力させることができます。
1 + 2 * 3
# => 7
puts RubyVM::InstructionSequence.compile("1 + 2 * 3").disasm
# == disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
# 0000 trace 1 ( 1)
# 0002 putobject_OP_INT2FIX_O_1_C_
# 0003 putobject 2
# 0005 putobject 3
# 0007 opt_mult <callinfo!mid:*, argc:1, ARGS_SIMPLE>
# 0009 opt_plus <callinfo!mid:+, argc:1, ARGS_SIMPLE>
# 0011 leave
# versus
(1 + 2) * 3
# => 9
puts RubyVM::InstructionSequence.compile("(1 + 2) * 3").disasm
# == disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
# 0000 trace 1 ( 1)
# 0002 putobject_OP_INT2FIX_O_1_C_
# 0003 putobject 2
# 0005 opt_plus <callinfo!mid:+, argc:1, ARGS_SIMPLE>
# 0007 putobject 3
# 0009 opt_mult <callinfo!mid:*, argc:1, ARGS_SIMPLE>
# 0011 leave
上記の例は、YARV命令の数に少し関係がありますが、印刷と実行の順序から、括弧のペアによって違いが生じることがわかります。
1 + 2
を括弧で囲みます 数学の演算の順序を上に移動して、加算が最初に実行されるようにします。
逆アセンブリ出力自体に括弧が実際に表示されるのではなく、コードの残りの部分に影響するだけであることに注意してください。
分解
逆アセンブリ出力は、すぐには理解できない可能性のある多くのことを出力します。
印刷される表形式では、すべての行が操作番号で始まります。その後、操作について説明し、最後に操作の引数について説明します。
これまでに見た操作の小さなサンプル:
trace
-トレースを開始します。詳細については、TracePointのドキュメントを参照してください。-
putobject
-スタック上のオブジェクトをプッシュします。 -
putobject_OP_INT2FIX_O_1_C_
-整数1
をプッシュします スタック上。最適化された操作。 (0
および1
最適化されています。) -
putstring
-スタックに文字列をプッシュします。 -
opt_plus
-加算演算(内部的に最適化)。 -
opt_mult
-乗算演算(内部的に最適化)。 leave
-現在のコードコンテキストをそのままにします。
Rubyインタープリターが開発者にとって使いやすく読みやすいRubyコードをYARV命令に変換する方法がわかったので、これを使用してアプリケーションを最適化できます。
メソッド全体、さらにはファイル全体をRubyVM::InstructionSequence
に渡すことができます。 。
puts RubyVM::InstructionSequence.disasm(method(:foo))
puts RubyVM::InstructionSequence.compile_file("/tmp/hello.rb").disasm
あるコードが機能する理由と、別のコードが機能しない理由を調べてください。特定のシンボルがコードの動作を他のシンボルとは異なるものにする理由を学びます。悪魔は詳細に宿っています。アプリ内でRubyコードがどのように動作しているか、そして何らかの方法で最適化できるかどうかを知っておくとよいでしょう。
最適化
インタープリターレベルでコードを表示して最適化する以外に、InstructionSequence
を使用できます。 コードをさらに最適化するため。
InstructionSequence
を使用 、Rubyの組み込みパフォーマンス最適化を使用して特定の命令を最適化することが可能です。利用可能な最適化の完全なリストは、RubyVM::InstructionSequence.compile_option =
にあります。 メソッドのドキュメント。
これらの最適化の1つは、末尾呼び出しの最適化です。 。
RubyVM::InstructionSequence.compile
メソッドは、この最適化を有効にするオプションを次のように受け入れます。
some_code = <<-EOS
def fact(n, acc=1)
return acc if n <= 1
fact(n-1, n*acc)
end
EOS
puts RubyVM::InstructionSequence.compile(some_code, nil, nil, nil, tailcall_optimization: true, trace_instruction: false).disasm
RubyVM::InstructionSequence.compile(some_code, nil, nil, nil, tailcall_optimization: true, trace_instruction: false).eval
RubyVM::InstructionSequence.compile_option =
を使用して、すべてのコードに対してこの最適化をオンにすることもできます。 。他のコードの前にこれをロードしてください。
RubyVM::InstructionSequence.compile_option = {
tailcall_optimization: true,
trace_instruction: false
}
Rubyでの末尾呼び出しの最適化の仕組みの詳細については、次の記事を確認してください:Rubyでの末尾呼び出しの最適化とRubyでの末尾呼び出しの最適化:背景。
結論
RubyがRubyVM::InstructionSequence
を使用してコードを解釈する方法の詳細 コードが実際に何をしているのかを確認して、パフォーマンスを向上させることができます。
このInstructionSequenceの紹介は、Rubyが内部でどのように機能するかを学ぶための楽しい方法かもしれません。知るか? Rubyのコード自体の一部に取り組むことに興味があるかもしれません。
これで、Rubyでのコードコンパイルの簡単な紹介は終わりです。この記事がどのように気に入ったか、質問がある場合、次に何を読みたいかを知りたいので、@AppSignalまでお知らせください。
-
Rubyメソッドをスパイする方法
Rubyには、TracePointを使用してアクセスできるトレースシステムが組み込まれています。 クラス。トレースできるものには、メソッド呼び出し、新しいスレッド、および例外があります。 なぜこれを使いたいのですか? さて、特定のメソッドの実行を追跡したい場合に便利です。他にどのようなメソッドが呼び出されているか、および戻り値は何かを確認できます。 いくつかの例を見てみましょう! メソッド呼び出しのトレース ほとんどの場合、TracePointが必要になります 組み込みメソッド(プット、サイズなど)ではなくアプリケーションコードをトレースするため。 これは、callを使用して行うこと
-
Rubyプログラムをデバッグおよび修正する方法
あなたのプログラムは、最初にあなたが望むことを正確に実行する頻度はどれくらいですか? 多くの場合、プログラムは期待どおりに機能しないため、ルビーのデバッグの技術を使用する必要があります。 理由を見つけるのに役立ちます。 次のエラーメッセージに精通している可能性があります。 undefined method some_method for nil:NilClass これは、nil値がコードへの道を見つけることができたことを意味します。 この記事で説明する手法を使用して、この問題や同様の問題に対処する方法を学びます。 エラーとスタックトレースを理解する Rubyインタープリターからエラー