■ Ruby スクリプトのプロファイラ

プロファイラって?

自分が書いたスクリプトが、実際どのように動いたか確認してみたくありませんか? 書いたスクリプトは、引数で渡されるデータによって動作が違ってきたりします。 このとき、もし一番よく使われる部分がわかれば、その部分の処理をうまく高速化できれば処理の効率がよくなります。 また、実行されていない部分がわかれば、その部分には bug が残っているかもしれません。 こういうときにプロファイラというものが役立ちます。

ここでは、とても簡単なプロファイラを用意しました。 基本的には、「ある」スクリプトに計測用の処理を埋め込み、そのスクリプトを実行します。 実行中に処理の結果をファイルに出力し、その結果をスクリプトのリストと合わせて表示するということをします。

ちなみに Ruby は、標準でプロファイラが使用可能です。

確認してみてください。
$ ruby -rprofile ruby_script.rb
のように実行してみてください。 ここで扱っているものとは違う情報がとれますよ。

■ 使い方

処理の流れは次のようになります。 計測したいスクリプトを ruby_script.rb としています。 mkprof.rb を実行して、計測用の仕組みを組み込んだ新しいスクリプトを作成します。 ここでは junk.rb という名前をつけています。 このスクリプトを実行することで、計測を行います。 計測結果は、prof_log.txt というファイルに出力されます。 計測結果をオリジナルのスクリプトに組み込むのが prprof.rb です。 結果は画面に表示されるので、適当に less などを使用してください。

$ mkprof.rb ruby_script.rb > junk.rb
$ ruby junk.rb 引数

ここで 'prof_log.txt' というファイルが作成される

$ prprof.rb ruby_script.rb | less
prprof.rb では、prof_log.txt という計測ファイル名を省略できます。 もし、ファイル名を別のものに置き換えた場合は、そのファイル名を指定してください。

ここでは、計測のための仕組みを組み込むとか、結果を組み込むとか書いていますが、直接スクリプトを修正するものではありません。 それぞれ標準出力に結果がでますので、リダイレクトしてファイルを生成して利用してください。 間違って、オリジナルのファイルにリダイレクトしないようにしてください。 オリジナルのファイルは大切にしましょう!

制限があります。 現在、スクリプトの行数は 1000 行までしか対応していません。 これは、mkprof.rb を修正することで対応可能です。 mkprof.rb したスクリプトを実行すると、カレントディレクトリに prof_log.txt というファイルをつくってしまいます。 同じファイル名を使っている場合には注意してください。

さてさて、実際に「使ってみたい」と思っていただくには、ここに実例を書くべきなのですが... 次のものは、mkprof.rb のプロファイル結果です。 処理対象は prprof.rb としました。

      while gets
   44   if prof_flag
    9     print '$r__prof[', $., '] += 1;'
        end
      
        if /\bexit\b/
    2     pr_exit
        elsif init_flag and /^$/
    1     print '$r__prof = []; $r__prof[0, ', MAX_line, '] = [0] * ', MAX_line, "\n"
          init_flag = FALSE
        elsif /<<(?:\"|\')?(\w+)(?:\"|\')?/
一部だけ抜き出したのですが、while が 44 回実行され、if が真のときの回数、条件での各処理の回数などがわかります。 いかがですか?

完全に対応しているわけではありません。 このため抜けがでてくると思います。 自分のスクリプトの書き方に合わせて適当に修正してお使いください。 または、アイディアだけ使って自分で新規に作られるのもいいと思いますよ。

■ ソースコード

スクリプトをセーブして、使用する場合には「テキスト」でセーブしてください。 これで使えるようになるはずです。

#! /usr/local/bin/ruby
# /home/tetsu/src/ruby/sdk/mkprof.rb
# Created: September 13,1998 Sunday 15:00:44
# Author: tetsu(WATANABE Tetsuya)
# $Id: mkprof.rb,v 1.9 2001/08/14 18:27:55 tetsu Exp $
# usage: mkprof.rb ruby_script.rb > junk.rb
#        ruby junk.rb
#        prprof.rb ruby_script.rb | less

def pr_exit
  print <<'E'

END {
f = File.open('prof_log.txt', 'w')
$r__prof.each_index do |i|
  f.print i, ' ', $r__prof[i], "\n" if $r__prof[i] != 0
end
f.close
}
E
end

MAX_line = 1000
line_no = 0
init_flag = true
prof_flag = false
here_doc_flag = false
exitp_flag = false

while gets
  if prof_flag
    print '$r__prof[', $., '] += 1;'
  end

  if ~/^__END__/
    pr_exit
    exitp_flag = true
  end

  print
  $_.sub!(/\s*\#.*$/, '')

  if init_flag and ~/^$/
    print '$r__prof = []; $r__prof[0, ', MAX_line, '] = [0] * ', MAX_line, "\n"
    init_flag = false
  elsif ~/<<(?:\"|\')?(\w+)(?:\"|\')?/
    here_doc_mark = $1
    here_doc_flag = true
  elsif here_doc_flag and ~/^#{here_doc_mark}/
    here_doc_flag = false
  end

  comma_flag = if ~/,$/ then true else false end

  prof_flag =
    if ~/\b(def|do|if|then|elsif|else|while|until)\b/ and
        not here_doc_flag and
        not comma_flag
      true
    else
      false
    end
end

if $. > MAX_line
  STDERR.print "over line (#{MAX_line})\n"
end

pr_exit if not exitp_flag
#! /usr/local/bin/ruby
# /home/tetsu/src/ruby/sdk/prprof.rb
# Created: September 13,1998 Sunday 15:36:22
# Author: tetsu(WATANABE Tetsuya)
# $Id: prprof.rb,v 1.4 1998/09/14 05:08:05 tetsu Exp $
#        mkprof.rb ruby_script.rb > junk.rb
#        ruby junk.rb
# usage: prprof.rb ruby_script.rb [prof_log.txt] | less

def usage
  STDERR.print "usage: #{$0} src_file [log_file]\n"
  exit 1
end

src_file = ARGV.shift
log_file = ARGV.shift || 'prof_log.txt'

if File.exist? log_file
  log_f = File.open(log_file)
else
  usage
end

if File.exist? src_file
  src_f = File.open(src_file)
else
  usage
end

c = {}

while log_f.gets
  line_no, count = $_.split
  c[line_no.to_i] = count
end

while src_f.gets
  if c.key?($.)
    printf('%7d ', c[$.])
  else
    print '        '
  end
  print
end

exit

■ 解説

「とりあえず」ということで解説します。

Ruby のスクリプトの中でどのポイントの実行回数を計測するか? は、「実行ブロック」となるところにカウンタを設定します。 ここでの「実行ブロック」は、if のような条件により実行される文の固まり end までや、do から end までの文の固まりのことです。 この実行ブロックが何度実行されたか? を計測します(同じことを繰り返し書いていますが)。 実際のところは、mkprof.rb を実行して、スクリプトを作成してみてください。 ポイントポイントに、オリジナルのソースコードの行番号をインデックスとした配列を埋め込んで計測しています。

たぶん、実用的に使うにはまだ必要なところが計測できなかったり、うまく初期化できなかったり、結果がちゃんとでなかったりするかもしれません。 ですが、mkprof.rb で作成されたスクリプトをもとに計測部分を追加したりして、結果をうまく活用してください。 ほしい部分の実行回数は「手」で追加するのもありですから(あらら)。 私のツールは、ややこしい仕事の 80% くらいを自動化して、残りの 20% をうまく... 以下省略。 完全自動化というのももちろんありなんですが、どんどん使っていくうちによくなっていくものです。 まあ、プロファイラというのは、使って自分のスクリプトの動作を理解していく手助けをするものなので、そちらの話題を書いた方がいいですよね。 いま、土台ができましたから、気がつくたびに使っていくということかな。

最後のしめも「とりあえず」ということで。

■ 余談

プロファイラとして実行時にどのような処理がより多く実行されるか? という確認が可能です。 そのほかに、プログラムの動作を確認するのに使えます。 デバックなどで「処理の流れを追いかける」のにもとても有効です。 もし、データによってよくわからない現象が起きているような場合、この行レベルプロファイラを使ってみるのも方法と思います。

■ 参考文献

プロファイラについては、次の本がとても参考になります。

プログラマのうちあけ話
続・プログラム設計の着想
J.L.ベントリー 著
野下浩平・古郡廷治 共訳
近代科学社
ISBN4-7649-0177-3
More Programming Pearls
ここでとり上げられている、素数を出力するプログラムを Ruby で書いてみて、ここで紹介している mkprof.rb/prprof.rb を使うと、同じような出力が得られます。 作成したスクリプトがどのように動作するか? 実際に確認できるので、プロファイラはなかなかいい道具です。

上記の本が、次のような形で新しくなりました。 私は扱っている内容とかとっても好きです。

珠玉のプログラミング
本質を見抜いたアルゴリズムとデータ構造
Programming Pearls Second Edition
ジョン ベントリー 著
小林健一郎 訳
ピアソン エデュケーション
ISBN4-89471-236-9
また、次の本もいろいろ勉強になることが多いです。
プログラミング作法
The Practice of Programming
Brian W.Kernighan
Rob Pike 著
福崎俊博 訳
アスキー
ISBN4-7561-3649-4
これらの本を題材に、Ruby を試してみてはいかがでしょうか?

さて、ここを読んでいるみなさん。 「プロファイルを使おう」ということで、今月をプロファイラ月間として、あれこれプロファイラを試してみませんか? 上記参考書でも勧めていますが、いいことだと思っています。

この他、「プログラミング言語 AWK」(トッパン)でも同様にプロファイラのことがとり上げられているようです。 私はいま手元にこの本がないので、詳しくはわからないのですが(会社に置いたまま...)。 この本は、AWK の解説書であると同時に、コンピュータサイエンスの手頃な入門書でもあります。 AWK という手軽な言語をいろいろ応用に利用しているので、スクリプト言語に興味がある方はぜひ一度手にとってみてはいかがでしょう? この本でとり上げられている題材は、Ruby で書き直すのにそれほど苦労は必要ないはずです。 著作権の問題があってできませんが、こういういい本が少し古くなってきたりしているので、Ruby で書き直すとかができるといいんですけど。 「UNIX プログラミング環境」とか「ソフトウェア作法」とか... みんな作者が同じだなぁ。

■ 履歴


渡辺哲也(WATANABE Tetsuya): Tetsuya.WATANABE atmark nifty.com