pockestrap

Programmer's memo

Rubyでメソッドのインライン展開をする

Rubyでメソッドのインライン展開をするRinlineというgemを作って、RubyちこくKaigiで発表した。

github.com

speakerdeck.com

docs.google.com

見るならSpeakerdeckよりもGoogle slidesの方がオススメ。オリジナルデータだし、URLがリンクになっている。

この記事では、スライドの概略的なことと、スライドに書かなかった補完を書く。

Rinlineとは

Rubyでメソッドのインライン展開をするGem。メソッド呼び出しの回数を減らしてコードを高速化するのが目的。

次のような使い方がある。

require 'rinline'


class C
  def foo
    bar
  end

  def bar
    puts 42
  end
end

Rinline.optimize do |r|
  # Cクラスのfooメソッド内のメソッド呼び出しが最適化される
  r.optimize_instance_method(C, :foo)

  # Cクラスの全てのインスタンスメソッド内のメソッド呼び出しが最適化される。
  r.optimize_instance_methods(C)

  # Cクラス内の全てのインスタンスメソッドとクラスメソッド内のメソッド呼び出しが最適化される。
  r.optimize_class(C)

  # Cクラスと、Cクラスのネームスペース以下に定義されたクラス(C::Bクラスなど)が最適化される。
  r.optimize_namespace(C)
end

基本的にはoptimize_namespaceを使えば良いと思う。

で、速くなるの?

なると言えばなるし、ならないと言えばならない。

たとえば次のようなマイクロベンチマークに対しては確実に効果が出ている。

require 'benchmark'

class C
  def plain
    m + n
  end

  def optimized
    m + n
  end

  def hand_optimized
    1 + 2
  end

  def m
    1
  end

  def n
    2
  end
end

require 'rinline'
Rinline.optimize do |r|
  r.optimize_instance_method(C, :optimized)
end

i = C.new

Benchmark.bm(20) do |x|
  x.report('plain')     { 100000000.times { i.plain } }
  x.report('optimized') { 100000000.times { i.optimized } }
  x.report('hand_optimized') { 100000000.times { i.hand_optimized } }
end
$ ruby benchmark/simple.rb
                           user     system      total        real
plain                  5.716059   0.000000   5.716059 (  5.719808)
optimized              3.791726   0.000000   3.791726 (  3.793648)
hand_optimized         3.660404   0.000000   3.660404 (  3.662295)

ただし、実アプリケーションに対しては効果が現れないか、そもそも最適化がうまく通らない。 optcarrotに対して最適化を実行することは成功しているのだけど、最適化してもしなくてもFPSに優位な差がでなくて悲しい。 また、RuboCopに対して最適化をしようとするとエラーで死ぬ。 スライド中でも話したのだけど、define_methodを使って定義されているメソッドを埋め込もうとして死んでしまう。

どうやって実装しているの?

スライドで説明しているけど分かりづらいと思う。

optimize_instance_methodメソッドがクラスとメソッド名を受け取る。 この情報があるとUnboundMethodが取得できます。そしてUnboundMethodからはRubyVM::AST::Nodeが取得できます。 そこからゴニョゴニョするとメソッド定義のソースコードが取れるので、それをごちゃごちゃと書き換えていい感じにしてevalします。

メソッド定義のNodeを見た時に、その中のVCALLやFCALLは(class_evalとかされなければ)そのメソッドが定義されているクラスのinstance_methodメソッドでメソッドオブジェクトを取得できるというのがキモです。 これに興奮してノリで作ったのがRinline。

まとめると、動いているRubyプログラムからASTを取れるというのが画期的で、RubyVM::AST.ofが便利すぎるという話です。


まだPoCという感じだし実用的ではないのだけれど、RubyVM::AST.ofを使うとこんな面白いこともできてしまうのだぞという気持ちです。