C++風構文で文字列のアロケーションを回避

C++風の構文（`cout << "..." << endl`）で無駄なアロケーションを回避しつつ、内部バッファーの再利用まで自動で行ってくれる文字列操作用のユーティリティーです。

なんと既存のコードに `CStr.COUT` と `ENDL`（省略可）を追加するだけでメモリ使用量が約40％減ります。


`CStr` = ***C***/***C***++ ***Str***ing


```cs
var str = CStr.COUT + "Value: " + 310 + CStr.ENDL;
//        ^^^^^^^^^ 起動式              ^^^^^^^^^^ 文字列への明示的変換 ※ENDLだけど改行は追加されない

// 型付けされた変数やメンバーヘの代入なら末端の ENDL は不要
MyApp.UserFullName = CStr.COUT + User.FamilyName + ' ' + User.FirstName;
//                   ^^^^^^^^^ 既存のコードにイニシエーターを足すだけ！

// 型ごとにフォーマット指定可（ただし全ての COUT で共用。空文字が一番処理速度が速い）
CStr.FloatFormat = "#,0.000";
```


ある程度構文のカスタマイズも可能 👇

```cs
global using static CppStringSyntax;  // C# 10.0
public static class CppStringSyntax { public static CStr.Handler @new => ... }

// CStr の利用が許される環境ならこのくらいやって良い
var s = @new + "Hello, world." + @string;  // 文字列に変換
var c = @new + "Hello, world." + @char;    // ReadOnlySpan<char> に変換

// auto とか C++ っぽさある
string s = @auto + "Hello, world." + 310;
```


[wotakuro](https://github.com/wotakuro) さんの 👇 に触発されてガッと作りました！！

https://github.com/wotakuro/StringBuilderTemporary/blob/master/README.ja.md




- [`Handler` 構造体](#handler-構造体)
    - [`ReadOnlySpan<char>` の取得](#readonlyspanchar-の取得)
    - [※スレッド内で複数同時に使用した場合](#スレッド内で複数同時に使用した場合)
    - [内部バッファー解放後の挙動](#内部バッファー解放後の挙動)
- [`ChunkRecycler` 構造体](#chunkrecycler-構造体)
- [構文のカスタマイズ](#構文のカスタマイズ)
- [パフォーマンス比較](#パフォーマンス比較)
- [ソースコード](#ソースコード)
- [おわりに](#おわりに)





# `Handler` 構造体

`CStr.COUT` は `Handler` 型を返します。

この `Handler` 構造体は`＋`演算子を実装しており、数値型などの構造体に対しては `TryFormat(...)` メソッドを使用して無駄なアロケーションを回避しながら、内部バッファーを利用して文字列を構築していくことが出来ます。

（最新の [`DefaultInterpolatedStringHandler`](https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.compilerservices.defaultinterpolatedstringhandler) と同じ処理速度＆省メモリ）


そして `ENDL` または `CHUNK` を加算することで次の `COUT` 呼び出し時に内部バッファーが再利用されます。何もしないと Handler 構造体の解放と共にバッファーは GC に回収されます。



## `ReadOnlySpan<char>` の取得

今風っぽく `ReadOnlySpan<char>` への変換も可能です。

```cs
// 末端に CHUNK を追加すると Chunk（ReadOnlySpan<char>）に変換
using var chunk = CStr.COUT + "Value: " + 310 + CStr.CHUNK;  // using するとバッファーを再利用
DoSomething(chunk.Span);
```



## ※スレッド内で複数同時に使用した場合

スレッド内で複数の `Handler` 型を同時に利用可能ですが、バッファーはスレッド毎に一つのみ再利用され余ったバッファーは GC に回収されます。

```cs
var str = CStr.COUT + "String!!";   // ENDL（ToString）無し
var other = CStr.COUT + "Other!!";  // --> new char[CStr.InitialCapacity] を新たに確保

_ = str.ToString();    // バッファーを再利用に回す
_ = other.ToString();  // 複数のバッファーが回ってきた場合は大きい方を残し他は GC 待機列へ
```


## 内部バッファー解放後の挙動

構造体はバッファー解放後に操作すると `NullReferenceException` を投げます。

が、エラーを意図的に起こすのは結構難しいです。

```cs
// 👇 だと問題なし　※ ENDL で文字列になっているので "Test" + "NoError" しているだけ
var test = CStr.COUT + "Test" + CStr.ENDL + "NoError";

// エラーを起こすにはこうする
CStr.Handler error = CStr.COUT + "Test";
_ = error.ToString();
_ = error.ToString();   // 複数回 ToString しても問題ないが
_ = error + "Error!!";  // 追加を試みるとエラー
```





# `ChunkRecycler` 構造体

`ReadOnlySpan<char>` 取得時にバッファーの寿命を管理する `Disposable` な `ref` 構造体です。

この構造体を破棄すると内部バッファーはプールに戻ります。破棄しなかった場合はバッファーは GC に回収されます。

```cs
using (var chunk = CStr.COUT + "Value: " + 310 + CStr.CHUNK)
{
    var span = chunk.Span;
    // 配列はプールに戻る
}

// using しないなら、、、
var chunk = CStr.COUT + "Value: " + 310 + CStr.CHUNK;

// 明示的に配列をプールに戻す必要あり
chunk.Dispose();
```





# 構文のカスタマイズ

専用の静的クラスを作ればある程度カスタマイズできます。`@ + "Hello, world" + $` とか実現したいなー。

```cs
public static class CppStringSyntax
{
    public static CStr.Handler @new
    {
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        get => CStr.COUT;
    }
    public static readonly CStr.StringTerminator @string = CStr.ENDL;
    public static readonly CStr.ChunkTerminator @char = CStr.CHUNK;
}

// 👇

using static MyApp.CppStringSyntax;

var s = @new + "Hello, world." + @string;
var c = @new + "Hello, world." + @char;
```





# パフォーマンス比較

- 秒間６０フレーム想定
- ループ内で毎回１０文字程度の文字列を生成→プロパティーに設定→（ヘルパー等使っていれば）破棄
- `Str_Int_*`： `Prefix + IntVal`
- `Str_Int_Str_*`： `Prefix + IntVal + Suffix`
- `*_CStr`： `Result = CStr.COUT + Prefix...;`
- `*_SysStr`： `Result = Prefix...;`
- `*_SbReuse`： 概ね[ココにある通りのヘルパー](https://qiita.com/sator_imaging/items/1393aa0efa3b064d77ec#stringbuilder-%E3%81%AE%E5%86%8D%E5%88%A9%E7%94%A8%E6%96%B9%E6%B3%95)で数値型の変換に `TryFormat` を使用
- `*_Interpolate`： `Result = $"{Prefix}{IntVal}";`


処理速度の倍率だけに着目するとかなり問題があるように感じるが、あくまでナノ秒レベルの話なので気にする必要はなさそう。重要なのは .NET 8 環境で `DefaultInterpolatedStringHandler` と同等の性能を達成している点。C# 的に最適な実装として間違ってないっぽい。


毎フレーム弄る GUI のラベル数を乗ずると考えればメモリ使用量の削減には相当な効果があるだろうし、細かい不要インスタンスが大量に出ては回収される状況は GC のスパイクを招くので、メモリ消費以外の意味でも恩恵は大きい（ハズ）



<p><details lang="ja" --open><summary>・・・</summary>

省メモリを達成しつつナノ秒レベルの処理速度を（やたらと速い）`SysStr` と同等にする方法もあって、それは各クラスが自身専用の `StringBuilder` を抱えて `SbReuse` の要領で文字列を構築するっていう手法。

ただ、プロパティへの文字列割り当ての度に複数行 `AppendFormatFast` を書いて `ToString -> Length = 0` を実行しなければならない上に得られる結果が約 1/60 マイクロ秒の最適化。

ナノ秒レベルの速度は上がるが各クラスが専用のバッファーを抱えることになるので明らかにマイナスの方がデカい。

<!------- End of Details JA Tag -------></details></p>




<!-- omit in toc -->
## .NET 8

全体的に高速化されて差も縮まっている。しかし `String.Concat` はなんでこんなに速いんだ？


| Method                       | Runtime       | Mean       | Ratio | Allocated | Alloc Ratio |
|----------------------------- |-------------- |-----------:|------:|----------:|------------:|
| Str_Int__x60_CStr            | .NET 8.0      | 1,980.6 ns |  1.13 |   2.81 KB |        0.60 |
| Str_Int__x60_SysStr          | .NET 8.0      |   866.1 ns |  0.49 |   4.69 KB |        1.00 |
| Str_Int__x60_SbReuse         | .NET 8.0      | 2,091.7 ns |  1.19 |   2.81 KB |        0.60 |
| Str_Int__x60_Interpolate     | .NET 8.0      | 1,898.8 ns |  1.08 |   2.81 KB |        0.60 |
| -
| Str_Int_Str__x60_CStr        | .NET 8.0      | 2,131.9 ns |  1.21 |   3.28 KB |        0.70 |
| Str_Int_Str__x60_SysStr      | .NET 8.0      | 1,142.2 ns |  0.65 |   5.16 KB |        1.10 |
| Str_Int_Str__x60_SbReuse     | .NET 8.0      | 2,225.7 ns |  1.26 |   3.28 KB |        0.70 |
| Str_Int_Str__x60_Interpolate | .NET 8.0      | 2,180.8 ns |  1.24 |   3.28 KB |        0.70 |



<!-- omit in toc -->
## .NET Core 2.1（Unity 想定）

処理時間が 2.5 倍になってるけども、そもそも他の処理に引っ張られてフレームレートは落ちるのでメモリ消費が少ない方が良いでしょう。ナノ秒レベルだし。


| Method                       | Runtime  | Mean       | Ratio | Allocated | Alloc Ratio |
|----------------------------- |--------- |-----------:|------:|----------:|------------:|
| Str_Int__x60_CStr            | Core 2.1 | 4,236.2 ns |  2.41 |   2.81 KB |        0.60 |
| Str_Int__x60_SysStr          | Core 2.1 | 1,760.4 ns |  1.00 |   4.69 KB |        1.00 |
| Str_Int__x60_SbReuse         | Core 2.1 | 3,777.9 ns |  2.15 |   2.81 KB |        0.60 |
| Str_Int__x60_Interpolate     | Core 2.1 | 5,638.7 ns |  3.20 |   4.22 KB |        0.90 |
| -
| Str_Int_Str__x60_CStr        | Core 2.1 | 4,495.9 ns |  2.55 |   3.28 KB |        0.70 |
| Str_Int_Str__x60_SysStr      | Core 2.1 | 1,942.7 ns |  1.10 |   5.16 KB |        1.10 |
| Str_Int_Str__x60_SbReuse     | Core 2.1 | 4,296.4 ns |  2.44 |   3.28 KB |        0.70 |
| Str_Int_Str__x60_Interpolate | Core 2.1 | 7,124.6 ns |  4.05 |   4.69 KB |        1.00 |





# ソースコード

https://gist.github.com/sator-imaging/a8b54e3692722d6ca1b898b36c76819c





# おわりに

<!--
`CppString` ってクラス名にして `using std = ...` を加えて `std.cout` もアリかなーと思いましたが IDE にエイリアス名の自動挿入機能がないのでやめました。
-->

演算子は `<` `>` が良かったけどペアがある演算子は戻り値の型が揃っていなければならず、、、そして C++ と同じ `<<` `>>` 演算子も C# 9.0 では定義できず。

```cs
// ENDL を「入力」すると文字列に
var str = CStr.COUT + "Value: " + 310 < CStr.ENDL;

// CHUNK に「出力」すると ReadOnlySpan<char> に
var chunk = CStr.COUT + "Value: " + 310 > CStr.CHUNK;
```

が、結果的には全てを＋演算子にまとめて強い型付けを行ったことで、間違った構文は全てコンパイルエラーになるので逆に良かったのかも。


<!--
C# の `StringBuilder` がなぜ進化しないかというと `ISpanFormattable` が `IFormattable` を実装していて、もうメソッドのオーバーロードのあいまいさの回避が不可能だからでしょう。
-->


--

Unity に関して言えば uGUI とか TextMesh Pro の内部バッファーを `Span<char>` で取り出して直接書き込む、みたいなことが出来るようにならないと根本的な解決にはならなそうだったり、翻訳とか対応していくと結局こういうユーティリティーは登場の機会が無かったり、ですが。


以上です。お疲れ様でした。