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

headerfor i in item のような埋め込み式の行番号がずれてしまってる問題は今回は無視するとして*1、よく見てみると i[:url] がそのまま埋め込まれている。 これには2つの問題がある。

  1. &< 等が html エスケープされない
    • これはさすがに厳しいのでは*2
  2. i[:url]nil や false だったとき、<a href='false'> のような出力になる
    • haml では、属性の値が nil または false のときは、その属性を出力しない仕様
    • ちなみに slim が生成したコードを見てわかるように、slim も同じ仕様
    • ただ、これに関しては「hamlit ではこういう仕様です」と言うこともできると思う

というわけで、この差によって 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 遅いですね……