さて、前回サロゲートペア対応した湯婆婆を作りました。
この湯婆婆ですが文字列補間を使ってます。$"xxxx{変数}xxxx"
のように書いて文字列中に変数を埋め込むやつですね。
これは裏では確か string.Format
に展開されているらしい?と聞いているのですが、これをやると裏で object の配列1つ、さらに埋め込んでいる変数がプリミティブ型だと Boxing が起きるという感じでパフォーマンスを重要視する人たちにとっては結構辛いもののようです。
湯婆婆に働き方改革
ということで、べたっと StringBuilder に Append しまくるのと文字列補間でどれくらい違うものなのか測ってみようと思います。.NET でベンチマークといったら BenchmarkDotNet がデファクトです。ということで、サクッと書いてみました。湯婆婆に田中さんの名前を中にしてもらう仕事だけになるべく注力してもらえるように以下のようなベンチマークを書いてみました。
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Text;
namespace Yubaba
{
class Program
{
static void Main(string[] args) => BenchmarkRunner.Run<Yubaba>();
}
public class Yubaba
{
public static string Name = "田中";
[Benchmark]
public string TraditionalWorkstyle()
{
var sb = new StringBuilder();
sb.AppendLine("契約書だよ。そこに名前を書きな。");
sb.AppendLine($"フン。{Name}というのかい。贅沢な名だねぇ。");
var newName = Name[1];
sb.AppendLine($"今からお前の名前は{newName}だ。いいかい、{newName}だよ。分かったら返事をするんだ、{newName}!!");
return sb.ToString();
}
[Benchmark]
public string NewWorkstyle()
{
var sb = new StringBuilder();
sb.AppendLine("契約書だよ。そこに名前を書きな。");
sb.Append("フン。");
sb.Append(Name);
sb.AppendLine("というのかい。贅沢な名だねぇ。");
var newName = Name[1];
sb.Append("今からお前の名前は");
sb.Append(newName);
sb.Append("だ。いいかい、");
sb.Append(newName);
sb.Append("だよ。分かったら返事をするんだ、");
sb.Append(newName);
sb.AppendLine("!!");
return sb.ToString();
}
}
}
TraditionalWorkstyle
が元々の処理準拠で NewWorkstyle
が文字列補間を排除したバージョンです。では測定してみましょう。
測定結果
.NET Framework 4.7.2 と .NET Core 3.1 と .NET 5 RC2 で実行してみました。
.NET Framework 4.7.2
| Method | Mean | Error | StdDev |
|--------------------- |---------:|--------:|--------:|
| TraditionalWorkstyle | 405.1 ns | 1.23 ns | 1.09 ns |
| NewWorkstyle | 172.4 ns | 0.76 ns | 0.67 ns |
.NET Core 3.1
| Method | Mean | Error | StdDev |
|--------------------- |---------:|--------:|--------:|
| TraditionalWorkstyle | 429.3 ns | 6.69 ns | 6.26 ns |
| NewWorkstyle | 178.1 ns | 2.26 ns | 2.00 ns |
.NET 5 RC2
| Method | Mean | Error | StdDev |
|--------------------- |---------:|--------:|--------:|
| TraditionalWorkstyle | 346.6 ns | 5.08 ns | 4.75 ns |
| NewWorkstyle | 175.0 ns | 2.67 ns | 2.50 ns |
.NET 5 だと文字列補間がちょっとだけ早いようにも見えますが、どちらも純粋に Append しただけのものと比べて倍くらいの差があります。
char の boxing を避ける
newName が char 型なので boxing させたくなければ ToString すればいいじゃない?という話しもあったりします。AnotherWorkstyle がこの改善を入れたバージョンです。
[Benchmark]
public string AnotherWorkstyle()
{
var sb = new StringBuilder();
sb.AppendLine("契約書だよ。そこに名前を書きな。");
sb.AppendLine($"フン。{Name}というのかい。贅沢な名だねぇ。");
var newName = Name[1];
sb.AppendLine($"今からお前の名前は{newName.ToString()}だ。いいかい、{newName.ToString()}だよ。分かったら返事をするんだ、{newName.ToString()}!!");
return sb.ToString();
}
実行してみましょう。上から順に .NET Framework, .NET Core 3.1, .NET 5 RC2 です。
.NET Framework
| Method | Mean | Error | StdDev |
|--------------------- |---------:|--------:|--------:|
| TraditionalWorkstyle | 405.9 ns | 1.48 ns | 1.31 ns |
| AnotherWorkstyle | 286.3 ns | 3.86 ns | 3.42 ns |
| NewWorkstyle | 167.2 ns | 0.91 ns | 0.76 ns |
.NET Core 3.1
| Method | Mean | Error | StdDev |
|--------------------- |---------:|--------:|--------:|
| TraditionalWorkstyle | 418.4 ns | 6.35 ns | 5.94 ns |
| AnotherWorkstyle | 263.8 ns | 4.50 ns | 3.99 ns |
| NewWorkstyle | 177.9 ns | 2.47 ns | 2.31 ns |
.NET 5 RC2
| Method | Mean | Error | StdDev |
|--------------------- |---------:|--------:|--------:|
| TraditionalWorkstyle | 343.2 ns | 0.93 ns | 0.87 ns |
| AnotherWorkstyle | 251.7 ns | 3.25 ns | 3.04 ns |
| NewWorkstyle | 182.6 ns | 1.28 ns | 1.13 ns |
早くなったけどべったりと Append するのに比べると遅いですね。(ToString を 3 回もしてるのを 1 回にしたら早くなると思うけど気力が尽きた)
ZString
C# でパフォーマンスといったら名前があがる Cysharp の neuecc さん謹製の ZString というライブラリを xin9le さんに教えてもらいました。
そしてここにさらに ZString のベンチマークを追加すると...??
— じんぐる (@xin9le) November 9, 2020
neuecc さんは他にも UniRx, UniTask, MessagePack for C# などなどすごい数のライブラリ書いてるので凄い。ということで ZString も試してみたいと思います。ということでベンチマークにこんなメソッドを追加して…
[Benchmark]
public string ZStringWorkstyle()
{
using var sb = ZString.CreateStringBuilder();
sb.AppendLine("契約書だよ。そこに名前を書きな。");
sb.Append("フン。");
sb.Append(Name);
sb.AppendLine("というのかい。贅沢な名だねぇ。");
var newName = Name[1];
sb.Append("今からお前の名前は");
sb.Append(newName);
sb.Append("だ。いいかい、");
sb.Append(newName);
sb.Append("だよ。分かったら返事をするんだ、");
sb.Append(newName);
sb.AppendLine("!!");
return sb.ToString();
}
今回は .NET 5 RC2 のみで計測してみました。
| Method | Mean | Error | StdDev |
|--------------------- |----------:|---------:|---------:|
| TraditionalWorkstyle | 344.09 ns | 0.985 ns | 0.873 ns |
| AnotherWorkstyle | 239.92 ns | 4.611 ns | 4.313 ns |
| NewWorkstyle | 181.68 ns | 1.140 ns | 1.067 ns |
| ZStringWorkstyle | 82.41 ns | 1.468 ns | 1.301 ns |
普通に文字列補間をするよりも 4 倍くらい早い…
別記事
@fujieda さんが記事書いてくれました。+ での連結は流石…早い…。
まとめ
ということで文字列補間を使わずに愚直に Append するだけにすると単純に考えると湯婆婆は今の 2 倍の人の雇用手続きの処理が出来るようになりますね。ZString を使うと 4 倍ですね…凄い。
将来的には文字列補間つかっても Append でべったり書くのとそん色ない感じになるといいですね。
ここら辺かな?
- Add String.Format overloads to avoid unnecessary allocations
- Proposal: String interpolation via AppendFormat pattern #28945
- API Proposal: Add Variant type to avoid boxing .NET intrinsic types #28882
因みに自分は文字列補間は普通に使っていきます。Append で連結は書くのも読むのも辛い…。どうしてもコアな部分で性能が超重要とかではない限りは。