K.Sasada's Home Page

こめんとのついか

こめんとこめんと!

message

please add long comment :).

_3(Mon)

Ruby VM アドベントカレンダーの 3 日目です.

今日も,Ruby 2.0 の新機能のご紹介です.

昨日 ご紹介した caller_locations ですが,これを使うことで簡単にパス名などだけを取り出すことができます.つまり,caller で必要であったパス名の切り出しのような正規表現の呼び出しなどが不要になります.そうすると,つまり,性能が良くなるのです.

caller をプロファイラなどに利用しようとするとき,性能はとても大事になります(性能は大事.ここ,テストに出ます).

では,実験してみましょう.正規表現で切り出す版と,path メソッドだけで取り出す版を作り,時間を計ってみました.

require 'benchmark'

max = 100_000

Benchmark.bm{|x|
  x.report{
    max.times{
      /(.+):\d/ =~ caller(0)[0]
    }
  }
  x.report{
    max.times{
      caller_locations(0)[0].path
    }
  }
}
#=>
ruby 2.0.0dev (2012-12-01 trunk 38127) [i386-mswin32_100]
       user     system      total        real
   1.872000   0.000000   1.872000 (  1.882239)
   0.312000   0.000000   0.312000 (  0.315540)

path メソッドで取り出す版は,6 倍程度速いことがわかります.まぁ,そりゃそうですよね.

(なお,ここではちょうテキトーな正規表現を使っているため,":" を含むようなパス名は正しくパースできません.その辺も,path メソッドを使うメリットですね)

さて,性能を考えると,そもそも「1つ上のフレームしか要らんのなら,1つ上だけ返してくれればいいのに」ということになります.

Ruby 2.0 から,Kernel#caller および Kernel#caller_locations は第2引数 n を取ることができ,これはスタックの深い場所から n 個取り出す,という意味になります.やってみましょう.

def m
  p caller(0)    #=> ["t.rb:2:in `m'", "t.rb:7:in `<main>'"]
  p caller(0, 1) #=> ["t.rb:3:in `m'"]
  p caller(0, 2) #=> ["t.rb:4:in `m'", "t.rb:7:in `<main>'"]
end

m

無引数だと,全部取り出す,1 と指定すれば 1 個だけ取り出す,ということがわかったと思います.

では,これが性能にどれくらい影響するか見てみましょう.

require 'benchmark'

max = 100_000
Benchmark.bm{|x|
  x.report("caller w/o 2nd arg"){
    max.times{
      caller(0)[0]
    }
  }
  x.report("caller w/ 2nd arg"){
    max.times{
      caller(0, 1)[0]
    }
  }
  x.report("caller_loc w/o 2nd arg"){
    max.times{
      caller_locations(0)[0]
    }
  }
  x.report("caller_loc w/ 2nd arg"){
    max.times{
      caller_locations(0, 1)[0]
    }
  }
}

#=>
ruby 2.0.0dev (2012-12-01 trunk 38127) [i386-mswin32_100]
                            user     system      total        real
caller w/o 2nd arg      1.513000   0.000000   1.513000 (  1.555697)
caller w/ 2nd arg       0.265000   0.000000   0.265000 (  0.259033)
caller_loc w/o 2nd arg  0.297000   0.000000   0.297000 (  0.298538)
caller_loc w/ 2nd arg   0.093000   0.000000   0.093000 (  0.090512)

第2引数で指定したものが随分と速いことがわかります.

なお,これはスタックフレームのサイズが大きいとより顕著になります(ちなみに,このときのスタックフレームのサイズは 9 でした).

ためしに,100個ほど足して(つまり,109フレーム)で測ってみましょう.

require 'benchmark'

def rec n=100, &b
  n == 0 ? yield : rec(n-1, &b)
end

max = 100_000
Benchmark.bm{|x|
  x.report("caller w/o 2nd arg"){
    rec{
      max.times{
        caller(0)[0]
      }
    }
  }
  x.report("caller w/ 2nd arg"){
    rec{
      max.times{
        caller(0, 1)[0]
      }
    }
  }
  x.report("caller_loc w/o 2nd arg"){
    rec{
      max.times{
        caller_locations(0)[0]
      }
    }
  }
  x.report("caller_loc w/ 2nd arg"){
    rec{
      max.times{
        caller_locations(0, 1)[0]
      }
    }
  }
}

#=>
ruby 2.0.0dev (2012-12-01 trunk 38127) [i386-mswin32_100]
                            user     system      total        real
caller w/o 2nd arg     19.172000   0.188000  19.360000 ( 19.644995)
caller w/ 2nd arg       0.500000   0.015000   0.515000 (  0.530067)
caller_loc w/o 2nd arg  3.478000   0.000000   3.478000 (  3.494444)
caller_loc w/ 2nd arg   0.297000   0.031000   0.328000 (  0.335042)

なんと,約40倍も違います.Raila などのフレームワークを利用すると,スタックフレームが大きくなることが多いので,もし caller の1フレーム分だけが欲しい,という時は活用してください.

さて,Rails という例が出てきたので,今日の最後の話題を紹介します.

Rails などのフレームワークの場合,スタックの深いところのフレームはフレームワークの分であり,興味のあるアプリケーションのスタックフレームは浅いところの一部だけ,ということがあります.つまり,caller の部分配列を取り出したい,という用途です.

そこで,caller および caller_locations は第一引数に Range を受けるようになりました.

caller(2..8) とすると,2つ上から8個上までの6フレーム分の情報を取り出します.また,フレームワークのスタックフレームがn個消費していることがわかっていれば,caller(1..-n) とすることで,1つ上のフレームから,全スタックフレームのサイズ - n の部分スタックフレームを取り出すことができます.

ちょっとわかりづらいですが,Array#[range] と全く同じように作ってありますので,困ったら Array#[range] の使い方を見て下さい.

ちなみに,今日紹介した第2引数の追加,および第1引数に Range を受け取れるようにする拡張は,Thread#backtrace および Thread#backtrace_locations にも行われています.

では,今日はこの辺で.


caller 関係しかでてこねーじゃねーか!

でも,もうちょっとだけ続くのじゃ.


http://gihyo.jp/news/info/2012/12/0101 に補足されなくて,やはり勘違いしているようで,何よりです.これからも勘違いしていきたい.


好きなだけ長いコメントをどうぞ。

お名前


back

tton 記述が使えます。YukiWikiな記述してりゃ問題ありません。

「行頭に#code」 と、「行頭に#end」 で挟むと、その間の行は pre で囲まれます。プログラムのソースを書くときに使ってください。

例:

#code

(なんかプログラム書く)

#end

リンクは

[[なまえ|http://www.example.org]]

とか

[[http://www.example.org]]

で貼れます。

$Date: 2003/04/28 10:27:51 $