• はじめに

本ツール CastOff は Ruby1.9.3 用のコンパイラです。 CastOff は、ユーザが与えた変数のクラス情報などを解釈し、 指定した Ruby のメソッドを C 拡張形式のメソッドへと変換します。 C 拡張に変換することで、Ruby の仮想マシンのオーバヘッドを削減し、 対象となるメソッドを高速化することができます。 本 README では、CastOff の基本機能について紹介します。 ご質問、ご要望等があれば、[email protected] もしくは github.com/soba1104/CastOff/issues まで、ご連絡ください。

  • ライセンス

Ruby 処理系のライセンスに従います。

  • インストール方法

$gem install cast_off

CastOff は Ruby 処理系に一切手をくわえることなく、Ruby 処理系の拡張ライブラリとして実装しています。 このため、gem コマンド経由で容易にインストールすることができます。 ただし、脱最適化などの処理が処理系の実装依存になっているため、Ruby 1.9.3 のみの対応となっています。 一応 Ruby 1.9.2 でも動作しますが、Ruby 1.9.3 よりも不安定かもしれません。 Ruby1.9.2 と Ruby1.9.3 以外のバージョンの Ruby 処理系では動作しないため、ご注意ください。

  • CastOff の利用における注意点

** 利用すると非互換になる機能 現状、CastOff を用いた場合、Ruby 処理系との非互換性が生じる可能性があります。 ここでは、現時点で把握できている、Ruby 処理系との非互換が生じる機能、 特にコンパイルエラーを出すことが出来ない機能について列挙します。 ここで列挙した機能を使用するプログラムやメソッドに対しては、CastOff を利用するべきではありません。 ここに列挙した機能以外で非互換になる点を発見された方は、 お手数ですが、[email protected] もしくは github.com/soba1104/CastOff/issues まで、ご報告をお願い申し上げます。

-継続(Continuation) Ruby の継続(Continuation) を用いた場合、実行コンテキストの保存に失敗することが確認できています。 継続を使用するプログラムでは、CastOff を利用しないでください。

-定数の再定義 CastOff は、コンパイルしたコードの読み込み時、もしくは最初に実行したときに定数解決を行います。 性能上の理由により、CastOff は定数解決後は常に同じ値を使いまわすため、定数の再定義に対応することができません。 定数の上書きを行うようなプログラムでは、CastOff を利用しないでください。 定数の上書きのフック、もしくは RubyVM が管理する世代番号のチェックが高速にできるようになった場合、 この非互換性は解消する予定です。

-メソッドや Proc のパラメータ取得 コンパイル済みメソッドや Proc に対する情報を取得しようとした場合、 コンパイル前と異なる挙動を示すメソッドがいくつかあります。 例えば、Method#arity や Proc#arity などの、メソッドや Proc の引数に 関する情報を取得しようとした場合、コンパイル前とコンパイル後では異なる値になります。

** 起動時間 CastOff は Ruby で実装しているので、CastOff を用いると、CastOff の読み込みというオーバヘッドが発生します。 このため、実行時間が短いプログラムに対してCastOff を利用すると、逆に遅くなる可能性があります。

また、CastOff を用いた場合、通常の require などの処理に加えて、CastOff がコンパイルしたメソッドの数だけ、コンパイル済みコードの読み込み、 および、メソッド上書きのフックなどの処理(以降、初期化処理と呼ぶ)が走ります。 コマンドラインから CastOff を利用した場合、閾値の設定次第では数多くのメソッドがコンパイルされるため、初期化処理に時間がかかります。 このため、ボトルネックとなるメソッドが分かっている場合は、スクリプトから CastOff を利用し、 コンパイル対象のメソッドを直接指定して利用した方が高速に動作します。

** コンパイル時間 CastOff は Ruby で実装しており、実装コストの兼ね合いから、コンパイル時間には配慮していません。 このため、CastOff によるコンパイルは非常に時間がかかります。 起動時間に関する注意でも述べたように、コマンドラインから CastOff を利用した場合、 閾値の設定次第では数多くのメソッドがコンパイルされます。 閾値を低く設定しすぎると、コンパイルに非常に時間がかかるという点に、ご注意ください。

また、コマンドラインから CastOff を利用する場合、対象プログラムを実行し、プロファイル情報を取得する必要があります。 このため、コンパイル対象の実行に数時間かかるようなプログラムに対しては、コマンドライン経由で CastOff を利用するべきではありません。 このようなプログラムに対しては、実行に時間がかからないような入力を与えるか、スクリプトから CastOff を利用してください。

** コンパイル済みコードの読み込み CastOff がコンパイル済みコードを読み込むためには、コンパイルしたメソッドが定義されている必要があります。 メソッドを定義し終わるまでは、コンパイル済みコードを読み込むことはできません。

CastOff をコマンドラインから利用した場合、CastOff は、クラス定義とメソッド定義をフックし、 コンパイルしたメソッドが定義済みかどうかを確認します。 そして、コンパイルしたメソッドが定義済みだった場合、コンパイル済みコードを読み込みます。 メソッド定義のフックには、BasicObject.method_added を使用しています。 コンパイル対象のスクリプト内で method_added を使用している場合、 (特に Ruby 1.9.2 では)コンパイル済みコードが正常に読み込まれない可能性があります。

** 特異メソッドの取り扱い CastOff は、インスタンスメソッドと特異メソッド両方のコンパイルを扱えますが、 基本的にインスタンスメソッドのコンパイルの方が得意です。 なるべく特異メソッドに対しては CastOff を使用しないことをおすすめします。

得意メソッドの取り扱いが苦手なのは、特異クラスが Marshal.dump に対応していないためです。 CastOff は、コンパイル時に扱った情報を Marshal.dump で保存し、ロード時に Marshal.load で読み込みます。 特異クラスは Marshal で取り扱うことができないため、特異クラスの情報は、 無かったものとして扱うことになります。このため、特異メソッドのコンパイルでは、 self に関する型情報を扱うことができず、(特異メソッドの self が内部的に参照するクラスは特異クラスであるため) インスタンスメソッドのコンパイルと比較して遅くなってしまいます。

例えば、次の2つのスクリプト A, B を CastOff でコンパイルし、実行時間を比較した場合、 32bit GNU/Linux GCC4.4.1 の環境では、A の方が B よりも約3.5倍高速です。


# スクリプト A class Fib

def fib n
  (n < 3) ? 1 : (fib(n-1) + fib(n-2))
end

end Fib.new.fib(40)



# スクリプト B def fib n

(n < 3) ? 1 : (fib(n-1) + fib(n-2))

end fib(40)


  • CastOff の利用方法

CastOff は Ruby のメソッドを C に変換し、高速化を実現するためのツールです。 CastOff の利用方法には、コマンドラインからの利用とスクリプトからの利用の2種類があります。 ここでは、これら2種類の利用方法について簡単に説明します。

** コマンドラインからの利用

*** コマンドラインからの利用の流れ CastOff が提供するコマンドラインツール cast_off では、 引数に高速化したいプログラムを指定することで、容易にコンパイルを行うことができます。 詳しくは下で解説しますが、コマンドラインからの CastOff の利用は、 次のようなコマンドを繰り返し実行することで行います。


$cast_off コンパイル対象のスクリプト コンパイル対象に与える引数の例


例えば、次のようなスクリプトの実行を高速化したい場合、

$ruby foo.rb bar

次のように実行することで、foo.rb と、使用するライブラリをコンパイルすることができます。 (この例での bar は、foo.rb に対する引数です)

$cast_off foo.rb bar

コマンドラインからのコンパイルは、次の2つの手順で行います。 1:コンパイル対象のメソッドを決定 2:コンパイル対象のプロファイル情報を収集し、コンパイル この2つのステップそれぞれで対象プログラムを実行します。 このため、上の例の場合、foo.rb を、コンパイルのために2度実行します。 実行の回数は、今後、1-2の手順をまとめることで、削減する予定です。

コンパイルした後は、–run オプションを用いることで、コンパイルしたコードを読み込んで動作させることができます。 上の例の場合、次のようなコマンドを実行することで、コンパイル済みコードを用いて foo.rb を実行することができます。

$cast_off –run foo.rb bar

また、コマンドラインからの利用で注意する必要があるのは、コンパイル時とは異なる引数を用いて実行する場合の挙動です。 上の例では、コンパイル時に bar という引数を与えています。 CastOff は、bar を引数として foo.rb を実行したときのプロファイル情報を基にコンパイルを行うため、 異なる引数を与えた場合、クラス情報の不整合を起こし、例外、もしくは脱最適化を発生させる可能性があります。

例えば、foo.rb が下の例のようなプログラムだった場合、bar という引数がわたってきた場合は String オブジェクトが、 baz という引数がわたってきた場合は Symbol オブジェクトが、sample メソッドに渡されます。


# 例) foo.rb def sample(o)

puts o.inspect

end case ARGV.shift when ‘bar’

sample('bar')

when ‘baz’

sample(:baz)

end


ここで、cast_off foo.rb bar として foo.rb をコンパイルすると、CastOff は、プロファイル情報から、 sample メソッドに渡されるオブジェクトは String オブジェクトのみであるという判断を下します。 そして、sample メソッドに渡されるオブジェクトが String オブジェクトであるという前提に基づいたコードを生成します。 このため、foo.rb に baz という引数を渡した場合、sample メソッドに対し、想定していなかった Symbol オブジェクトが 渡ってきてしまい、コンパイルの前提条件が崩れてしまいます。 CastOff は、コンパイルの前提条件が崩れたことを検出したときに、例外の発生、もしくは脱最適化を行います。 例外を発生させた場合は実行を継続させることができず、脱最適化を行った場合はパフォーマンスに対するペナルティが発生してしまいます。

このような場合には、

$cast_off foo.rb bar

とした次に、

$cast_off foo.rb baz

としてください。 このようにすることで、引数を bar とした場合、引数を baz とした場合の両方のプロファイル情報を用いてコンパイルを行うことができます。 プロファイル情報やコンパイル結果を削除したい場合は、–clear という引数を与えて実行してください。

***コマンドライン引数 cast_off [options] [programfile] [arguments]

***オプション一覧 –run コンパイル済みコードを用いて、対象プログラムを実行します。

–deoptimize 脱最適化を有効にします。

–clear プロファイル結果やコンパイル結果を削除します。 name オプションによって foo と名づけたコンパイル結果を削除する場合、次のコマンドを使用してください。 $cast_off –clear –name=foo

–threshold=COUNT COUNT 回以上実行されたメソッドをコンパイルするよう、閾値を設定します。 COUNT のデフォルト値は100です。

–name=NAME コンパイル結果に NAME という名前をつけます。 ここでつけた名前は、コンパイル済みコードが既に存在するかどうかの確認に使用します。 コンパイル済みコードが見つかり、コンパイル対象やコンパイルオプション(スクリプトからの利用を参照) に変更が無かった場合、CastOff はコンパイル済みコードを再利用します。 name オプションを使用しなかった場合、CastOff は File.basename() の結果を名前として使用します。

–verbose コンパイルの進捗とコンパイル結果に関する内部情報を表示します。

-h, –help ヘルプメッセージを表示します。

–version CastOff のバージョンを表示します。

** スクリプトからの利用

*** スクリプトからの利用の流れ CastOff をスクリプトから用いる場合、コンパイルのタイミングやコンパイル対象のメソッド、コンパイルオプションを、 CastOff に対して直接指定することができます。 詳しくは下で解説しますが、スクリプトからの CastOff の利用は、次のようなメソッドを用いて行います。


CastOff.compile(クラス,メソッド名,binding,クラス情報)


例えば、次のようなメソッドの実行を高速化したい場合、


class Tarai

def tarai( x, y, z )
  if x <= y
  then y
  else tarai(tarai(x-1, y, z),
             tarai(y-1, z, x),
             tarai(z-1, x, y))
  end
end

end


tarai メソッドを定義した後に、次のように記述することで、 tarai メソッドを Ruby の C 拡張にコンパイルし、 コンパイル済みの C 拡張に tarai メソッドを置き換えることができます。


CastOff.compile(Tarai, :tarai, [:x, :y, :z] => Fixnum, Fixnum => => Fixnum)


ここでの CastOff.compile に対する引数はそれぞれ次のような意味をもっています。 第一引数: コンパイルしたいメソッドが Tarai クラスに定義されていることを指定しています。 第二引数: コンパイルしたいメソッドの名前が tarai であることを指定しています。 第三引数: コンパイル対象である Tarai#tarai メソッド内のクラス情報を指定しています。

[:x, :y, :z] => Fixnum は、ローカル変数 x, y, z が、常に Fixnum オブジェクトであることを指定しています。
Fixnum => {:- => Fixnum} は、Fixnum#- が、常に Fixnum オブジェクトを返すことを指定しています。
ローカル変数 x, y, z や Fixnum#- の返り値は Bignum オブジェクトであることも想定できますが、
たらい回し関数にそのような入力をあたえることはまず無いと判断し、常に Fixnum であることを指定しています。
このように、第三引数では、想定する用途において渡ってくるオブジェクトのみを指定します。

このように、CastOff.compile などの、CastOff へのコンパイル指定を行うメソッドを用いることで、 ボトルネックとなっているメソッドをコンパイルしていくことができます。 コマンドラインからの利用よりも、コンパイル対象やコンパイルの条件をより細かく指定できるのが、スクリプトからの利用の利点です。

コンパイルして得た C 拡張のコードは、コンパイルのための条件や、 コンパイル対象のメソッドを定義したファイルに変更が加えられていなかった場合に、自動的に再利用されます。 このため、1度コンパイルが済んだ後では、コンパイルにかかる時間を気にする必要はありません。

CastOff をスクリプトから利用するうえで強くおすすめするのは、CastOff.compile の呼び出しを、高速化したいプログラムとは別ファイルに記述するという点です。 このようにした方が、CastOff への依存を簡単に無くすことができ、CastOff によってトラブルが発生した場合の対処が容易になります。 また、コンパイル済みの C 拡張コードを再利用するためには、コンパイル対象のファイルに変更が加えられていない必要があるため、 コンパイル済みコードの再利用が容易になります。 例えば、高速化したいプログラムに対しては、次のように、patch.rb の require 文のみを追記してください。


# 高速化したいプログラム

# 一通りのメソッド定義 class Foo

def foo
  ...
end

end

class Bar

def bar
  ...
end

end …

require ‘patch’ # この行だけ追記


そして、patch.rb の中から、CastOff.compile を呼び出してください。


# patch.rb

require ‘cast_off’

class Foo

CastOff.compile(self, :foo, binding, クラス情報)

end

class Bar

CastOff.compile(self, :bar, binding, クラス情報)

end


このようにすることで、require ‘patch’ の1行を削除するだけで、容易に CastOff への依存を解消することができます。 また、Foo#foo のコンパイルに用いるクラス情報を更新しても、Bar#bar の再コンパイルが走ることが無くなります。

***クラス情報の指定 CastOff に対するクラス情報の指定は、次のように行います。

-変数のクラス情報の指定: –ローカル変数、引数 :ローカル変数名 => クラス :ローカル変数名 => [クラス1, クラス2, …]

:ローカル変数名1, :ローカル変数名2, …

> クラス

:ローカル変数名1, :ローカル変数名2, …

> [クラス1, クラス2, …]

–インスタンス変数 :@インスタンス変数名 => クラス :@インスタンス変数名 => [クラス1, クラス2, …]

:@インスタンス変数名1, :@インスタンス変数名2, …

> クラス

:@インスタンス変数名1, :@インスタンス変数名2, …

> [クラス1, クラス2, …]

–クラス変数 :@@クラス変数名 => クラス :@@クラス変数名 => [クラス1, クラス2, …]

:@@クラス変数名1, :@@クラス変数名2, …

> クラス

:@@クラス変数名1, :@@クラス変数名2, …

> [クラス1, クラス2, …]

–グローバル変数 :$グローバル変数名 => クラス :$グローバル変数名 => [クラス1, クラス2, …]

:$グローバル変数名1, :$グローバル変数名2, …

> クラス

:$グローバル変数名1, :$グローバル変数名2, …

> [クラス1, クラス2, …]

-メソッドの返り値のクラス情報の指定: クラス => => 返り値のクラス, :メソッド名2 => [返り値のクラス1, 返り値のクラス2, …], …

-定数のクラス情報の指定: 定数のクラス情報の指定は、変数やメソッドの返り値とは異なります。 定数のクラス情報の指定は、定数解決に使用できる Binding オブジェクトを渡すことで行います。

例えば、次のような Sample#sample メソッドをコンパイルする場合、


COUNT = 1000000 class Sample

def sample()
  COUNT.times do
    ...
  end
end

end


次のように指定することで、COUNT という定数が Fixnum オブジェクトであることを指定することができます。


class Sample

CastOff.compile(self, :sample, binding, クラス情報)

end


上のように、CastOff.compile の引数に Binding オブジェクトを渡すと、CastOff はコンパイル時に、 与えられた Binding オブジェクトを用いて定数を参照し、定数のクラス情報を解決します。 ここで与える Binding オブジェクトは定数解決にのみ使用するため、 定数の解決に使用できる Binding オブジェクトならば、どのようなものを渡してもかまいません。 上の例では、CastOff.compile の呼び出しを、Sample クラスのクラス定義文の中で行うことで、 Sample#sample メソッドと同様の定数スコープを構築し、CastOff に渡しています。

Ruby では、クラス定義文を好きな場所で呼び出し、既存のクラスの定数スコープを得ることができます。 このため、Sample#sample メソッドを定義した箇所と、CastOff.compile を呼び出す箇所は、別ファイルでもかまいません。 CastOff への依存を簡単に無くすためにも、CastOff.compile の呼び出しは、高速化したいプログラムとは別ファイルに まとめて記述したほうが使いやすいと思います。これについては、「スクリプトからの利用の流れ」を参照してください。

*** コンパイルの指定 ここでは、CastOff にコンパイルを指定するための方法を簡単に解説します。 クラス情報の指定方法については、上記のスクリプトからの利用の流れを参照してください。

-Ruby のインスタンスメソッドのコンパイル指定 –指定方法 CastOff.compile(クラス or モジュール, メソッド名, binding) CastOff.compile(クラス or モジュール, メソッド名, binding, クラス情報) CastOff.compile(クラス or モジュール, メソッド名, クラス情報) –備考 Ruby のメソッドをコンパイルし、C 拡張に変換、対象のメソッドを上書きします。 メソッド名は Symbol オブジェクトで指定してください。 第三引数で与える Binding オブジェクトは、定数の解決に使用します。

-Ruby の特異メソッドのコンパイル指定 –指定方法 CastOff.compile_singleton_method(オブジェクト, メソッド名, binding) CastOff.compile_singleton_method(オブジェクト, メソッド名, binding, クラス情報) CastOff.compile_singleton_method(オブジェクト, メソッド名, クラス情報) –備考 特異メソッドのコンパイルは、CastOff.compile ではなくこちらを使用してください。 CastOff.compile と同様に、対象のメソッドを上書きします。 メソッド名は Symbol オブジェクトで指定してください。 第三引数で与える Binding オブジェクトは、定数の解決に使用します。

-Ruby プログラムの一部をコンパイルし、実行させる指定 –指定方法 CastOff.execute(クラス情報) { ブロック } –備考 与えられたブロックをコンパイルし、実行します。

*** コンパイルオプションに関する指定 CastOff をスクリプトから用いる場合、コンパイルの条件(コンパイルオプション)について様々な指定を行うことができます。 ここでは、CastOff に対して指定できるコンパイルオプションについて簡単に解説します。

-実行時にクラス情報のチェックを行うガードの有無 –指定方法 CastOff.inject_guard(true or false) —デフォルト値 true —備考 クラス情報に関するガードを挿入する(true)かしない(false)かを設定します。 クラス情報に関するガードとは、プログラマが手動で与えたクラス情報や、 プロファイルによって得たクラス情報が正しいかどうかを、実行時に検査するためのものです。 ガードでの検査に失敗した場合のデフォルトの挙動は例外ですが、 CastOff.deoptimize(true) とすることで、脱最適化を有効にすることができます。

–脱最適化 —指定方法 CastOff.deoptimize(true or false) —デフォルト値 false —備考 ガードでの検査に失敗した場合に、脱最適化を行う(true)か、例外を発生させる(false)かを設定します。 脱最適化を有効にすることで、ガードでの検査失敗時にも実行を継続させることができます。 しかし、脱最適化に必要な情報を保持する必要があり、CastOff による高速化の度合いが低下します。

–コンパイル条件のチェックのスキップ —指定方法 CastOff.skip_configuration_check(true or false) —デフォルト値 false —備考 コンパイル済みコードを再利用していいかどうかを判定するときに、 コンパイル条件(コンパイルオプションや CastOff に与えるクラス情報など) のチェックをスキップする(true)かスキップしない(false)かを設定します。

CastOff は、デフォルトでは、コンパイル済みコードを再利用していいかどうかを判定するときに、 指定されたコンパイル条件が、コンパイル済みのものと現在与えられているもので同一かどうかの判定を行います。 このとき、コンパイル条件が異なっていた場合は再度コンパイルを行います。 CastOff.skip_configuration_check(true) とすると、このコンパイル条件のチェックをスキップします。

コンパイル条件のチェックには Marshal.load などの実行コストの高い処理を複数回呼び出すため、時間がかかります。 CastOff に与えるコンパイル条件が決定した場合は、このオプションを使用して、 コンパイル条件のチェックをスキップし、ロード時間を短縮することをおすすめします。

–コンパイル済みコードの再利用 —指定方法 CastOff.reuse_compiled_code(true or false) —デフォルト値 true —備考 コンパイル済みコードの再利用をする(true)かしない(false)かを設定します。 再コンパイルを強制したい場合は、false に設定してください。

–定数の先読み —指定方法 CastOff.prefetch_constant() —デフォルト値 true —備考 定数の先読みを行う(true)か行わない(false)かを設定します。 定数の先読みを行わせるためには、Binding オブジェクトを、コンパイル時に与える必要があります。 定数の先読みを指定した場合、CastOff は、定数の解決をロード時、もしくは最初に実行されたときに行います。 そして、先読みを行った定数を常に使用します。 このため、先読みを行った場合、定数を上書きした場合も、上書き前の値を使用してしまいます。 定数の上書きを使用するプログラムをコンパイルする場合、定数の先読みをオフにしてください。 ただし、定数の先読みをオフにした場合、定数の参照に rb_const_get を複数回使用するため、 定数参照がボトルネックになる可能性があります。

–コンパイル経過や CastOff の内部情報表示の指定 —指定方法 CastOff.verbose(true or false) —デフォルト値 false —備考 コンパイルの経過や CastOff の内部情報を表示する(true)かしない(false)かを設定します。 ここでの CastOff の内部情報とは、次のようなものです。 1:クラス情報を解決できなかった変数やメソッドの返り値 2:複製を削減できなかった文字列リテラル

–組み込み変数に対する非互換性の許容 —指定方法 CastOff.allow_builtin_variable_incompatibility(true or false) —デフォルト値 false —備考 $1 などの、正規表現に関する組み込み変数の非互換性を許容する(true)か、許容しない(false)かを設定します。 $1 などの、正規表現に関する組み込み変数は、メソッド呼び出し毎に個別のスコープを持ちます。 例えば、次のような foo と bar では、これらの組み込み変数に対し、それぞれ異なるスコープを持っています。 このため、foo 内で $1 の値が “foo” となるにも拘らず、bar 内の puts $1 で表示されるのは、“bar” となります。


def foo()

"this is foo" =~ /(foo)/ # $1 = "foo" となる

end

def bar()

"this is bar" =~ /(bar)/ # $1 = "bar" となる
foo()
puts $1 # bar と表示

end

def baz()

"this is baz" =~ /(baz)/ # $1 = "baz" となる
puts $1 # baz と表示

end


これに対し、CastOff を用いた場合、foo と bar のこれらの組み込み変数に対するスコープが共有されてしまいます。 つまり、CastOff を使用すると、foo によって $1 の値が “foo” に上書きされ、bar 内の puts $1 で表示されるのが、“foo” となってしまいます。 以上の理由により、CastOff では、コンパイル対象のメソッドが $1 などの組み込み変数を使用していた場合、コンパイルエラーを発生させます。

しかし、上の例の baz のように、$1 が上書きされないようなメソッドでは、このような非互換性は問題とはなりません。 baz メソッドのように、組み込み変数のスコープに対する非互換性が崩れていても問題ない場合は、このオプションを使用してください。 CastOff.allow_builtin_variable_incompatibility(true) とすると、 $1 などの組み込み変数を使用していても、コンパイルを行うことができます。