Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyはプログラムをどのように解釈して実行するのか?トークン化からバイトコードまで徹底解説

使っているツールへの理解が深まるほど、開発者としての判断力は向上します。特にパフォーマンスの問題をデバッグする際には、Rubyがプログラムを実行するときに内部で何をしているのかを理解しておくと非常に役立ちます。

この記事では、ごくシンプルなプログラムがトークン化され、字句解析され、構文解析され、最後にバイトコードへコンパイルされるまでの道のりを追いかけていきます。Rubyが標準で提供しているツールを使いながら、インタプリタの動作を一段階ずつ覗き見てみましょう。

ご安心ください。エキスパートでなくても読み進められる内容です。技術マニュアルというよりも、ガイド付きツアーのような感覚でお楽しみください。

サンプルプログラムのご紹介

例として、単純な if/else 文を使用します。スペースを節約するために三項演算子で書いていますが、中身は普通の if/else です。

x > 100 ? 'foo' : 'bar'

後ほど見ていきますが、これほど単純なプログラムでも、処理の過程でかなりの量のデータへと変換されていきます。

注意: 本記事のコード例はすべて Ruby (MRI) 2.2 で動作確認したものです。他のRuby実装では動作しない場合があります。また、Ruby 3.x など最近のバージョンでは命令名や出力形式が一部異なることがありますので、あらかじめご了承ください。

トークン化(Tokenizing)

Rubyインタプリタがプログラムを実行できるようになるには、まず自由形式で書かれたプログラミング言語を、より構造化されたデータへ変換する必要があります。

最初のステップは、プログラムを小さな塊に分割することです。この塊のことをトークン(token)と呼びます。

# これはただの文字列
"x > 1"

# こちらはトークン
["x", ">", "1"]

Rubyの標準ライブラリには Ripper というモジュールが用意されており、Rubyインタプリタ本体とほぼ同じやり方でRubyコードを処理できます。

以下の例では、tokenize メソッドを使ってコードをトークン化しています。ご覧のとおり、トークンの配列が返されます。

require 'ripper'
Ripper.tokenize("x > 100 ? 'foo' : 'bar'")
# => ["x", " ", ">", " ", "1", " ", "?", " ", "'", "foo", "'", " ", ":", " ", "'", "bar", "'"]

トークナイザはかなり「単純」です。文法的に完全に不正なRubyコードを渡しても、問答無用でトークン化してしまいます。

# 不正なコード
Ripper.tokenize("1var @= /foobar`")
# => ["1", "var"]

字句解析(Lexing)

字句解析は、トークン化のさらに一歩先の段階です。ここでも文字列はトークンへ分割されますが、それぞれのトークンに追加情報が付けられます。

以下の例では、Ripperを使って先ほどの小さなプログラムを字句解析しています。ご覧のとおり、各トークンに識別子を示す :on_ident、演算子を示す :on_op、整数を示す :on_int といった種類のタグが付与されています。

require 'ripper'
require 'pp'

pp Ripper.lex("x > 100 ? 'foo' : 'bar'")

# [[[1, 0], :on_ident, "x"],
#  [[1, 1], :on_sp, " "],
#  [[1, 2], :on_op, ">"],
#  [[1, 3], :on_sp, " "],
#  [[1, 4], :on_int, "100"],
#  [[1, 5], :on_sp, " "],
#  [[1, 6], :on_op, "?"],
#  [[1, 7], :on_sp, " "],
#  [[1, 8], :on_tstring_beg, "'"],
#  [[1, 9], :on_tstring_content, "foo"],
#  [[1, 12], :on_tstring_end, "'"],
#  [[1, 13], :on_sp, " "],
#  [[1, 14], :on_op, ":"],
#  [[1, 15], :on_sp, " "],
#  [[1, 16], :on_tstring_beg, "'"],
#  [[1, 17], :on_tstring_content, "bar"],
#  [[1, 20], :on_tstring_end, "'"]]

各行の [1, 0] のような数値は、トークンの位置(行番号と桁位置)を示しています。ただし、この時点ではまだ本格的な構文チェックは行われていません。レキサーもまた、不正なコードを問題なく処理してしまうのです。

構文解析(Parsing)

Rubyがコードを扱いやすい塊へ分割できたら、いよいよ構文解析の開始です。

構文解析の段階で、Rubyはテキストを抽象構文木(AST:Abstract Syntax Tree)と呼ばれる構造へ変換します。抽象構文木とは、あなたのプログラムをメモリ上で表現したものです。

ひるがえって言えば、プログラミング言語とは本来、抽象構文木を人間にとってより書きやすい形で記述するための手段にすぎない、とも言えるでしょう。

require 'ripper'
require 'pp'

pp Ripper.sexp("x > 100 ? 'foo' : 'bar'")

# [:program,
#  [[:ifop,
#    [:binary, [:vcall, [:@ident, "x", [1, 0]]], :>, [:@int, "100", [1, 4]]],
#    [:string_literal, [:string_content, [:@tstring_content, "foo", [1, 11]]]],
#    [:string_literal, [:string_content, [:@tstring_content, "foobar", [1, 19]]]]]]]

この出力をそのまま読むのは容易ではありませんが、じっくり眺めていると、元のプログラムとの対応関係がなんとなく見えてくるはずです。

# プログラム全体を定義
[:program,
 # 「if」操作を実行
 [[:ifop,
   # 条件式を評価(x > 100)
   [:binary, [:vcall, [:@ident, "x", [1, 0]]], :>, [:@int, "100", [1, 4]]],
   # 条件が真なら "foo" を返す
   [:string_literal, [:string_content, [:@tstring_content, "foo", [1, 11]]]],
   # 条件が偽なら "bar" を返す
   [:string_literal, [:string_content, [:@tstring_content, "foobar", [1, 19]]]]]]]

この時点で、Rubyインタプリタはあなたが何をしてほしいのかを正確に把握しています。このままでもプログラムを実行できます。実際、Ruby 1.9より前のバージョンでは、まさにこの方式で実行されていました。しかし現在のRubyでは、もう一段階の処理が挟まれています。

バイトコードへのコンパイル

現在のRubyは、抽象構文木を直接たどって実行するのではなく、まずASTをより低レベルなバイトコードへコンパイルします。

そして、このバイトコードがRuby仮想マシンによって実行されるのです。

RubyVM::InstructionSequence クラスを使えば、仮想マシンの内部動作を覗き見ることができます。以下の例では、サンプルプログラムをコンパイルし、さらに逆アセンブルして人間が読める形式にしています。

puts RubyVM::InstructionSequence.compile("x > 100 ? 'foo' : 'bar'").disassemble
# == disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
# 0000 trace            1                                               (   1)
# 0002 putself
# 0003 opt_send_without_block <callinfo!mid:x, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0005 putobject        100
# 0007 opt_gt           <callinfo!mid:>, argc:1, ARGS_SIMPLE>
# 0009 branchunless     15
# 0011 putstring        "foo"
# 0013 leave
# 0014 pop
# 0015 putstring        "bar"
# 0017 leave

おっと!急にアセンブリ言語っぽくなりましたね。一つずつ丁寧に読み解いていきましょう。

# self に対してメソッド x を呼び出し、結果をスタックに積む
0002 putself
0003 opt_send_without_block <callinfo!mid:x, argc:0, FCALL|VCALL|ARGS_SIMPLE>

# 数値 100 をスタックに積む
0005 putobject        100

# 比較を実行(x > 100)
0007 opt_gt           <callinfo!mid:>, argc:1, ARGS_SIMPLE>

# 比較結果が偽なら、位置15へジャンプ
0009 branchunless     15

# 比較結果が真だった場合、"foo" を返す
0011 putstring        "foo"
0013 leave
0014 pop

# ここが位置15。比較が偽だった場合はここへジャンプして "bar" を返す
0015 putstring        "bar"
0017 leave

こうして生成された命令列を、Ruby仮想マシン(YARV)が先頭から順番に実行していきます。以上が、Rubyがプログラムを動かすまでの全行程です!

まとめ

以上、とても簡略化された、少し漫画的なRubyインタプリタのツアーでした。ここで紹介したツールを活用すれば、「Rubyが自分のプログラムをどう解釈しているのか」という推測作業の大部分を、確かな事実に置き換えることができます。ASTほど具体的な情報はないのですから。

次に奇妙なパフォーマンスの問題に行き詰まったときは、ぜひバイトコードを覗いてみてください。直接的な解決には至らないかもしれませんが、視点が変わり、思わぬヒントが得られることもあるでしょう。:)

  1. キーロガーの検出と削除方法を徹底解説|感染の兆候と対策ガイド

    キーロガーとは? キーロガーは、キーボードへの入力内容を記録し、その情報を攻撃者へ送信するスパイウェアの一種です。パスワード、アカウント情報、メール、検索ワード、個人情報など、入力したすべてのデータが記録されてしまいます。 キーロガーの目的はただ一つ、PCでのキーストロークやスマートフォン・タブレットでのタップ操作を記録することです。匿名ブラウザを使っていても、キーロガーはデバイス自体にインストールされているため、入力内容を追跡できてしまいます。 キーロガーの検出方法 PCにキーロガーが潜んでいる兆候はシンプルです。ブラウザの動作が遅い、マウスやキーボードの操作にラグがある、カーソルが勝手に

  2. PCの不要なプログラムを完全にアンインストールする6つの方法

    整理整頓されたクリーンなPCは、散らかったPCよりも常に快適です。高いパフォーマンスとセキュリティを維持するためには、PCを常に最適な状態に保つことが大切です。不要なプログラムが溜まると動作が重くなり、ハードディスクの容量も圧迫されてしまいます。 そのため、定期的なお掃除は必須ですが、実は簡単な作業ではありません。不要なプログラムをアンインストールしようとしても、痕跡が残ってしまったり、さまざまな理由で削除できないことも少なくありません。 そんなときはどうすればいいのでしょうか?この記事では、不要なプログラムを完全にアンインストールするためのさまざまな方法をご紹介します。 最も一般的で手軽な