🐙

.NET Text Template Benchmarks

2022/05/26に公開

改定について

C#の日本語Discordサーバー岩永さんあえとすさんに色々おしえていただき、T4とRazorの内容について、下記の点について訂正しました。

  • T4の将来性について不透明な部分が多く、その点に注意が必要であること
  • RazorでSQLのような出力はできないと記載したが、実はできたこと

いつも色々お知恵いただきありがとうございます!

はじめに

動的なSQL組み立てるのに、実行時テキストテンプレート利用しようかな?という思い付きの元、2022年現在、じゃぁテンプレートエンジンなにを使ったらいいのかな?

ということで、いくつかのテンプレートエンジンを調査して、併せてベンチマークを取得してみました。

詳細に興味がありましたら、下記にコードを公開しているのでご覧ください。

注意事項

  • ベンチマーク結果はあくまで「SQLを動的に組み立てる」レベルの簡易なものです
  • テンプレートエンジンのサポートする機能は多様で、ベンチマークがすべてではありません

とは言え、一定の傾向は見られるため公開しておきたいと思います。

計測条件

  • .NET 6.0.5 (6.0.522.21309)
  • Intel Core i9-9900K

計測対象

計測対象 Ver. 補足
T4実行時テキストテンプレート Visual Studio標準のテキストテンプレート
RazorEngine.NetCore 3.1.0 ASP.NET Core MVCで利用できるRazor構文を、ASP.NET Core外でも利用できるように実装されたテンプレートエンジン
Scriban 5.4.5 Liquid互換モードも備えたテキストテンプレート
DotLiquid 2.2.656 Rubyで使われているLiquidの.NET実装。VS CodeなどにExtensionがあり扱いやすい。
Handlebars.Net 2.1.2 利用者が色々拡張できるのが特色っぽい?テキストテンプレート

今回はじめて調べてものも含まれており、あくまで私の主観で間違っている可能性があるので、利用されるときはご注意ください。

計測内容

下記はT4のテンプレートですが、大体つぎのようなSQLの生成処理をベンチマークしています。

<#@ template debug="false" hostspecific="false" language="C#" inherits="TemplateBase" linePragmas="false" #>
select
	*
from
	Employee
where
	1 = 1
<# if (FirstName is not null) { #>
	and FirstName = @FirstName
<# } #>
<# if (LastName is not null) { #>
	and LastName = @LastName
<# } #>

計測結果

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
Intel Core i9-9900K CPU 3.60GHz (Coffee Lake), 1 CPU, 16 logical and 8 physical cores
.NET SDK=6.0.300
  [Host]     : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT  [AttachedDebugger]
  DefaultJob : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT
Method Mean Error StdDev
T4 125.7 ns 2.01 ns 1.78 ns
Razor 1,051.3 ns 2.59 ns 2.30 ns
RazorWithCompile 1,380.5 ns 12.98 ns 11.50 ns
Scriban 14,232.9 ns 113.27 ns 105.95 ns
ScribanWithParse 23,290.5 ns 357.11 ns 316.57 ns
DotLiquid 1,836.4 ns 12.87 ns 11.41 ns
DotLiquidWithParse 13,017.7 ns 90.85 ns 75.87 ns
Handlebars 469.2 ns 2.44 ns 2.16 ns
HandlebarsWithCompile 619,223.6 ns 5,142.62 ns 4,558.80 ns

考察

T4テンプレートが一歩抜け出して早いです。T4とそれ以外を比較すると、やはりT4はデザイン時、つまりVisual Studioで実装時にテンプレートをパースしておけるため、初回実行時のコストが大きく異なります。

ただ、C#の日本語Discordサーバー岩永さんからアドバイス頂いたのですが、T4は開発サイド側が今後積極的に投資していく心づもりがないようです。

実際、今回利用している実行時テキストテンプレートはVisual Studio 2019の時代からテンプレートの動作に不具合があり、手で修正が必要だったりします。そのため採用には慎重になった方が良いのかもしれません。

T4以外を見た場合、Razorはがテンプレートをパースする処理も高速でバランスが良い感じです。後述しますが構文的にも見やすくて好きです。

実行時性能だけみるとHandlebarsが優秀です。しかしHandlebarsはテンプレートのパースがとびぬけて遅いです。とはいえ619[ms]なので利用しがたいというほどではありません。Handlebarsは拡張が自分で作れるようなので、そのあたりが魅力的な人には良いのかな?

今回の目的からみると、DotLiquidも使いやすそうな性能がでています。シンプルなテキストテンプレートに集中したエンジンだからでしょうか?

Scribanは実行性能が他に比べて少し重いですね。それでも14[ms]ですから、体感できるわけではないです。Scribanは、テンプレートエンジン内ではスネークケース(first_name)で、コードからはPascalCase(FirstName)として扱うあたりにちょっと癖があるように感じました。

というわけで私個人的には、性能突き詰めるならT4だけど将来性が怪しく、Razorのバランスが良さそうだという結論になりました。

最後に、各テンプレートエンジンのテンプレートと、実行コードを参考に掲載しておきます。

テンプレートと実行コード

T4

<#@ template debug="false" hostspecific="false" language="C#" inherits="TemplateBase" linePragmas="false" #>
select
	*
from
	Employee
where
	1 = 1
<# if (FirstName is not null) { #>
	and FirstName = @FirstName
<# } #>
<# if (LastName is not null) { #>
	and LastName = @LastName
<# } #>
T4TextTemplate template = new("FirstName", "LastName");
var result = template.TransformText();

Razor

select
	*
from
	Employee
where
	1 = 1
@if (@Model.FirstName != null) {
@:	and FirstName = @@FirstName
}
@if (@Model.LastName != null) {
@:	and LastName = @@LastName
}

個人的には制御構文が一番見やすく、性能もバランスが良いのでおすすめ。

var result = Engine
            .Razor
            .RunCompile(Template, "SqlTemplate", typeof(Model), new Model("FirstName", "LastName"));

Scriban

select
	*
from
	Employee
where
	1 = 1
{{ if first_name }}
	and FirstName = @FirstName
{{ end }}
{{ if last_name }}
	and LastName = @LastName
{{ end }}
var result = Scriban.Template
        .Parse(Template)
        .Render(new Model("FirstName", "LastName"));

テンプレート内部がスネークケースで、プログラムはPascalCaseまたはCamelCaseというのがちょっと個のみじゃないかも。

DotLiquid

select
	*
from
	Employee
where
	1 = 1
{% if FirstName %}
	and FirstName = @FirstName
{% endif %}
{% if LastName %}
	and LastName = @LastName
{% endif %}
var result = DotLiquid.Template
        .Parse(Template)
        .Render(
            Hash.FromAnonymousObject(
                new Model("FirstName", "LastName")));

なんでHash?

Handlebars

select
	*
from
	Employee
where
	1 = 1
{{ #if FirstName }}
	and FirstName = @FirstName
{{ /if }}
{{ #if LastName }}
	and LastName = @LastName
{{ /if }}

if分の、開くと閉じるの指定が独特。はじめて見た。

var result = Handlebars.Compile(Template)(new Model("FirstName", "LastName"));

実行もちょっと独特。

Discussion