Erubis の Preprocessing 機能を使って Ruby on Rails の View 層を高速化する
初稿:2007-09-29
書いた人:桑田 誠
はじめに
Erubisとは、eRuby 処理系の一つであり、eruby や ERB の代替として使用できます。特長は、動作が高速なことと、拡張性が非常に高いことです。また Ruby on Rails を標準でサポートしています。
本稿では、Ruby on Rails において Erubis の Preprocessing 機能を使う方法を紹介します。Preprocessing 機能を使うと、Ruby on Rails におけるヘルパー関数の呼び出しが大幅に高速化されます。ベンチマークの結果では、最大で約2倍速くなりました。
なお本稿執筆時点での Erubis の最新版は 2.4.0 です。以降ではこのバージョンを使って説明します。
Erubis について
Erubisとは、eruby や ERB と同じく eRuby 処理系の一つです。次のような特長があります。
- pure Ruby でありながら極めて高速(ERB の約3倍、eruby の約1.1倍)
- Ruby 以外のプログラミング言語に対応(PHP, C, Java, JavaScript, Perl, Scheme)
- 埋め込みの書式を変更可能(デフォルトは「<% %>」)
- 「<% %>」の前後の空白と改行を自動的に削除
- 「<%= %>」がデフォルトでHTMLエスケープするように設定可能
- Binding オブジェクトの代わりに Hash オブジェクトを使用可能
- YAML ファイルの読み込みをサポート
- Enhancer と呼ばれるモジュール群を使って様々な拡張が可能
- 行頭における「%」のサポート
- print文のサポート
- など
- Ruby on Rails を標準でサポート
本稿ではこれらの詳細については説明せず、Erubis を Ruby on Rails で使う方法についてのみ説明します。
Erubis のインストール
Erubis をインストールするには、RubyGems を使う方法と、setup.rb を使う方法の2つがあります。
RubyGems を使う場合は、管理者権限で「gem install erubis」を実行してください。
RubyGems が使えない場合は、Erubis をダウンロードして管理者権限で setup.rb を実行してください。
インストールが成功したかどうかを確かめるために、簡単なスクリプトを実行してみましょう。
次のように表示されれば成功です。
Ruby on Rails で Erubis を使う
Ruby on Rails で Erubis を使うには、erubis/helpers/rails-helper.rb を使います。
config/environment.rb の最後に以下を追加してください。
この設定により、Ruby on Rails における eRuby エンジンとして、ERB のかわりに Erubis が使われるようになります。
Webサーバを再起動し、ログに「Erubis 2.4.0」と出力されていることを確認してください。
以下は設定オプションの説明です。
- Erubis::Helpers::RailsHelper.engine_class
- eRubyファイルをRubyスクリプトに変換するクラスを指定します。デフォルトは Erubis::Eruby ですが、Erubis::FastEruby にすると若干速くなります。
- Erubis::Helpers::RailsHelper.init_properties
- Erubis::Eruby.new() に渡すオプションです。通常は指定する必要はありません。
- Erubis::Helpers::RailsHelper.show_src
- コンパイル結果をログファイルに出力するかどうかを指定します。true なら出力し、false なら出力しません。また nil であれば、development モードのときだけ出力します。デフォルトでは nil です。
- Erubis::Helpers::RailsHelper.preprocessing
- Preprocessing 機能を有効にします(Preprocessing 機能については後述)。デフォルトは false なので、ここでは true に設定してください。
Preprocessing 機能
Erubis では、Ruby on Rails のために Preprocessing 機能が用意されています。これを使うと、Ruby on Rails の View 層を大幅に高速化できます。
このセクションでは、Preprocessing 機能について解説します。
Preprocessing 機能の概要
Erubis の Preprocessing 機能とは、eRuby 文字列を Ruby スクリプトに変換する際に、埋め込まれたロジックの一部を実行する機能です。これにより、変換された Ruby スクリプト の実行が高速になります。
詳しく説明しましょう。ERB や Erubis のような eRuby 処理系の動作は、次のように2つのステージに分けられます。
- eRuby 文字列を、Ruby スクリプトに変換する(変換ステージ)。
- 変換後の Ruby スクリプトを eval などで実行する(実行ステージ)。
ここでは前者を変換ステージ、後者を実行ステージと呼ぶことにします。
ここで大事なことは、同じ eRuby 文字列を何度も実行するときに、__実行ステージはその都度実行する必要があるが変換ステージは1回だけでよい__ということです。
例えばファイル list.rhtml を100回実行するとき、これを Ruby スクリプトに変換するのは最初の1回だけでよく、毎回変換する必要はないということです。
実際、Ruby on Rails では最初に list.rhtml を Ruby のメソッドに変換し、あとはこのメソッドを実行しています。
さて、通常の eRuby 処理系では、eRuby 文字列に埋め込まれた Ruby コード(文および式)は、実行ステージで実行されます。このうち、一部のコードを変換ステージで実行するのが Preprocessing 機能です。
つまり、__事前に実行できるコードはあらかじめ実行しておく__ことで、実行ステージでの負荷を減らし、View 層を高速化しようというわけです。
Preprocessing 機能のサンプル
Erubis では、Preprocessing 用の埋め込み書式を用意しています。
- 「[% 文 %]」は、Preprocessing 用の埋め込み文を表します。
- 「[%= 式 %]」は、Preprocessing 用の埋め込み式を表します。
サンプルを見てみましょう。
Ruby on Rails のアプリケーションを作成し、Erubis をインストールします。任意の View ファイルに次のような記述を追加します。
Preprocessing 機能により、「[%= … %]」の部分は変換時に実行され、次のようになります。
そして、最終的に次のような Ruby スクリプトに変換されます。変換結果がログファイルに出力されるので確かめてください。
これを見ると、「<%= 式 %>」は変換後もそのまま残っているのに対し、「[%= 式 %]」は変換時ですでに実行済みであることが分かります。
つまり Preprocessing 機能を使えば、実行ステージでは Ruby 式を実行しなくて済むため、View 層の動作が高速になります。
Ruby on Rails のヘルパー関数である link_to() は、意外と動作コストがかかるうえ、頻繁に実行されます。Ruby on Rails の View 層が遅いのは、実はこういったヘルパー関数の呼び出しと実行が原因です。このコストをなくすことで、View 層はかなり高速化できます。
引数に変数を含む場合
ヘルパー関数の引数に変数を含む場合は、それらを「?(‘…’)」で囲みます。「?()」は引数として文字列をとり、それを「<%=」と「%>」で囲んだ文字列を返すユーティリティ関数です。また erubis/helpers/rails-helper.rb で定義されています。
サンプルを見てみましょう。任意の View ファイルに次のような記述を追加します。
Preprocessing 機能により、これは次のように展開されます。
そして最終的には次のような Ruby スクリプトに変換されます。
これを見ると、「_?(‘…’)」で囲まれた部分は変換時には評価されず、実行時に評価されることがわかります。
link_to() の呼び出しは実行コストが高いですが、「@item.id」の評価だけなら実行コストはとても低いです。つまり「_?()」を使うことで、引数に変数を含むような関数呼び出しも高速化できます。
Partial テンプレートの展開
Rubyist Magazine 19号の記事『RubyOnRails を使ってみる 【第 10 回】 パフォーマンスチューニング』に、def_erb_method() を使って partial テンプレートの呼び出しを高速化する方法が紹介されています。
Preprocessing 機能を使うと、もっと簡単に partial テンプレートの呼び出しを高速化できます。
やってることは、partial テンプレートを読み込んでその場に展開しているだけです。簡単ですね。
Partial テンプレートの中でも Preprocessing 機能を使う場合は、次のようにします。
この場合は、Partial テンプレートを読み込むためのヘルパー関数を定義するとよいでしょう。
これを使うと、先ほどのテンプレートは次のように簡単になります。
ただし Preprocessing 機能を使うと、partial テンプレートのデバッグは困難になります (エラー時の行番号が変わるため)。Preprocessing 機能を使わない状態で充分テストしてから、Preprocessing 機能を使うようにしてください。
ループ展開
Preprocessing 機能を使って、ループをあらかじめ展開しておくこともできます。
例えば、次のような都道府県名のリストがあるとします。
これを使って<select>タグと<option>タグを生成する場合、通常はヘルパー関数を使って次のようにします。
これは次のような eRuby コードとだいたい同じです。
これを見れば分かるように、select()メソッド内では<option>タグを生成するために毎回ループが実行されています。
しかし日本の都道府県は数も名前も決まっており、アプリケーション実行中に変化することはありません。そのため、本来ならば毎回ループを実行する必要はなく、あらかじめ47個の<option>タグに展開しておくことが可能です。
そこで、Preprocessing 機能を使って、ループを事前に実行してしまいましょう。
このコードは Preprocessing 機能により、次のような eRuby スクリプトに変換されます。
これを見ると、ループが事前に実行されていることがわかります。
つまりあらかじめループが展開されるため、実行速度が速くなります。
しかし、このままだと入力エラーがあった場合に、<select> タグの周りに赤い border ラインが引かれません。入力エラーのことも考慮すると、次のように書く必要があります。
最終的に、かなり複雑なコードになりました。もとは「<%= select(‘address’, ‘state’, jp_states()) %>」という1行だけだったのが、Preprocessing 機能を使おうとすると10行になってしまいました。複雑さと速度を天秤に掛け、本当に速度が必要な場面でのみ使うとよいでしょう。
Preprocessing 機能が使える場面と使えない場面
Ruby on Rails のヘルパー関数は、2種類に分けることができます。
- 同じ引数を与えれば同じ戻り値を返す関数は、Preprocessing 機能を使うことができます()。例えば link_to() は同じ引数を与えれば同じ <a> タグが返されるので、Preprocessing 機能を使うことができます。
- 同じ引数を与えても異なる結果を返すような関数は、Preprocessing 機能を使うことができません。例えば text_field() 関数は同じ引数を与えても、入力値のある・なし、エラーのある・なしで、異なる結果が返されるため、Preprocessing 機能は使えません。
これは、他のロジックについても同じことがいえます。「都道府県の一覧を出力する」のように、毎回同じ結果になるロジックは Preprocessing 機能を使うことができます。そうでない場合は、Preprocessing 機能を使うことができません。
現在、多くの Web アプリケーション用フレームワークにおいて、View 層は「テンプレートエンジン」+「ヘルパー関数」という組み合わせになっています。そのため、View 層を高速化しようとすると、テンプレートエンジンだけでなく、ヘルパー関数についても高速化をする必要があります。
今までは、ヘルパー関数を高速化するには、文字通り関数そのものを高速化するしかありませんでした。しかし Preprocessing 機能を前提とすると、関数の実行速度よりも、関数が Preprocessing 可能かどうかが重要になります。例えば text_field() を Preprocessing 可能な仕様に変更できれば、たとえ関数自体の実行速度が遅くても、View 層全体としては大幅に高速化できるでしょう。
つまり Preprocessing 機能は、View 層におけるヘルパー関数のあり方を大きく変える可能性があるといえます。
ベンチマーク
Preprocessing 機能でどのくらい速くなるのかを知るために、また ERB と Erubis とでどのくらい差が出るかを知るために、ベンチマークを実行してみました。以下、その解説です。
ベンチマーク用アプリケーションの作成
以下の手順で、ベンチマーク用のアプリケーションを作成しました。テーブルにデータを挿入する insert_stocks.sql をダウンロードしておいてください。
ここまで来たら、ブラウザで http://localhost:3000/stocks/ にアクセスし、データが表示されることを確認します。
続いて app/controllers/stocks_controller.rb を編集し、StocksController#list() を次のように変更します。
また list1.rhtml と list2.rhtml をダウンロードし、app/views/stocks/ にコピーします。
list1.rhtml は Preprocessing 機能なし、list2.rhtml は Preprocessing 機能ありのテンプレートです。
もちろん Erubis の設定も必要です。セクション『Ruby on Rails で Erubis を使う』の手順に従って、Erubis を設定します。
ここまで来たらサーバを再起動し、ブラウザで http://localhost:3000/stocks/list1 と http://localhost:3000/stocks/list2 にアクセスし、動作を確認します。
ベンチマークとその結果
ベンチマークは、Apache Bench (ab) を使い、1000 リクエストを発行して行いました。
なお環境は MacOS X 10.4 Tiger, Intel CoreDuo 1.83MHz, Memory 2GB, Ruby 1.8.6, Ruby on Rails 1.2.3 です。
実行結果は次の通りです。
name |
sec |
rec/sec |
ERB |
60.060 |
16.65 |
Erubis (Preprocessing なし) |
59.277 |
16.87 |
Erubis (Preprocessing あり) |
32.342 |
30.92 |
これを見ると、次のことが分かります。
- ERB を Erubis に替えただけでは速くならない。
- Preprocessing 機能を使うと、最大で約2倍速くなる。
ベンチマーク結果の考察
ベンチマーク結果では、ERB と Erubis との差はほとんどありませんでした。
Erubis は確かに ERB より速いのですが、それがアプリケーション全体には影響を与えていません。
このことから Ruby on Rails では、eRuby によって費やされている時間はアプリケーション全体の実行時間からするとほんのわずかであることがわかります。つまりアプリケーション自体が遅すぎるため、ERB と比べたときの Erubis の速さなぞ誤差の範囲でしかないのです。
そもそも、ERB にしろ Erubis にしろ、単体で計測すると 1 秒あたり 1000〜2000 ページぐらい楽に生成することができます。それに比べると、1 秒あたり 16 リクエストしか処理できない Ruby on Rails のなんと遅いことでしょうか。これだけ Ruby on Rails が遅いと、単に ERB から Erubis に切り替えたところで何の効果もありません。
また Preprocessing 機能なしと比べると、Preprocessing 機能ありの場合は約2倍速くなっていることがわかります。今回のテンプレートでは、Preprocessing 機能を用いてヘルパー関数 link_to() の呼びだしを大幅に削減しています。このことから、View 層における実行時間の大部分が、ヘルパー関数によるものであることがわかります。
前に『View 層を速くするにはテンプレートエンジンだけでなくヘルパー関数も速くする必要がある』と書きました。Ruby on Rails ではたしかに View 層に時間がかかりますが、__View 層が遅い原因は ERB ではなくヘルパー関数__だったわけです。
今回のテンプレートでは、Preprocessing 機能を使って link_to() の呼び出しを削減しているわけですから、link_to() が比較的重いことがわかります。link_to() の何が重いのかは詳しく調べてみないとわかりませんが、恐らく URL を生成する部分 (つまり url_for() の呼び出し) が遅いのでしょう。やはりというか routes まわりは鬼門ですね。
その他
Erubis に関するその他の話題について紹介します。
Trimモード
ERB には、trim モードという設定があります。trim モードとは、「<% %>」の前後の空白および改行を出力する・しないを設定するためのオプションです。
Ruby on Rails では、trim モードはデフォルトで以下のように設定されています。
- 「<% 文 %>」または「<%= 式 %>」を使うと、前後の空白および改行は削除されない。
- 「<%- 文 -%>」を使うと、前後の空白および改行を削除する。
- 「<%= 式 -%>」を使うと、後ろの改行を削除する。
Erubis にはこのような trim モードは存在せず、次のように動作します。
- 「<% 文 %>」の場合は、前後の空白および改行を削除する。
- 「<%= 式 %>」の場合は、前後の空白および改行は削除されない。
- 「<%- 文 -%>」は「<% 文 %>」と同じとみなされ、「<%-= 式 -%>」は「<%= 式 %>」と同じとみなされる。
この違いを見てみましょう。例えば次のような eRuby コードがあったとします。
これを ERB で実行すると、次のように改行が出力されてしまいます。
Erubis で実行すると、余計な改行が出力されません。
また、「<% %>」の代わりに「<%- -%>」を使ってみます。
これを ERB で実行すると、改行がまったく出力されません。
Erubis だと、先ほどのと同じ結果になります。
ERB と Erubis にはこのような違いがあるので注意してください。
ERB::Util::h() 関数
ERB::Util::h() は、「< > & “」を「< > & "」に変換する関数です。
ERB::Util::h() の定義は次のようになっています。
しかし、これは文字列置換を4回も行っており、大変効率が悪いです。
そこで Erubis では、erubis/helpers/rails-helper.rb において、次のように ERB::Util::h() を再定義してきます。
新しい定義では、文字列置換を1回だけで済ませているため、特にエスケープする文字(「< > & “」)が少ない場合に大幅に高速化されます。
ただしエスケープする文字が多い場合は、新しい定義ではブロック呼び出しのコストが大きくなるため、もとの定義より遅くなる場合があります。そのようなときは、ERB::Util::html_escape() を使ってあげるといいでしょう(ERB::Util::html_escape() は再定義されないため)。
まとめ
本稿では、Erubis の Preprocessing 機能を使うことで、Ruby on Rails の View 層を大幅に高速化する方法を紹介しました。ERB の代わりに Erubis を使うだけではほとんど効果はありませんが、Preprocessing 機能を使うとヘルパー関数の呼び出しを大幅に削減できるため、かなりの高速化が実現できます。
また Preprocessing 機能は、フレームワークにおけるヘルパー関数のあり方を大きく変える可能性があります。なぜなら、関数の実行速度よりも、関数が Preprocessing 可能かどうかのほうが速度に大きく影響するためです。さらに Preprocessing 機能の考え方は、View 層だけにとどまらず、他の部分でも利用できるかもしれません。
本稿が Ruby on Rails の発展に役立てば幸いです。
参考
- Erubis
- 本稿で紹介した eRuby 処理系である Erubis のサイトです。
- RailsExpress.blog - Rails Template Optimizer Beta Test
- ヘルパー関数を事前に Ruby コードに展開することで、View 層を高速化するプラグイン『template optimizer』の紹介です。Preprocessing 機能とは違って Ruby on Rails に特化しており、なんとヘルパー関数を自前で構文解析しているようです。Erubis の Preprocessing 機能と比べ、実装はかなり大変ですが、eRuby テンプレートを一切変更する必要がないという大きな長所があります。