faml と slim、hamlit のパフォーマンスの差
http://k0kubun.hatenablog.com/entry/2015/03/31/004021 を見て、「faml は slim と同等とか言いながら slim よりずっと遅いじゃん」と思われると悔しいので一応解説しておく。 なお、このエントリは slim 3.0.3 と hamlit 0.4.2 に基いている。
なぜこのベンチマークで faml が遅いのか、結論から言うと、この中で faml だけ自動 html エスケープが有効になっているからだ。 haml はデフォルトでは自動 html エスケープは無効であり、hamlit もそれに倣っている。
slim のベンチマークで用いている slim のビューでは ==
を使って明示的に html エスケープを無効化している https://github.com/slim-template/slim/blob/v3.0.3/benchmarks/view.slim 。
そのため、faml だけが自動 html エスケープを行っている。
faml が行ってる slim とのベンチマーク比較では、元々の view.slim から ==
を =
に変更して html エスケープを有効にしている。
そして haml, hamlit でも escape_html: true
というオプションを渡して slim のベンチマークを実行すると、travis-ci 上のある実行では以下のような結果になった。
https://travis-ci.org/eagletmt/faml/jobs/57304778
ruby benchmark/slim.rb Calculating ------------------------------------- Haml 1.261k i/100ms Faml 3.741k i/100ms Hamlit 4.369k i/100ms Slim 3.846k i/100ms ------------------------------------------------- Haml 14.524k (± 2.6%) i/s - 73.138k Faml 52.477k (± 2.3%) i/s - 265.611k Hamlit 64.082k (± 2.8%) i/s - 323.306k Slim 55.219k (± 1.7%) i/s - 276.912k Comparison: Hamlit: 64082.1 i/s Slim: 55219.4 i/s - 1.16x slower Faml: 52477.4 i/s - 1.22x slower Haml: 14524.2 i/s - 4.41x slower
faml はたしかに slim より遅い結果になっているが、それほど大きな差は無い。
実際、このベンチマークで生成される Ruby のコードにはほとんど差が無い。差といえば、Faml::Helpers
を extend してるかどうか、出力する html に改行文字が含まれるかどうか、余分な変数代入や to_s
があるかどうか、くらいだ。
% bundle exec slimrb --compile benchmark/view.slim _buf = []; _buf << ("<!DOCTYPE html><html><head><title>Simple Benchmark</title></head><body><h1>".freeze); ; ; ; ; ; _buf << (::Temple::Utils.escape_html((header))); ; _buf << ("</h1>".freeze); unless item.empty?; ; _buf << ("<ul>".freeze); ; for i in item do; ; if i[:current]; ; _buf << ("<li><strong>".freeze); ; _buf << (::Temple::Utils.escape_html((i[:name]))); ; _buf << ("</strong></li>".freeze); else; ; _buf << ("<li><a".freeze); ; _slim_codeattributes1 = i[:url]; case (_slim_codeattributes1); when true; _buf << (" href=\"\"".freeze); when false, nil; else; _buf << (" href=\"".freeze); _buf << (::Temple::Utils.escape_html((_slim_codeattributes1))); _buf << ("\"".freeze); end; _buf << (">".freeze); _buf << (::Temple::Utils.escape_html((i[:name]))); ; _buf << ("</a></li>".freeze); end; end; _buf << ("</ul>".freeze); else; ; _buf << ("<p>The list is empty.</p>".freeze); ; end; _buf << ("</body></html>".freeze); _buf = _buf.join
% bundle exec faml compile benchmark/view.haml _buf = []; extend ::Faml::Helpers; _buf << ("<!DOCTYPE html>\n<html>\n<head>\n<title>Simple Benchmark</title>\n</head>\n<body>\n<h1>".freeze); ; ; ; ; ; ; _faml_compiler1 = (header; ; ); _buf << (::Temple::Utils.escape_html((_faml_compiler1.to_s))); _buf << ("</h1>\n".freeze); unless item.empty?; ; _buf << ("<ul>\n".freeze); ; for i in item; ; if i[:current]; ; _buf << ("<li>\n<strong>".freeze); ; _faml_compiler2 = (i[:name]; ; ); _buf << (::Temple::Utils.escape_html((_faml_compiler2.to_s))); _buf << ("</strong>\n</li>\n".freeze); else; ; _buf << ("<li>\n<a".freeze); ; _faml_html1 = (i[:url]); case (_faml_html1); when true; _buf << (" href".freeze); when false, nil; else; _buf << (" href='".freeze); _buf << (::Temple::Utils.escape_html((_faml_html1))); _buf << ("'".freeze); end; _buf << (">".freeze); _faml_compiler3 = (i[:name]; ; ); _buf << (::Temple::Utils.escape_html((_faml_compiler3.to_s))); _buf << ("</a>\n</li>\n".freeze); end; end; _buf << ("</ul>\n".freeze); else; ; _buf << ("<p>The list is empty.</p>\n".freeze); ; end; _buf << ("</body>\n</html>\n".freeze); _buf = _buf.join
さて、このベンチマークではたしかに hamlit が faml や slim をはっきり上回っている。hamlit 0.4.2 がどのような Ruby のコードを生成しているか見てみる。
% bundle exec ruby -e 'require "hamlit"; puts Hamlit::Engine.new(escape_html: true).call(File.read("benchmark/view.haml"))' _buf = []; _buf << ("<!DOCTYPE html>\n<html>\n<head>\n<title>Simple Benchmark</title>\n</head>\n<body>\n<h1>".freeze); ; ; ; ; _buf << (::Temple::Utils.escape_html(((header).to_s))); _buf << ("</h1>\n".freeze); ; unless item.empty?; _buf << ("<ul>\n".freeze); for i in item; if i[:current]; _buf << ("<li>\n<strong>".freeze); _buf << (::Temple::Utils.escape_html(((i[:name]).to_s))); _buf << ("</strong>\n</li>\n".freeze); ; ; ; else; _buf << ("<li>\n<a href='".freeze); _buf << (i[:url]); _buf << ("'>".freeze); _buf << (::Temple::Utils.escape_html(((i[:name]).to_s))); _buf << ("</a>\n</li>\n".freeze); ; ; end; ; end; ; _buf << ("</ul>\n".freeze); ; ; else; _buf << ("<p>The list is empty.</p>\n".freeze); ; end; ; _buf << ("</body>\n</html>\n".freeze); ; ; _buf = _buf.join
header
や for i in item
のような埋め込み式の行番号がずれてしまってる問題は今回は無視するとして*1、よく見てみると i[:url]
がそのまま埋め込まれている。
これには2つの問題がある。
というわけで、この差によって slim のベンチマークにおいて hamlit が高速になっていると考えられる。
最後に恣意的に用意したマイクロベンチマークの結果を自慢しておくと、実行時に Hash のマージと attribute 全体のレンダリングを行う必要があるケースでは faml が最も高速になっている。 というか、slim は文法はともかくコンパイラは本当によくできていて、この点くらいしかぼくには勝てそうにない。 先程と同じ travis-ci 上での実行から引用すると、
ruby benchmark/rendering.rb benchmark/attribute_builder.haml benchmark/attribute_builder.slim Calculating ------------------------------------- Haml 693.000 i/100ms Faml (Array) 2.417k i/100ms Faml (String) 2.064k i/100ms Hamlit (Array) 1.896k i/100ms Hamlit (String) 1.712k i/100ms Slim (Array) 2.005k i/100ms Slim (String) 1.653k i/100ms ------------------------------------------------- Haml 7.767k (± 4.4%) i/s - 38.808k Faml (Array) 30.186k (±17.5%) i/s - 147.437k Faml (String) 28.314k (±10.8%) i/s - 140.352k Hamlit (Array) 22.381k (±20.5%) i/s - 106.176k Hamlit (String) 21.914k (±10.4%) i/s - 109.568k Slim (Array) 26.194k (± 9.7%) i/s - 130.325k Slim (String) 20.715k (± 5.7%) i/s - 104.139k Comparison: Faml (Array): 30186.2 i/s Faml (String): 28313.7 i/s - 1.07x slower Slim (Array): 26194.2 i/s - 1.15x slower Hamlit (Array): 22381.0 i/s - 1.35x slower Hamlit (String): 21914.2 i/s - 1.38x slower Slim (String): 20714.7 i/s - 1.46x slower Haml: 7766.6 i/s - 3.89x slower
となっている。haml 遅いですね……