こないだ言ってたPlackアプリのサンプルでは、出来るだけ普段使ってない構成にしようと目論んでて、そのために例えばテンプレートエンジンにはText::MicroTemplateを使ってみたりしている。

Text::MicroTemplateはシンプルで軽くてなかなか良かった。ちなみにこんな感じで使う。

use Text::MicroTemplate qw(:all);

my $html = render_mt('Hello, <?= $_[0] ?>', 'faultier')->as_string;

でもファイルから読みたいよねーそれ実装しなきゃなのかなーとか思ってたらちゃんとText::MicroTemplate::Fileってのがあって、こっちを使えばキャッシュも使ってくれるしラッパーの機能とかもあるしで素敵だ。

?# hello.mt
Hello, <?= $_[0] ?>
use Text::MicroTemplate::File;

my $mtf = Text::MicroTemplate::File->new(use_cache => 1);
my $html = $mtf->render_file('hello.mt', 'faultier');

で、ここまでちょこちょこ弄ってみて思ったけど、やっぱりviewの中で@_とかを触るのはなんか嫌だ。そりゃ、引数にHashRef渡してやるとかテンプレートの頭で変数に入れてやるとかすればいいんだろうけど、そういう生々しい情報はviewで扱いたくない。つまり、やりたいのはこういうこと。

?# hello.mt
Hello, <?= $name ?>
use Text::MicroTemplate::File;

my $mtf = Text::MicroTemplate::File->new(use_cache => 1);
my $html = $mtf->render_file('hello.mt', { name => 'faultier' });

で、どの辺をいじればいいんだろうと読んでたら、Text::MicroTemplateのbuildメソッドの中身はこんな風になっていた。

    my $expr = << "...";
package $_mt->{package_name};
sub {
    ${_mt_setter}local \$SIG{__WARN__} = sub { print STDERR \$_mt->_error(shift, 4, \$_from) };
    Text::MicroTemplate::encoded_string((
        $_code
    )->(\@_));
}
...

ふむふむ。この$_mt_setterってなんだろうなと見てったら、デフォルトでは空、つまり何もしないんだけど、ここに適当なコードを突っ込むとテンプレートを処理する前にやることを追加できるようだ。

use Text::MicroTemplate qw(:all);

$Text::MicroTemplate::_mt_setter = 'my $name = shift;';
my $html = render_mt('Hello, <?= $name ?>', 'faultier')->as_string;

おお、出来た。んでも実はText::MicroTemplate::Fileの方ではこれはできない。build_fileの中でbuildを呼ぶ前に

local $Text::MicroTemplate::_mt_setter = 'my $_mt = shift:';

ってやっちゃってるし、テンプレートに渡す引数がわかるのはrender_fileの段階だし、大体これ多分外から挙動を制御する用じゃないよなぁ。というわけで当初は自前でモジュール組んでたんだけど、良く考えたら事前に引数を名前付きで渡せないこと以外は概ねText::MicroTemplate::Fileで良いわけで、作ってるうちにどんどん似てきてしまったのでじゃあText::MicroTemplate::Fileを拡張すればいいじゃんってことになった。それでこんなものを作ってみた。

faultier's p5-text-microtemplate-file-bindvars at master - GitHub

use Text::MicroTemplate::File::BindVars;

my $mtf = Text::MicroTemplate::File::BindVars->new(use_cache => 1);
my $html = $mtf->render_file('hello.mt', { name => 'faultier' });

動いたー。一応Text::MicroTemlate::Fileと同等の内容のテスト通るし、ベンチも取ってみたらキャッシュ無しのときでほんのちょびっと速いくらい、キャッシュ有りだとちょっと残念なことに15%ほどもパフォーマンス落ちちゃうけどそれでもTTの4、5倍くらい速いしまぁいっかーなんて思ったりしました。どうせここ全然ボトルネックじゃないし。

とそんな感じで一通り書き終えてこの記事をまとめてるところでこんなものがあるのに気付いた。

Text::MicroTemplate::Extended - search.cpan.org

…これでいいじゃん。マクロとかブロックとかあるし高機能じゃん。最初に良く調べなさいよって話ですねほんと。CPANで関係ありそうなモジュール探すとか基本中の基本じゃん。残念すぎる。

せっかくなので取ったベンチ結果。

Benchmark: timing 10000 iterations of T::MT::E, T::MT::F, T::MT::F::BV, TT...
  T::MT::E: 16 wallclock secs (16.01 usr +  0.61 sys = 16.62 CPU) @ 601.68/s (n=10000)
  T::MT::F: 17 wallclock secs (16.00 usr +  0.39 sys = 16.39 CPU) @ 610.13/s (n=10000)
T::MT::F::BV: 16 wallclock secs (15.85 usr +  0.39 sys = 16.24 CPU) @ 615.76/s (n=10000)
        TT: 46 wallclock secs (45.60 usr +  0.55 sys = 46.15 CPU) @ 216.68/s (n=10000)
              Rate           TT     T::MT::E     T::MT::F T::MT::F::BV
TT           217/s           --         -64%         -64%         -65%
T::MT::E     602/s         178%           --          -1%          -2%
T::MT::F     610/s         182%           1%           --          -1%
T::MT::F::BV 616/s         184%           2%           1%           --
Benchmark: timing 10000 iterations of T::MT::E, T::MT::F, T::MT::F::BV, TT...
  T::MT::E:  1 wallclock secs ( 1.04 usr +  0.06 sys =  1.10 CPU) @ 9090.91/s (n=10000)
  T::MT::F:  1 wallclock secs ( 0.75 usr +  0.05 sys =  0.80 CPU) @ 12500.00/s (n=10000)
T::MT::F::BV:  1 wallclock secs ( 1.10 usr +  0.06 sys =  1.16 CPU) @ 8620.69/s (n=10000)
        TT:  5 wallclock secs ( 4.92 usr +  0.00 sys =  4.92 CPU) @ 2032.52/s (n=10000)
                Rate           TT T::MT::F::BV     T::MT::E     T::MT::F
TT            2033/s           --         -76%         -78%         -84%
T::MT::F::BV  8621/s         324%           --          -5%         -31%
T::MT::E      9091/s         347%           5%           --         -27%
T::MT::F     12500/s         515%          45%          37%           --

できること少ないのに負けてるじゃん…。ちなみに、T::MT::EとT::MT::F::BVは全く同じテンプレートがレンダリングできたので同じの使ってる。キャッシュの仕方が悪いんだろうなー。でもT::MT::Eの方はオブジェクト生成時に引数指定しなきゃだから、複数ファイルをレンダリングさせたらちょっと変わったりするかしら。あとはまぁ高度なことをやらせればその分速度落ちるけど、そもそもT::MT:FとT::MT::F::BVには出来ないことなのでそこで差がついたとしてもT::MT::Eを選ぶなぁ。しかしそれにしてもTT遅いな。残念な子ですわね。

追記

やってみたけど大してかわんない。そもそも何万何十万て回数やるうちの数回オブジェクト生成が増えたところで差が付くわけもないだろっつー。あまりに意味ないのでもうちょい見直そう。

さらに追記

ちょっと試行錯誤してみたけど上手くいかなかった。思い付きで作ったものじゃ勝てないかー。ほとんど何もやってないんだけどな…。で、単純に1ファイルだけのテンプレートでキャッシュ有りだと当然のことながらText::MicroTemplate::Fileのがずっと速い(余計なことしてないから)。十万回やったら3秒くらい差がついた。T::MT::EとT::MT::F::BVの差はまぁ数%ってところ。十万回やって0.3秒違うだけだしTTとの40秒の差とくらべたらなんてことないかな。

ラッパー有りだと細かい処理の差よりそっちのが重いのでT::MT::FとT::MT::F::BVの差が3割弱から1割強まで縮まる。悪くはない。是非T::MT::Eのテンプレート継承機能との差を試したかったんだけどなんか上手く動かせなくて悩み中。TTは十万回やると1分くらい帰ってこなくなるのでもうなんかいいやって感じになってきた。いや、うちのサービスのほとんどがTT使ってるんだからキャッシュの仕組みとか工夫してさえいれば大した問題じゃない(だってDBとかのがずっと遅いんだもの)のはわかってるんだけど、こんだけ速度に差が付いてて、素のPerlで書けて、HTMLのエスケープもデフォルトでやってくれて、とか考えたらこっち使いたくなるねぇ。