C# で湯婆婆を実装してみる「性能のいい湯婆婆を目指して」

さて、前回サロゲートペア対応した湯婆婆を作りました。

[C# で湯婆婆を実装してみる（𠮷田さんにも対応）](https://qiita.com/okazuki/items/03049da368222b562da9)

この湯婆婆ですが文字列補間を使ってます。`$"xxxx{変数}xxxx"` のように書いて文字列中に変数を埋め込むやつですね。

[C# における文字列補間](https://docs.microsoft.com/ja-jp/dotnet/csharp/tutorials/string-interpolation)

これは裏では確か `string.Format` に展開されているらしい？と聞いているのですが、これをやると裏で object の配列1つ、さらに埋め込んでいる変数がプリミティブ型だと Boxing が起きるという感じでパフォーマンスを重要視する人たちにとっては結構辛いもののようです。

## 湯婆婆に働き方改革

ということで、べたっと StringBuilder に Append しまくるのと文字列補間でどれくらい違うものなのか測ってみようと思います。.NET でベンチマークといったら [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet) がデファクトです。ということで、サクッと書いてみました。湯婆婆に田中さんの名前を中にしてもらう仕事だけになるべく注力してもらえるように以下のようなベンチマークを書いてみました。

```csharp
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 がこの改善を入れたバージョンです。

```csharp
[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](https://twitter.com/neuecc) さん謹製の [ZString](https://tech.cygames.co.jp/archives/3383/) というライブラリを [xin9le](https://twitter.com/xin9le) さんに教えてもらいました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">そしてここにさらに ZString のベンチマークを追加すると...？？</p>&mdash; じんぐる (@xin9le) <a href="https://twitter.com/xin9le/status/1325679025495236609?ref_src=twsrc%5Etfw">November 9, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

neuecc さんは他にも UniRx, UniTask, MessagePack for C# などなどすごい数のライブラリ書いてるので凄い。ということで ZString も試してみたいと思います。ということでベンチマークにこんなメソッドを追加して…

```csharp
[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 さんが記事書いてくれました。+ での連結は流石…早い…。

[C#で性能のいい湯婆婆](https://qiita.com/fujieda/items/1523c3ee656caef91aa8)

## まとめ

ということで文字列補間を使わずに愚直に Append するだけにすると単純に考えると湯婆婆は今の 2 倍の人の雇用手続きの処理が出来るようになりますね。ZString を使うと 4 倍ですね…凄い。

将来的には文字列補間つかっても Append でべったり書くのとそん色ない感じになるといいですね。

ここら辺かな？

- [Add String.Format overloads to avoid unnecessary allocations](https://github.com/dotnet/runtime/issues/14484)
- [Proposal: String interpolation via AppendFormat pattern #28945](https://github.com/dotnet/runtime/issues/28945)
- [API Proposal: Add Variant type to avoid boxing .NET intrinsic types #28882](https://github.com/dotnet/runtime/issues/28882)

因みに自分は文字列補間は普通に使っていきます。Append で連結は書くのも読むのも辛い…。どうしてもコアな部分で性能が超重要とかではない限りは。