RubyKaigi2007: C より速い Ruby プログラム

はじめに

Ruby をはじめとする動的なスクリプト言語は、C や Java のような静的なコンバイル型言語と比べると動作速度が遅いです。 しかし、それがそのままアプリケーションの動作速度を決定するわけではありません。 アプリケーションの動作速度には、実装言語の速度よりも、アルゴリズムやデータ構造といった知識の有無や、プログラミング上の工夫やアイデアのほうがずっと大きく影響を与えます*1

本稿では、C 言語で書かれた拡張モジュールより高速に動作する pure Ruby プログラムを示すことで、アプリケーションの速度には言語よりも知識やアイデアの方がずっと重要であることを示します。

具体的な題材としては、実アプリケーションで広く使われている eRuby の処理系を扱います (eRuby については後述)。 学校の教科書では、よくフィボナッチ数列の高速化などを使って「アルゴリズムの工夫が大事である」と説明していますが、実際のアプリケーションでフィボナッチ数列を使うことなど滅多にありません。 我々が知りたいのは自分のプログラムで使える知識であり、教科書の中でしか出てこないようなものを使って「工夫が大事」といわれても実感がわきません。

本稿ではそのようなことがないように、eRuby の処理系という、現場で広く使われているものを取り上げます。これにより、知識や工夫が大事であることを実感していただけたらと思います。

なお本稿は、RubyKaigi2007 における同名のセッション を文書化したものです。 発表資料 (pdf) も併せてご覧下さい。

eRuby について

今回の題材である eRuby について説明します。 eRuby(embedded Ruby) とは、テキストファイル中に Ruby の文や式を埋め込むための仕様です (大雑把にいえば PHP のようなものだと思ってください)。 list0-1 はそのサンプルです。「<%= ... %>」の中に Ruby の式を、「<% ... %>」の中に Ruby の文を埋め込みます。

list0-1: eRuby のサンプル

<table>
 <tbody>
{{*<% for item in list %>*}}
  <tr>
   <td>{{*<%= item %>*}}</td>
  </tr>
{{*<% end %>*}}
 </tbody>
<table>

式や文が埋め込まれたテキストファイルは、Ruby のスクリプトへ「変換」され、そのまま Ruby スクリプトとして「実行」されます。この「変換」と「実行」を行うのが eRuby の処理系です。

現在、eRuby の主な処理系は次の 2 つです。

eruby
C 言語による拡張モジュールとして書かれた処理系です。自分でコンパイルする必要がありますが、動作は高速です。
ERB
pure Rubyで書かれた処理系です。Ruby 1.8 以降に標準添付されており、最も広く使われています。

名前の混乱を避けるために、以降では前者を「C 実装 eruby」と呼ぶことにします。後者はそのまま「ERB」と呼びます。

C 実装 eruby と ERB では、変換結果に違いがあります。 list0-2 は C 実装 eruby による変換例です。 1 行ずつ print 文に変換しているのが特徴です。

list0-2: C 実装 eruby による変換例

print "<table>\n"
print " <tbody>\n"
{{* for item in list *}}; print "\n"
print "  <tr>\n"
print "   <td>"; print(( {{*item*}} )); print "</td>\n"
print "  </tr>\n"
{{* end *}}; print "\n"
print " </tbody>\n"
print "<table>\n"

list0-3 は ERB による変換例です。1行ずつ変換している点は C 実装 eruby と同じですが、print 文ではなく文字列結合を使っているのが特徴です。

list0-3: ERB による変換例

_erbout = ''; _erbout.concat "<table>\n"
_erbout.concat " <tbody>\n"
{{* for item in list *}}; _erbout.concat "\n"
_erbout.concat "  <tr>\n"
_erbout.concat "   <td>"; _erbout.concat(( {{*item*}} ).to_s); _erbout.concat "</td>\n"
_erbout.concat "  </tr>\n"
{{* end *}}; _erbout.concat "\n"
_erbout.concat " </tbody>\n"
_erbout.concat "<table>\n"
_erbout

変換された Ruby スクリプトを実行すると、例えば list0-4 のような出力が得られます。 C 実装 eruby と ERB とでは、変換された Ruby スクリプトは違いますが、その実行結果は同じものになります。

list0-4: 実行されて得られる出力の例

<table>
 <tbody>

  <tr>
   <td>AAA</td>
  </tr>

  <tr>
   <td>BBB</td>
  </tr>

  <tr>
   <td>CCC</td>
  </tr>

 </tbody>
<table>

ベンチマークの内容

ここで、このあとで使用するベンチマークデータについて説明します。ベンチマーク一式は以下からダウンロードできます。

ベンチマークで使用する eRuby ファイルは list0-5 のようになります。 ループ部分が 20 行あり、それを 20 回ループします。その前後に 53 行と 5 行のデータがついており、最終的に約 400 行の HTML ファイルを出力します。

list0-5: ベンチマーク用 eRuby ファイル (erubybench.rhtml)

     1:	<?xml version="1.0" encoding="UTF-8"?>
     2:	<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
     3:	          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
     4:	<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
        .....
    47:	  <table>
        ....
    53:	   <tbody>
    54:	{{*<%*}}
    55:	{{*n = 0*}}
    56:	{{*for item in list*}}
    57:	    {{*n += 1*}}
    58:	{{* %>*}}
    59:	    <tr class="{{*<%= n % 2 == 0 ? 'even' : 'odd' %>*}}">
    60:	     <td style="text-align: center">{{*<%= n %>*}}</td>
    61:	     <td>
    62:	      <a href="/stocks/{{*<%= item['symbol'] %>*}}">{{*<%= item['symbol'] %>*}}</a>
    63:	     </td>
        .....
    77:	    </tr>
    78:	{{*<%*}}
    79:	{{*end*}}
    80:	{{* %>*}}
    81:	   </tbody>
    82:	  </table>
    83:	
    84:	 </body>
    85:	</html>

なお本稿でのベンチマークでは、純粋に eRuby 処理系としてだけの能力を測るため、HTML エスケープ (サニタイズ) は行っていません。ご注意ください。

またベンチマークに使った環境は次の通りです。この環境で 10000 回実行した時間を計測しました。

  • MacBook, Intel CoreDuo 1.83GHz, Memory 2GB
  • MacOS X 10.4 Tiger
  • Ruby 1.8.6
  • eruby 1.0.5
  • GCC 4.0.1

C 実装 eruby と ERB のベンチマーク結果は table0-1 のようになりました。 これを見ると、C 実装 eruby は ERB と比べて 3 倍ほど高速ですが、高速なのは eRuby ファイルの変換部分だけであり、変換後の Ruby スクリプトを実行する部分ではそれほどの違いはないことがわかります。

table0-1: ベンチマーク結果

処理系 変換部分(sec) 実行部分(sec) 合計(sec)
C 実装 eruby 1.390 13.958 15.348
ERB 27.963 14.849 42.812

このように、 C 実装 eruby で速くなるのは変換部分だけであり、実行部分は速くはなりません。 この点は大事ですので覚えておいてください。

実装

ここからは、実際に eRuby 処理系を pure Ruby で実装していきます。 それを少しずつ改良しながら、ベンチマークをとって速度を確認していきます。

実装#1: 最初の実装

まず最初の実装は list1-1 のようになります。 ポイントは「入力を1行ずつに分解して構文解析する」という点です。

list1-1: 最初の実装 (myeruby1.rb)

### プログラム
class MyEruby1
  ...
  def convert(eruby_str)
    s = '_buf = ""; '
    kind = :text
    {{*eruby_str.each_line do |line|*}}     # 1行ずつに分解
      {{*line.scan(/(.*?)(<%=?|%>)/) do*}}  # '<%' と '<%=' と '%>' を探す
        str = $1                      # テキストまたは埋め込み文や式
        s << _convert_str(str, kind) unless str.empty?
        case $2
        when '<%'  ;  kind = :stmt    # 埋め込み文
        when '<%=' ;  kind = :expr    # 埋め込み式
        when '%>'  ;  kind = :text    # テキスト
        end
      end
      text = $' || line               # 残りのテキスト
      s << _convert_str(text, kind) unless text.empty?
    end
    s << "_buf\n"
    return s
  end
  ...
end

変換されたRubyスクリプトは list1-2 のようになります。 ポイントは「1行ずつの文字列結合に変換する (ERB とほぼ同じ)」という点です。

list1-2: 変換後のRubyスクリプト

### 変換結果
_buf = ""; _buf << "<html>\n";
_buf << "<body>\n"
_buf << " <table>\n"
{{* for item in list ;*}} _buf << "\n";
_buf << "  <tr>\n"
_buf << "   <td>"; {{*_buf << (item['name']).to_s;*}} _buf << "</td>\n";
_buf << "  </tr>\n";
{{* end ;*}} _buf << "\n";
...

ベンチマーク結果は table1-1 のようになりました。 変換部分が大変なボトルネックになっていることが分かります。

table1-1: ベンチマーク結果

処理系 変換部分(sec) 実行部分(sec) 合計(sec)
C 実装 eruby 1.390 13.958 15.348
ERB 27.963 14.849 42.812
MyEruby1 (initial implement) 79.210 13.320 92.530

最初の実装は、大変遅いものとなりました。 これを少しずつ改良していきます。

実装#2: 行への分解をやめる

まず最初の改良として、入力を1行ずつ分解して解析するのをやめ、入力全体をひとつの文字列としてそのまま解析するようにします (list2-1)。 これにより、1 行ずつに分解するコストがなくなるため、Ruby スクリプトへの変換部分が速くなります。

list2-1: 1行ずつに分解するのをやめる (myeruby2.rb)

## 改良前
    {{*eruby_str.each_line do |line|*}}
      {{*line*}}.scan(/(.*?)(<%=?|%>)/).each do
        ...
      end
    end

## 改良後
    {{*eruby_str*}}.scan(/(.*?)(<%=?|%>)/m).each do
      ...
    end

また変換後の Ruby スクリプトも、1 行ずつに分解するのではなく、複数行をまとめて扱うようにします (list2-2)。 これにより、文字列結合のメソッド呼び出しが大幅に削減できるので、変換後の実行部分が速くなります。 今までだと、例えば 50 行の HTML があれば 50 回メソッドが呼び出されていたのが、この方法だと 1 回で済むわけですから、かなりの高速化が期待できます。

list2-2: 複数行をまとめて扱う

## 改良前
_buf << "<html>\n"
_buf << " <body>\n"
_buf << "  <h1>title</h1>\n"

## 改良後
_buf << '<html>
 <body>
  <h1>title</h1>
';

ベンチマーク結果は table2-1 のようになりました。 変換部分で 8 倍の高速化、実行部分で 70% 以上の高速化、トータルで 5 倍高速化されました。 C 実装 eruby にはまだ負けますが、ERB には余裕で勝つことができました。

table2-1: ベンチマーク結果

処理系 変換部分(sec) 実行部分(sec) 合計(sec)
C 実装 eruby 1.390 13.958 15.348
ERB 27.963 14.849 42.812
MyEruby1 (initial implement) 79.210 13.320 92.530
MyEruby2 (no split lines) 9.937 8.408 18.345

入力を 1 行ずつに分解するのは C や Perl でよく見かけますが、それにならう必要性はありません。 メモリ容量がよほど厳しい場合は別として、通常は入力文字列を行に分解せずそのまま扱う方が高速になります。

実装#3: 構文解析をパターンマッチに

次に、構文解析をやめて正規表現によるパターンマッチで済ませるようにします (list3-1)。 構文解析といっても大したことをやっているわけではないのですが、それっぽいことをしているので、この部分をパターンマッチに置き換えます。

list3-1: 構文解析をせず、パターンマッチで済ます (myeruby3.rb)

## 改良前
    ...
    kind = :text
    eruby_str.scan(/(.*?)(<%=?|%>)/m) do
      s << _convert_str($1, kind)
      case $2
      when '<%=' ;  kind = :expr
      when '<%'  ;  kind = :stmt
      when '%>'  ;  kind = :text
      end
    end
    ...

## 改良後
    ...
    eruby_str.scan({{*/(.*?)<%(=?)(.*?)%>/*}}m) do
      s << _convert_str($1, {{*:text*}})
      {{*if $2 == '='*}}
        {{*s << _convert_str($3, :expr)*}}
      {{*else*}}
        {{*s << _convert_str($3, :stmt)*}}
      {{*end*}}
    end
    ...

変換後の Ruby スクリプトは先ほどのと同じですので省略します。

ベンチマーク結果は table3-1 の通りです。 これを見ると、変換部分が 70% 以上高速化され、全体として C 実装 eruby より速くなったことが分かります。

table3-1: ベンチマーク結果

処理系 変換部分(sec) 実行部分(sec) 合計(sec)
C 実装 eruby 1.390 13.958 15.348
ERB 27.963 14.849 42.812
MyEruby1 (initial implement) 79.210 13.320 92.530
MyEruby2 (no split lines) 9.937 8.408 18.345
MyEruby3 (pattern matching) 5.738 8.466 14.204

2回目の改良で、早くも目的は達成されました。 もちろん変換部分だけをみると C 実装 eruby にはかなわないのですが、変換後の Ruby スクリプトの実行が大幅に高速化されているため、全体としては C 実装 eruby よりも速くなっています。

このように、構文解析をまじめに行うかわりにパターンマッチで済ませることができると、大幅に高速化できる場合があります。 例えば XML ファイルからデータを取り出すときに、XML パーサを使うかわりにパターンマッチで済ませられないか検討するとよいでしょう。

実装#4: 正規表現をチューニング

次に、正規表現をチューニングします。 正規表現のチューニングでは、速い正規表現を使うことも大事ですが、それ以上に遅い正規表現を使わないことが大事です。

先ほどのプログラムだと、カッコを使ったグルーピングが 3 つありました。このうち 2 つめと 3 つめはマッチする文字列が短いですが、1 つめはかなり長い文字列にマッチします。

実は正規表現のグルーピングは、長い文字列にマッチするととたんに遅くなることが分かっています。 そこでこの部分を改良し、正規表現のグルーピングを使わずに自力で切り出してくるようにします (list4-1)。 グルーピングの数が 3 つから 2 つに減っていることに注意してください。 プログラムは長くなってしまいますが、高速化とのトレードオフです。

list4-1: 正規表現のグルーピングを避けて自前で切り出す (myeruby4.rb)

## 改良前
    ...
    eruby_str.scan(/{{*(.*?)*}}<%(=)?(.*?)%>/m) do
      text, equal, code = $1, $2, $3
      ...
    end
    ...

## 改良後
    ...
    {{*pos = 0*}}
    eruby_str.scan(/<%(=)?(.*?)%>/m) do
      equal, code = $1, $2
      {{*match = Regexp.last_match*}}
      {{*len   = match.begin(0) - pos*}}
      {{*text  = eruby_str[pos, len]*}}
      {{*pos   = match.end(0)*}}
      ...
    end
    ...

変換後の Ruby スクリプトに変更はありません。

ベンチマーク結果 (table4-1) を見ると、変換部分が確かに高速化されていることが分かります。

table4-1: ベンチマーク結果

処理系 変換部分(sec) 実行部分(sec) 合計(sec)
C 実装 eruby 1.390 13.958 15.348
ERB 27.963 14.849 42.812
MyEruby1 (initial implement) 79.210 13.320 92.530
MyEruby2 (no split lines) 9.937 8.408 18.345
MyEruby3 (pattern matching) 5.738 8.466 14.204
MyEruby4 (optimized regexp) 3.620 8.356 11.976

正規表現や SQL のように宣言的な記述を行うようなものはどれも、使うだけなら簡単ですが、中の仕組みがブラックボックス化しているため、チューニングが困難です。 その分、チューニングの余地が大きい箇所でもあります。 興味のある人は正規表現の動作コストをいろいろ調べてみてください。

実装#5: メソッドをインライン展開

次に、メソッドをインライン展開することで、メソッド呼び出しの回数を減らしてみます (list5-1)。 Ruby のメソッド呼び出しはそれなりのコストがかかるので、これを減らすことは一定の効果があります。

ただし、高速化よりもプログラムの保守性を優先するようにしてください。 今回はそもそも別メソッドにしなくてよいケースでしたのでインライン展開してよかったのですが、実際のプログラムでは必ずしもそうとは限りませんので注意してください。

list5-1: メソッドをインライン展開 (myeruby5.rb)

### 改良前
        ...
        s << _convert_str(code, :expr)
        s << _convert_str(code, :stmt)
        s << _convert_str(text, :text)
        ...
### 改良後
        ...
        {{*s << "_buf << (#{code}).to_s; "*}}
        {{*s << "#{code}; "*}}
        {{*text.gsub!(/[\\']/, '\\\\\&')*}}
        {{*s << "_buf << '#{text}'; " unless text.empty?*}}
        ...

今回も変換後の Ruby スクリプトに変更はありません。

ベンチマーク結果 (table5-1) を見ると、変換部分が 15% 以上高速化していることがわかります。

table5-1: ベンチマーク結果

処理系 変換部分(sec) 実行部分(sec) 合計(sec)
C 実装 eruby 1.390 13.958 15.348
ERB 27.963 14.849 42.812
MyEruby1 (initial implement) 79.210 13.320 92.530
MyEruby2 (no split lines) 9.937 8.408 18.345
MyEruby3 (pattern matching) 5.738 8.466 14.204
MyEruby4 (optimized regexp) 3.620 8.356 11.976
MyEruby5 (inline method) 3.091 8.320 11.411

変換部分の時間は、pure Ruby でありながら C 実装 eruby の 2 倍強で済んでおり充分高速といえますので、あとは実行部分の改善を図っていきます。

実装#6: 配列バッファを使ってメソッド呼び出しを削減する

次に、変換後の Ruby スクリプトにおいて、文字列バッファではなくて配列バッファを使ってみます (処理系のプログラムはmyeruby6.rb)

変換後の Ruby スクリプト (list6-1) では、今までだと複数回の String#<< メソッドの呼び出しが必要だったのが、配列バッファを使うと 1 回の Array#push() メソッドの呼び出しで済ませることができます。 このため、メソッド呼び出しの回数を減らすことができ、変換後の実行部分が高速化されます。

list6-1: String#<< のかわりに Array#push() を使い、メソッド呼び出しを削減する

### 改良前
_buf = '';
_buf << '<td>'; _buf << n.to_s; _buf << '</td>';
_buf

### 改良後
{{*_buf = [];*}}
{{*_buf.push(*}}'<td>', n, '</td>'{{*)*}};
{{*_buf.join*}}

ベンチマーク結果 (list6-1) を見ると、実行部分が高速化し、全体で 10% 弱速くなっていることが分かります。

table6-1: ベンチマーク結果

処理系 変換部分(sec) 実行部分(sec) 合計(sec)
C 実装 eruby 1.390 13.958 15.348
ERB 27.963 14.849 42.812
MyEruby1 (initial implement) 79.210 13.320 92.530
MyEruby2 (no split lines) 9.937 8.408 18.345
MyEruby3 (pattern matching) 5.738 8.466 14.204
MyEruby4 (optimized regexp) 3.620 8.356 11.976
MyEruby5 (inline method) 3.091 8.320 11.411
MyEruby6 (array buffer) 3.139 7.381 10.520

なお配列を使って高速化されるのは、Array#push() メソッドが複数個の引数をとることができるおかげで全体のメソッド呼び出しを削減できるからです。 残念ながら String#<< メソッドや String#concat() メソッドは引数を 1 つしかとれないので、メソッド呼び出しを削減することには使えません。

しかし、文字列バッファでは別の方法を使ってメソッド呼び出しを削減できます。 これを次に見てみます。

実装#7: 式展開を使ってメソッド呼び出しを削減

配列バッファではなく文字列バッファを使う場合は、文字列中の式展開を使うことで String#<< メソッドの呼び出しを減らすことができます (list7-1)。

式展開とは、Ruby の文字列リテラルの中に任意の式を埋め込むことができる機能です。 Perl や PHP でも似た機能がありますが、これらの言語では埋め込めるのが基本的に変数名のみに限定されているのに対し、Ruby ではメソッド呼び出しや四則演算など任意の式を埋め込むことができます。

サンプルを見れば分かるように、変換後の Ruby スクリプトにおいて従来だと文字列結合のメソッド呼び出しが複数回必要だったのが、式展開を使うと 1 回で済むようになるため、変換後の実行部分を高速化できます。

list7-1: 文字列中の式展開を使って、String#<< メソッドの呼び出しを削減する

### 改良前
_buf << '<tr>
<td>'; _buf << n.to_s; _buf << '</td>
</tr>';

### 改良後
_buf << {{*%Q`*}}<tr>
<td>{{*#{n}*}}</td>
</tr>{{*`*}}

またこのサンプルでは「% 記法」を使っています。 「%Q`...`」は「"..."」と同じ意味ですが、文字列リテラルを囲むのに「`」以外の任意の記号を使うことができます。 「"」で文字列リテラルを表すと、HTML中にも「"」が頻出するため、そのエスケープ処理のコストが馬鹿になりません。 そこで「% 記法」を使い、例えば「`」のようなHTML中に現れにくい文字を使うことで、エスケープ処理を避けています。

なお処理系のプログラムはmyeruby7.rb

ベンチマーク結果 (table7-1) を見ると、実行部分の速度は配列バッファを使う方法と比べてほぼ同じであることがわかります。 つまり、メソッド呼び出しを減らすことによる効果はどちらも同じだといえます。

table7-1: ベンチマーク結果

処理系 変換部分(sec) 実行部分(sec) 合計(sec)
C 実装 eruby 1.390 13.958 15.348
ERB 27.963 14.849 42.812
MyEruby1 (initial implement) 79.210 13.320 92.530
MyEruby2 (no split lines) 9.937 8.408 18.345
MyEruby3 (pattern matching) 5.738 8.466 14.204
MyEruby4 (optimized regexp) 3.620 8.356 11.976
MyEruby5 (inline method) 3.091 8.320 11.411
MyEruby6 (array buffer) 3.139 7.381 10.520
MyEruby7 (interpolation) 3.004 7.411 10.415

式展開を使ってメソッド呼び出しを削減する方法は、Ruby の特徴をうまく利用した、Ruby ならではの方法といえます。 逆にいえば、Ruby 以外では利用できない方法ともいえます。 他の言語では、配列バッファを使う方法がいいでしょう。

この時点で、 C 実装 eruby と比べて約 1.5 倍高速化されました。 プログラムの工夫がいかに大事であるか、実感していただけましたでしょうか。

実装#8: ファイルキャッシュ

次に、変換後の Ruby プログラムをファイルにキャッシュするようにします (list8-1)。 今までは、ファイルを読み込んで毎回 Ruby スクリプトへ変換していましたが、変換後の Ruby プログラムをファイルにキャッシュすれば、この変換コストをほぼゼロにすることができます。

list8-1: 変換後の Ruby スクリプトをファイルにキャッシュする (myeruby8.rb)

## 改良前
  ...
  def convert_file(filename)
    prog = convert(File.read(filename))
    return prog
  end
  ...

## 改良後
  ...
  def convert_file(filename)
    if キャッシュファイルがない or 古い
      prog = convert(File.read(filename))
      キャッシュファイルに書き込む
    else
      prog = キャッシュファイルを読み込む
    end
    return prog
  end
  ...

ベンチマーク結果 (table8-1) を見ると、変換部分のコストがほぼゼロになり、全体で 20% 以上高速化されたことかわかります。

table8-1: ベンチマーク結果

処理系 変換部分(sec) 実行部分(sec) 合計(sec)
C 実装 eruby 1.390 13.958 15.348
ERB 27.963 14.849 42.812
MyEruby1 (initial implement) 79.210 13.320 92.530
MyEruby2 (no split lines) 9.937 8.408 18.345
MyEruby3 (pattern matching) 5.738 8.466 14.204
MyEruby4 (optimized regexp) 3.620 8.356 11.976
MyEruby5 (inline method) 3.091 8.320 11.411
MyEruby6 (array buffer) 3.139 7.381 10.520
MyEruby7 (interpolation) 3.004 7.411 10.415
MyEruby8 (file caching) 0.662 7.360 8.022

またキャッシュを使えば、C 実装 eruby を使う必要がまったくないことも分かります。 C 実装 eruby によって速くなるのは Ruby スクリプトへの変換部分だけですが、キャッシュを使えばこの変換自体を省くことができるので、わざわざ C 言語を使ってまで高速化する必要はなかったわけです。

キャッシュを使う方法は、ERB にも適用可能です。 ERB での変換コストはとても高いので、ERB を使った CGI プログラムを書いている人は、ぜひキャッシュを使いましょう。

実装#9: 関数にして実行

次に、文字列を eval するのをやめて、関数を定義して実行するようにします (list9-1)。

文字列を eval する方法では、Ruby パーサが文字列を解析して構文木を生成するコストが毎回発生します。 関数にして実行すれば、このコストをほぼなくすことができます。

list9-1: evalの代わりに、関数に変換して実行 (myeruby9.rb)

### 改良前
  ...
  prog = myeruby.convert_file(filename)
  eval prog
  ...

### 改良後
  ...
  {{*def define_method(body, args=[])*}}
    {{*eval "def self.evaluate(#{args.join(',')}); #{body}; end"*}}
  {{*end*}}
  ...
  prog = myeruby.convert_file(filename)
  args = ['list']
  {{*myeruby.define_method(prog, args)*}}
  {{*myeruby.evaluate()*}}
  ...

ただし、この方法で効果があるのは、ひとつのプロセス中で同じ eRuby ファイルを何度も実行する場合です。 Web アプリケーションでいえば、mod_ruby や FastCGI では効果がありますが、CGI では効果はないでしょう。

ベンチマーク結果 (table9-1) を見ると、約 1.5 倍高速化されたことがわかります。 つまり、eval だと全実行時間のうち約 3 分の 1 がプログラムのパースに費やされていることになります。

table9-1: ベンチマーク結果

処理系 変換部分(sec) 実行部分(sec) 合計(sec)
C 実装 eruby 1.390 13.958 15.348
ERB 27.963 14.849 42.812
MyEruby1 (initial implement) 79.210 13.320 92.530
MyEruby2 (no split lines) 9.937 8.408 18.345
MyEruby3 (pattern matching) 5.738 8.466 14.204
MyEruby4 (optimized regexp) 3.620 8.356 11.976
MyEruby5 (inline method) 3.091 8.320 11.411
MyEruby6 (array buffer) 3.139 7.381 10.520
MyEruby7 (interpolation) 3.004 7.411 10.415
MyEruby8 (file caching) 0.662 7.360 8.022
MyEruby9 (define method) 0.001 5.105 5.106

この結果から想像できるように、パース後の構文木をファイルにキャッシュできると、関数にすることなくeRubyを高速化することができるはずです。 しかし残念ながら、現在の Ruby ではそれができません。 例えば Python だとパース後の (構文木ではなく) バイトコードをファイルに保存することができるので、関数にすることなく、また CGI でも、eval を高速に実行できます。 現在開発中の Ruby 1.9 ではバイトコードインタプリタになるので、バイトコードをファイルに保存できるようになれば、Python と同じように eval が大幅に高速化できるでしょう。

なお関数に変換するかわりに、Proc オブジェクトに変換して instance_eval() で実行する方法でも、同じように高速化できます(個人的にはこのほうが好みです)。興味のある人は試してみてください。

高速化のための原則

今回行った高速化の方法は、大きく3つの原則に分類できます。

1つめは余計な処理をしないということです。今回でいうと次の方法が当てはまります。

  • 入力を1行ずつに分解しない (実装#2、変換部分で 69.17sec 削減)
  • キャッシュを使うことで変換作業を減らす (実装#8、変換部分で 2.44sec 削減)
  • 関数化することでパースするコストを減らす (実装#9、実行部分で 2.26sec 削減)

2つめはメソッド呼び出しを減らすということです。今回でいうと次の方法が当てはまります。

  • 複数行をまとめて1回で出力する (実装#2、実行部分で 4.91sec 削減)
  • メソッドをインライン展開する (実装#5、変換部分で 2.11sec 削減)
  • 配列バッファとArray#push()メソッドを使う (実装#6、実行部分で 0.94sec 削減)
  • 文字列中の式展開を使う (実装#7、実行部分で 0.91sec 削減)

3つめは正規表現を見直すことです。今回でいうと次の方法が当てはまります。

  • 構文解析をやめてパターンマッチを使う (実装#3、変換部分で 4.20sec 削減)
  • 長い文字列にマッチするようなグルーピングを避けて自前で切り出す (実装#4、変換部分で 2.12sec 削減)

言語や環境によって個々の方法は変わっても、これらの原則は普遍的に通用します。 細かいテクニックを覚えることよりも、こういった普遍的な原理や原則のほうを大事にしてください。

他言語との比較

本稿での内容は、Erubis という eRuby 処理系に反映されています。 それも含めて、他の言語で使われているテンプレートエンジンでベンチマークをしてみました (ベンチマークの内容は本稿で使用したのと同じものです)。 その結果が table10 です。

table10: ベンチマーク結果

言語 ツール 時間(sec)
Ruby MyEruby7 (interporation) 10.42
Ruby MyEruby8 (cached) 8.02
Ruby MyEruby9 (def_method) 5.11
Ruby eruby 15.32
Ruby ERB 42.67
Ruby Erubis 12.78
Ruby Erubis (cached) 9.15
Ruby Erubis::Fast (cached) 8.22
Perl Template-Toolkit 26.40
Python Django 50.55
Python TurboGears(Kid) 344.16
Java Velocity 13.24

使用したソフトウェアのバージョンは次の通りです。

ここで大事なのは、どれが速くてどれが遅いかということではなく、もちろん「○○という機能がある/ない」という話でもなく、言語の速度がそのままアプリケーションの速度につながるわけではないということです。

Java が速いからといって、Java で作られたアプリケーションも速いとは限りません。 Ruby が遅いからといって、Ruby で作られたアプリケーションも遅いと決めつける必要はありません。 少なくとも、Reflection を多用したような動的な Java プログラムよりも素の Ruby プログラムのほうが速いです。

また eRuby は Velocity や JSP より速くできる*2ので、Web アプリケーションのビュー層に限っていえば、Java で性能要件が満たせられるなら Ruby でも満たすことができるはずです。 今回のデータを見れば、10,000 ページ生成するのにノート PC で 10 秒もかかってないわけですから、ビュー層としては充分な速度でしょう。 つまりスクリプト言語でも工夫次第で十分な速度がだせるのです。

アプリケーションの速度は、アルゴリズムやデータ構造、使用するライブラリやデータベースなど、様々な要因が絡んできます。 言語の速度は、アプリケーションの速度を決定する要因の 1 つでしかなく、それよりも他の要因の方がずっと大きいということを知っておいてください。

まとめ

本稿では eRuby の処理系を題材にとり、C言語 による拡張モジュールより高速 な pure Ruby プログラムを作成しました。 速度は、キャッシュなしで約 1.5 倍高速、キャッシュありなら約 2 倍高速、さらに関数化すれば約 3 倍高速になりました。 また他言語でのライブラリと比較しても、圧倒的に速いことがわかりました。

繰り返しになりますが、本稿でいいたかったことは、言語の速度とアプリケーションの速度は別物であり、アプリケーションの高速化には言語の速度よりも知識やアイデアの方が重要だということです。 アプリケーションの速度を決定する要因はいくつもあり、言語はそのうちのひとつに過ぎず、かつその影響度は一般的に考えられているよりも低いのです*3

スクリプト言語では最高速度を得ることはできませんが、たいがいのシステムが必要とする速度は十分得られます。 あなたが開発したアプリケーションが遅いとしたら、ほとんどの場合においてそれは Ruby やスクリプト言語のせいではなく、あなた自身のせいです。 安易に言語のせいにするまえに、自分のプログラムを見直してみましょう。 またスクリプト言語を使うなら、どれだけ速いかにこだわるのではなく、自分が必要とする速度がでればそれでよいという考えを持ちましょう。

なおテンプレートエンジンの速度に興味がある方は、Tenjin の Web ページに様々な言語のテンプレートエンジンを使ったベンチマーク結果が公開されていますので、そちらをご覧下さい。

本稿がスクリプト言語の発展に役立てば幸いです。

*1 もちろんアプリケーションの特性に依ります。

*2 JSP は Velocity より遅いことが知られているため、Velocity より速ければ JSP よりも速いといえます。ただし、昔の JSP は Velocity の半分程度の速さだったのが、最近はかなり改善されています。なお table10 に JSP がないのは、JSP の実行には Servlet コンテナが必要であり、テンプレートエンジンのように単体で実行することができないためです。

*3 繰り返しますがアプリケーションの特性に依ります。