Slimより高速なHaml実装「Hamlit」
RubyでHTMLを生成するのにERB以外でよく使われるテンプレート言語にHamlやSlimがあります。haml *1 をやめて高速なslimに移行する人が多かったのですが、私はHamlのシンタックスの方が好きなので、slimが用意したベンチマークでslimより高速なHaml実装「Hamlit」を3月にリリースしました。 *2
これはslimが提供しているベンチマークでHTML escapeを有効にし *3、FamlとHamlitを追加したベンチマークの結果です。なおHamlitは完全にHaml互換の仕様ではなく、この非互換が有利に働いています。
互換性と性能が大幅に向上したHamlit v2.0
Hamlitの互換性の問題
Hamlitは最初のv0.1の時点で上記のようなベンチでSlimより高速ではあったのですが、以下のような欠陥がありました。
- HTMLエスケープがない
- エラーがコンパイル時に出る & ランタイムで出ても行番号がズレる
- コーナーケースで属性のレンダリングが重複したり、消えたりする
- 最適化の影響で、書き方が静的か動的かでHTMLのレンダリング結果が変わってしまう
- 空白除去オペレータの挙動がおかしい
社内の20,000個以上のテストケースがあるRailsアプリで使ってみたら5,000個くらいのテストが落ちる状態でした。 以前、これを地道に潰しながら致命的な仕様の見直しを行ったHamlit v1.0.0をリリースしました。v1のどこかのバージョンでそのアプリの全てのテストが通っています。
それでも上記の問題が完全に潰せていたわけではなく、保守性的にも様々な問題を抱えていたため、2ヶ月かけてフルスクラッチを行い、大幅に互換性を改善したHamlit v2.0をリリースしました。
もう一つのHaml実装「Faml」との戦い
Hamlitより先にリリースされたhamlより高速なHaml実装としてFamlがあります。 slimが提供しているベンチマーク(をFamlが公平に参加できるようにしたもの)ではHamlitの非互換性が有利に働きHamlitの方が高速に動作しますが、その他の多くのベンチマークではC拡張を持っているFamlの方が高速に動作する状態でした。
Famlが恣意的に用意したベンチマークでは、1.35倍程度Hamlitの方が遅い状態でした。 さて、 v2.0ではHamlitもC拡張を用意 してC言語レベルでの最適化を行い、上記のような動的なテンプレートでも以下のようにHamlitの方が高速に動作するようになりました。 (Travisでの結果)
hamlit2 v2.0.1: 24225.3 i/s (0.041ms) faml v0.7.0: 19405.8 i/s (0.052ms) - 1.25x slower hamlit1 v1.7.2: 10450.2 i/s (0.096ms) - 2.32x slower haml v5.0.0.beta.2: 5260.5 i/s (0.190ms) - 4.61x slower
でも昨日FamlがC++になって速くなってるので、明日には抜かれているかもしれません。
静的解析エンジン
FamlはRubyの式が静的であればコンパイル時にレンダリング結果を確定させる機構を持っているのですが、Hamlitのそれはかなり貧弱のため、そういったケースでもレンダリング速度で劣っていました。 真面目に実装しなおしたため、上記のテンプレートの値をFamlやHamlitが解析可能な位置にズラしたベンチマークでは、 本家hamlの569倍 の速度が出るようになりました。 (Travis)
hamlit2 v2.0.1: 3639877.1 i/s (0.000ms) faml v0.7.0: 1434853.5 i/s (0.001ms) - 2.54x slower hamlit1 v1.7.2: 17734.8 i/s (0.056ms) - 205.24x slower haml v5.0.0.beta.2: 6388.2 i/s (0.157ms) - 569.78x slower
コンパイル速度の問題
これまで出てきたのは全てレンダリング速度に関するベンチマークですが、本来はコンパイル速度も全体のパフォーマンスに影響し得ます。 Webサービスではあるテンプレートのコンパイル回数に比べてレンダリング回数の方がかなり多くなるためコンパイル速度は無視できますが、Hamlitはコンパイルが非常に遅く、レンダリング回数がそれほど多くないタスクで使うには必ずしもHamlitをオススメできない状態でした。
これはhamlのリポジトリにあるstandard.hamlという少し大きめのテンプレートですが、コンパイル速度でFamlに勝てるようになりました。(Travis) コンパイル時に最適化をあまり行わないhamlにはまだ勝てていません。
haml v5.0.0.beta.2: 297.5 i/s (3.361ms) hamlit2 v2.0.1: 152.6 i/s (6.551ms) - 1.95x slower faml v0.7.0: 139.4 i/s (7.173ms) - 2.13x slower hamlit1 v1.7.2: 78.0 i/s (12.827ms) - 3.82x slower
RubyKaigiでどうやって速くしたか話します
RubyKaigi 2015に、Famlの作者である @eagletmt さんと一緒に登壇します。一つの枠で二人のアイデアが聞けるのでお得です。 どう速くしたのかの詳細はあえてこの記事では書きませんでしたが、何を考えて作ったのかとかも含めRubyKaigiで話す予定です。是非聞きに来てください!!
*1:言語に関してはHaml、gemに関してはhamlで表記します
*2:Hamlit v0.1.0リリース時の記事: http://k0kubun.hatenablog.com/entry/2015/03/31/004021
*3:元のベンチではSlimもERBもRubyのスクリプトに対してHTML escapeを行わないオペレータを使っているんだけど、HamlにはHTML escapeされるオペレータを使っているのでSlimやERBでもHTML escapeされるように直してる。HTML escapeしない側に倒さないのは、ベンチに加えたいFamlにはHTML escapeしないオプションがないのと、Railsの標準だとescapeされるため。