はじめに
今回はZString
という文字列生成におけるGC.Alloc
をゼロにするライブラリについて紹介したいと思います。
今回、文字列生成におけるメモリアロケーションをゼロにする「ZString」というライブラリを公開しました。
ZString – Unity/.NET CoreにおけるゼロアロケーションのC#文字列生成 | Cygames Engineers' Blog
触った感じ特に大きなデメリットもなさそうですし、積極的に導入して良いライブラリかもしれません。
環境
Unity 2022.2.1f1
ZString 2.5.0
対応バージョン
2021.3 or later
Supporting minimum Unity version is 2021.3.
GitHub - Cysharp/ZString: Zero Allocation StringBuilder for .NET Core and Unity.
導入
unitypackageの利用
以下のRelease
ページから最新のものをダウンロードしてUnityへインポートしてください。
github.com
ZString
はSystem.Runtime.CompilerServices.Unsafe.dll
への依存があります。
ただ.unitypackage
の中に同梱されている(Plugins
の中)ので、気にする必要はありません。
packagemanager(manifest.json)
PakcageManager
を利用して以下のコマンドを打ち込みます。
https://github.com/Cysharp/ZString.git?path=src/ZString.Unity/Assets/Scripts/ZString
しかしこの場合はSystem.Runtime.CompilerServices.Unsafe.dll
を自分で導入する必要があります。
Nuget for Unity
やNuGet importer for Unity
などNuGetのパッケージをUnityへ導入するツールを有志の方が開発されていますが、今回はnuget.org
から取ってきてUnityに入れます。
Download package
ボタンを押し、nupkg
ファイルをダウンロード。
拡張子を.zip
に変換して展開後、lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll
をUnityのPlugins
フォルダの中に入れます。
基礎的な使い方
まずは簡単な使い方について、readme
を参考にしながら実験結果付きで載せておきます。
GitHub - Cysharp/ZString: Zero Allocation StringBuilder for .NET Core and Unity.
[SerializeField] private TextMeshProUGUI tmp; private void Start() { int x = 0; int y = 1; int z = 2; // GC.Alloc : 60B + 32B(String.Format) = 92B _ = $"{x}+{y}+{z}"; // x + y + zと同じ意味 // GC.Alloc : 28B(ZString.Concat) // NOTE: String.FastAllocateStringによるGC.Allocみたい _ = ZString.Concat(x, y, z); // GC.Alloc : 66B(String.Format) + 60B = 126B _ = String.Format("x:{0}, y:{1:000}, z:{2:P}", x, y, z); // GC.Alloc : 66B(ZString.Format) // ZString.Concat同様にString.FastAllocateStringによるGC.Alloc _ = ZString.Format("x:{0}, y:{1:000}, z:{2:P}", x, y, z); // GC.Alloc : 116B + 104B(String.Join) = 220B _ = String.Join(',', x, y, z); // GC.Alloc : 44B + 32B(ZString.Join) = 76B _ = ZString.Join(',', x, y, z); // TMPro名前空間 // GC.Alloc : 60B + 56B(String.Format) = 116B ?? tmp.text = String.Format("Position: {0}, {1}, {2}", x, y, z); // Cysharp.Text名前空間 // GCAlloc : 122B ?? tmp.SetTextFormat("Position: {0}, {1}, {2}", x, y, z); // System.Text.StringBuilder.StringBuilder // GC.Alloc : 492B { var sb = new StringBuilder(); sb.Append("foo"); sb.AppendLine("42"); sb.AppendFormat("{0} {1:.###}", "bar", 123.456789); var str = sb.ToString(); // GC.Alloc : 122B tmp.SetText(sb); } // Create StringBuilder // GC.Alloc : 178B using (var sb = ZString.CreateStringBuilder()) { sb.Append("foo"); sb.AppendLine(42); sb.AppendFormat("{0} {1:.###}", "bar", 123.456789); // and build final string var str = sb.ToString(); // for Unity, direct write to TextMeshPro // GC.Alloc : 122B tmp.SetText(sb); } // prepare format, return value should store to field(like RegexOptions.Compile) // 272B(ZString.PrepareUtf16) + 44B = 316B var prepared = ZString.PrepareUtf16<int, int>("x:{0}, y:{1:000}"); _ = prepared.Format(10, 20); // C# 8.0, Using declarations // create Utf8 StringBuilder that build Utf8 directly to avoid encoding using var sb2 = ZString.CreateUtf8StringBuilder(); sb2.AppendFormat("foo:{0} bar:{1}", x, y); // directly write to steam or dest to avoid allocation //await sb2.WriteToAsync(stream); //sb2.CopyTo(bufferWritter); //sb2.TryCopyTo(dest, out var written); }
以下のメソッドを利用しましたが、全体的にGC.Alloc
を抑えることができています。
ZString.Concat
ZString.Format
ZString.Join
Cysharp.Text.TextMeshProExtensions.SetTextFormat
ZString.CreateStringBuilder
ZString.PrepareUtf16
ZString.CreateUtf8StringBuilder
ただCysharp.Text.TextMeshProExtensions.SetTextFormat
とString.Format
を使ったTMP_Text.text
への代入だと、逆になってしまっていました。私の使い方が間違っているのだと思いますが、要調査ですね。
用意されているAPI
readme
に一覧でまとめてあります。
github.com
まだZString
歴が短すぎる私が、独断と偏見でいくつかセレクションしてみました。
[SerializeField] private TextMeshProUGUI tmp; private void Start() { // ZString.CreateStringBuilder using (Utf16ValueStringBuilder sb = ZString.CreateStringBuilder()) { // Utf16ValueStringBuilder.Append sb.Append("a"); // Utf16ValueStringBuilder.AppendJoin sb.AppendJoin(',', 1, 2); // Utf16ValueStringBuilder.AppendFormat sb.AppendFormat("x:{0}", 1); // Utf16ValueStringBuilder.Length Debug.Log(sb.Length); // 7 // Utf16ValueStringBuilder.AsSpan ReadOnlySpan<char> span = sb.AsSpan(); // Utf16ValueStringBuilder.AsMemory // 補足 : Spanと違ってclassのフィールドにしたりasync/awaitで使える奴 ReadOnlyMemory<char> memory = sb.AsMemory(); // Utf16ValueStringBuilder.TryCopyTo // お試しでNativeArrayを使ってみたり NativeArray<char> nativeArray = new NativeArray<char>(sb.Length, Allocator.Temp, NativeArrayOptions.UninitializedMemory); sb.TryCopyTo(nativeArray, out var charsWritten); Debug.Log(charsWritten); // 7 nativeArray.Dispose(); // Utf16ValueStringBuilder.ToString Debug.Log(sb.ToString()); // a1,2x:1 } // ZString.CreateStringBuilder(bool notNested) // ネストしない場合はnotNestedをtrueにすると高速に動作する using (Utf16ValueStringBuilder sb = ZString.CreateStringBuilder(notNested: true)) { sb.Append("aaa"); sb.Append("bbb"); Debug.Log(sb.ToString()); //aaabbb } // ZString.Join Debug.Log(ZString.Join(',', 1, 2, 3)); // 1,2,3 // ZString.Concat Debug.Log(ZString.Concat(1, 2, 3)); // 123 // ZString.Format Debug.Log(ZString.Format("x:{0}", 1)); //x:1 // ZString.PrepareUtf16 // Utf16PreparedFormat.Format var prepared = ZString.PrepareUtf16<int, int>("x:{0}, y:{1:000}"); Debug.Log(prepared.Format(1, 2)); // x:1, y:002 Debug.Log(prepared.Format(3, 4)); // x:3, y:004 Debug.Log(prepared.Format(5, 6)); // x:5, y:006 // TextMeshProExtensions.SetText using (var sb = ZString.CreateStringBuilder(notNested: true)) { // notNestedがtrueのときはZString.Concat/Join/Formatを使ってはダメなことに注意 sb.Append("aaa"); sb.Append("bbb"); tmp.SetText(sb); // aaabbb } // TextMeshProExtensions.SetTextFormat tmp.SetTextFormat("Position: {0}, {1}, {2}", 1, 2, 3); // Position: 1, 2, 3 }
ネットワークやファイル入出力等においてEncoding.UTF8.GetBytes(stringBuilder.ToString())
をするくらいならこちらを。
using(var sb = ZString.CreateUtf8StringBuilder()) using(var fs = File.Open("foo.txt", FileMode.OpenOrCreate)) { sb.Append("foo"); sb.AppendLine(42); sb.AppendFormat("{0} {1:.###}", "bar", 123.456789); // write inner Utf8 buffer to stream await sb.WriteToAsync(fs); // or get inner buffer // .AsSpan(), .AsMemory(), TryCopyTo }
ZString – Unity/.NET CoreにおけるゼロアロケーションのC#文字列生成 | Cygames Engineers' Blog
StringBuilderのネストについて
ZString.CreateStringBuilder(notNested:true)
というものがあり、ネストをしない場合はより高速に実行できるとのこと。
// OK, return buffer immediately. using(var sb = ZString.CreateStringBuilder(true)) { sb.Append("foo"); return sb.ToString(); }
// NG using(var sb = ZString.CreateStringBuilder(true)) { sb.Append("foo"); using var sb2 = ZString.CreateStringBuilder(true); // NG, nested stringbuilder uses conflicted same buffer var str = ZString.Concat("x", 100); // NG, ZString.Concat/Join/Format uses threadstatic buffer }
GitHub - Cysharp/ZString: Zero Allocation StringBuilder for .NET Core and Unity.
Utf16ValueStringBuilderやUtf8ValueStringBuilderを引数で渡す
Utf16ValueStringBuilder
とUtf8ValueStringBuilder
はmutable
な構造体なので、参照渡しをしなければいけません。
void Build() { var sb = ZString.CreateStringBuilder(); try { BuildHeader(ref sb); BuildMessage(ref sb); } finally { // when use with `ref`, can not use `using`. sb.Dispose(); } } void BuildHeader(ref Utf16ValueStringBuilder builder) { //.. } void BuildMessage(ref Utf16ValueStringBuilder builder) { //.. }
GitHub - Cysharp/ZString: Zero Allocation StringBuilder for .NET Core and Unity.
文字列結合の注意点
// NG : 54B // OK : 0B using (var sb = ZString.CreateStringBuilder()) { // NG sb.Append("aaa" + 2); // OK sb.Append("aaa"); sb.Append(2); }
極力+
の利用を抑える。
仕組み
作者の河合さんが記事を書いています。
tech.cygames.co.jp