slim-template/slimのcompiled benchでオリジナルのhamlに比べ8倍高速に動作するhaml実装をリリースしました。
なぜ高速なHaml実装を作ったのか
個人的にhamlのシンタックスのほうが好きなので、「hamlは遅いからslimを使う」みたいな人を減らしたかったから。以前slimの普及に貢献したんだけど、気が変わったのでhamlを応援することにした。
実は他にも既にeagletmt/famlという高速なHaml実装が存在していたんだけどベンチを走らせたらslimより遅かったので、slimを打倒するべく再実装した。
どのくらいHamlより速いのか
自分の実装に都合のいいベンチマークを作るのは簡単なので、公平性を期すためにslim-template/slimのcompiled benchと同じものを使い、誰でも同じ環境が使えるtravisで走らせた。結果はこちら。
hamlit: 133922.9 i/s erubis: 123464.1 i/s - 1.08x slower slim: 110404.3 i/s - 1.21x slower faml: 92009.3 i/s - 1.46x slower haml: 15810.4 i/s - 8.47x slower
こういうhamlを1秒に1,339,22回レンダリングできるらしい。 本家の8.47倍 のベンチが出た。
ちなみにこのベンチはhamlをパースする時間を含んでいない純粋なレンダリング速度のベンチになっている。なぜこのベンチを見ているかというと、実際にRailsとかで使うときは最初にhamlがRubyにコンパイルされてActionViewにメソッドが生え、それをリクエストが来るたびに呼ぶという挙動になっているので、実際にはRubyにコンパイルする時間はあまりパフォーマンスには影響がないため。
なぜHamlより速いのか
レンダリング速度を速くするには、文字列を返すRubyのコードを最適化していくということになる。高速化のために意識したことを書く。
Templeのfilterを使う
Templeというテンプレートエンジンを実装するためのフレームワークがある。これはテンプレートをパースするクラスを作ってTempleで扱えるS式を生成すると、htmlに変換してくれるというもの。Templeはコンパイルの過程で最適化のためのフィルタを差し込むことができ、これが相当賢いのでTempleで実装してフィルタをいくつか刺すだけで結構速くなる。
さらなる高速化のためにオリジナルのフィルタのアイデアを1つ考えていたんだけど、実際にはフィルタを作ってみると速くなると思っていたベンチの結果が悪化してしまい、これ以上の最適化は難しいなという結論に達した。
ランタイムで余計なことをしない
Hamlはhtmlタグの要素が静的にコンパイル可能でもランタイムで毎回生成している。あとはタグにclassが複数あったらアルファベット順に並ぶみたいな機能があり、そういうのもラインタイムで走る。コンパイラが賢ければランタイム時の余計な処理を減らすことができるので、なるべくそうなるようにした。Templeの件とこのへんまではeagletmt/famlと大体同じだけど、内部実装は結構違くなってる。Famlはどうしてもランタイムでattributeを生成しなければならない場合はC拡張で動くのでその場合はFamlのほうが速そう。
あとはHamlは毎回ビューでHaml::Utils
をextendしてるんだけど、Railsの場合はkaminariみたいにActionView::Base
に直接includeさせちゃえばいいのでは?と思ったのでそうした。
地道に生成コードを確認して余計なコードを減らす
パフォーマンスチューニングはプロファイリングと地道な改善が重要だと思っていて、たとえば仕様上必要ない.to_s
呼び出しをなくすとか、無駄な変数代入や制御構文が生成されないように工夫したりとか、なるべくstring interpolationを使うようにコンパイルするとか、何度も生成コードを見て余計な処理を削った。CIでベンチを走らせ、遅くなっていたら理由を調べて直した。そういう地道な改善の結果、slimやRails標準のerubisより速いベンチが出るようになった。
本当は考えていたオリジナルのフィルタでもっと速いコードが生成できる予定だったんだけど、rblineprofのプロファイル結果を見ながら構想していたものが実際のベンチだと遅くなってしまうという罠にハマり、あまり派手な改善はできなかった。
Hamlとの互換性は?
haml/haml-specというhaml実装の正しさをチェックするためのテストスイートがある。このテストケース全てでHamlと全く同じ文字列を生成するテストを全部通っているので、大体haml互換で動くと思う。
あと、hamlはattributeのパースが結構怪しい感じなんだけど(例えば%span{ a: '}' }
はエラーになる)、字句解析に標準ライブラリのRipperを使って実装したので、文字列中に{
や}
が入っていてもちゃんと理解してパースしてくれる。本家よりちゃんと動く可能性がある(?)。最近書いたRailsアプリで使い始めた。
使ってみてください
ベンチで競争したものの、正直テンプレートエンジンとか何使ってもそんな全体のパフォーマンスに影響ないと思う。けどHamlはちょっと遅すぎるので、FamlなりHamlitなりを使ったほうが良いという見解です。