ドキュメント

Unity Engine

Unity Engine

文字列とテキスト

Reduce the memory allocations that string and text manipulation cause in your project.
読み終わるまでの所要時間 17 分最終更新 1ヶ月前

Unity プロジェクトにおけるパフォーマンス問題の一般的な原因のひとつに、文字列とテキストの扱いがあります。C# では、すべての文字列が不変です。文字列を操作した場合には必ず新しい完全な文字列の割り当てが発生します。これは比較的負荷が高く、大規模な文字列やデータセット、またはタイトなループで文字列結合を頻繁に行うと、パフォーマンスの問題の原因となる可能性があります。
さらに、N 文字列の連結には N-1 中間文字列の割り当てが必要なため、連結が繰り返されるとマネージメモリに高負荷が掛かる大きな原因ともなり得ます。
タイトなループ内またはフレームごとに文字列を連結しなければならない場合は、StringBuilder を使用して実際の連結処理を行うようにしてください。StringBuilder インスタンスを再利用することで、不要なメモリ割り当てを最小化することもできます。
マイクロソフトから、C# で文字列を扱う場合のベストプラクティスのリストが提供されています。MSDN ウェブサイト msdn.microsoft.com で確認できます。

ロケールの強制と序数比較

文字列関連のコードでよく見られる主要なパフォーマンス問題の 1 つは、速度の遅いデフォルト文字列 API を意図せず使用してしまうことです。こういった API はビジネスアプリケーション向けに開発されており、テキスト中の文字に関する多様な文化的、言語的ルールに基づいて文字列を扱えるよう意図されています。
例えば、以下の参考コードは US-English ロケールで実行された場合には true を返しますが、多くの欧州ロケールでは false を返します。
ノート: Unity 5.3 と 5.4 の時点では、 Unity のスクリプティングランタイムは常に US English (en-US) ロケールで実行されます。
String.Equals("encyclopedia", “encyclopædia”);
ほとんどの Unity プロジェクトでは、これは不要です。序数に基づく比較を使用したほうが約 10 倍速くなります。序数に基づく比較は C や C++ のプログラマーには馴染み深い方法で、各バイトが表す文字に関わらず、単純に文字列の一連のバイトを比較します。
序数に基づく文字列比較への切り替えは、
StringComparison.Ordinal
を最終引数として
String.Equals
に提供するだけで簡単に行えます。
myString.Equals(otherString, StringComparison.Ordinal);

非効率的なビルトイン String API

序数による比較に切り替えるだけでなく、一部の C#
String
API は著しく非効率的であることが知られています。これらの中には
String.Format
、
String.StartsWith
、
String.EndsWith
などがあります。
String.Format
は置き換えが難しいですが、非効率的な文字列比較方法の最適化は簡単に行えます。
マイクロソフトは、ローカライズ用の調整を必要としない文字列比較に
StringComparison.Ordinal
を渡すことを推奨しています。 Unity のベンチマークでは、カスタム実装と比べてこの影響は比較的小さいことを示しています。

方法

100k の短い文字列に掛かる時間(ms/ミリ秒)

String.StartsWith
、デフォルトカルチャ
137
String.EndsWith
、デフォルトカルチャ
542
String.StartsWith
、序数
115
String.EndsWith
、序数
34
カスタムの
StartsWith
で置き換え
4.5
カスタムの
EndsWith
で置き換え
4.5
String.StartsWith
と
String.EndsWith
は両方とも、以下のような単純な手書きコード版に置き換えることが可能です。
public static bool CustomEndsWith(this string a, string b) { int ap = a.Length - 1; int bp = b.Length - 1; while (ap >= 0 && bp >= 0 && a [ap] == b [bp]) { ap--; bp--; } return (bp < 0); } public static bool CustomStartsWith(this string a, string b) { int aLen = a.Length; int bLen = b.Length; int ap = 0; int bp = 0; while (ap < aLen && bp < bLen && a [ap] == b [bp]) { ap++; bp++; } return (bp == bLen); }

正規表現

正規表現は、文字列の検出と操作を行う強力な方法ですが、パフォーマンスに非常に高い負荷がかかる場合があります。さらに、C# ライブラリの正規表現の実装により、単純な
IsMatch
ブーリアンクエリであっても、大規模な一時データ構造を "内部的" に割り当てます。この一時的なマネージメモリの大きな変化は、初期化中を除いて許容できないものとみなされます。
正規表現が必要な場合は、正規表現を文字列パラメーターとして受け入れる静的な
Regex.Match
または
Regex.Replace
メソッドを使用しないことを強く推奨します。これらのメソッドは正規表現をオンザフライでコンパイルし、生成されたオブジェクトをキャッシュしません。
これは、ごく当たり障りのない 1 行コードです。
Regex.Match(myString, "foo");
しかし、これは実行される度に 5 KB のガベージを生成します。このガベージは簡単なリファクタリングによって無くすことができます。
var myRegExp = new Regex("foo");myRegExp.Match(myString);
この例では、
myRegExp.Match
への 1 回の呼び出しが生成するガベージは 320 バイト "のみ" です。単純なマッチング処理の場合、これでも高負荷ではありますが、先の例に比べればかなり改善されています。
したがって、正規表現が不変の文字列リテラルであれば、 Regex オブジェクトのコンストラクターの最初のパラメーターとして渡してプリコンパイルするほうが格段に効率的です。プリコンパイルした Regex はその後再使用します。

XML、 JSON、およびその他の長文式テキスト構文解析

テキストの構文解析は、多くの場合、ロード時に発生する最も負荷の高い処理のひとつです。場合によっては、テキストの構文解析に掛かる時間がアセットのロードとインスタンス化に掛かる時間を超えることもあります。
この理由は使用するパーサーによって異なります。C# のビルトイン XML パーサーは非常に柔軟に使用できますが、特定のデータレイアウト向けに最適化できません。
サードパーティのパーサーの多くはリフレクションを基盤に構築されています。リフレクションは、(パーサーがデータレイアウトの変化に素早く適応できるようになるため) 開発中には非常に優れた選択肢ですが、速度が遅いことで知られています。
Unity が導入したビルトインの JSONUtility API によって部分的にこれを解決することができます。これは、JSON の入出力を行う Unity のシリアル化システムにインターフェースを提供するものです。ほとんどのベンチマークでは、純粋な C# JSON パーサーよりも高速ですが、Unity のシリアル化システムに対する他のインターフェースと同様の制限があります。つまり、追加コードなしでは、Dictionary などの複雑なデータ型をシリアル化できません。
ノート: シリアル化処理中に複雑なデータ型の変換を行うために必要な処理を追加する簡単な方法は、ISerializationCallbackReceiver インターフェースに関するページを参照してください。
テキストデータの構文解析に起因するパフォーマンス問題が発生した場合は、 3 つの解決方法をご検討ください。

オプション 1: ビルド時に構文解析する

テキスト構文解析の負荷を回避する最善の方法は、ランタイムでのテキスト構文解析を完全に無くすことです。これは基本的に、テキストデータを何らかのビルドステップでバイナリ形式に "ベイクする" ことを意味します。
この手法を採る開発者のほとんどは、何らかの ScriptableObject から派生するクラス階層にデータを移動し、その後データをアセットバンドル経由で分配します。Youtube の Unite 2016 における Richard Fine の 講演 で、 ScriptableObject の使用に関して非常に有益な解説が提供されています。
この方法は最高のパフォーマンスを提供しますが、動的に生成する必要のないデータにしか適しません。これが最も適しているのはゲームデザインパラメーターなどのコンテンツです。

オプション 2: 分割して遅延ロードを行う

2 つ目は、構文解析する必要のあるデータを小さく分割する方法です。分割したデータの構文解析に掛かる負荷はいくつかのフレームに分散されます。理想的なのは、データの中で、理想のユーザー体験を提供するために必要な部分を特定し、その部分のみをロードすることです。
簡単な例を以下に示します。プロジェクトがプラットフォームゲームの場合、すべてのレベルのデータを 1 つの巨大な塊にまとめてシリアル化する必要はありません。データをレベルごとに個別のアセットに分割し、さらにレベルをエリアごとにセグメント化すれば、プレイヤーの進行に合わせてデータを解析できます。
これは簡単そうに聞こえますが、実際にはツールコードに相当な労力が掛かり、またデータ構造の再整理が必要になる可能性もあります。

オプション 3: スレッド

プレーンな C# オブジェクト内に完全に構文解析され、Unity API とのインタラクションを一切必要としないデータの場合、構文解析処理をワーカースレッドに移動することが可能です。
この方法は、コアを多数持つプラットフォームにおいて非常に有益です。ただし、デッドロックや競合状態が発生しないように慎重にプログラミングする必要があります。
ノート: iOS デバイスは最高で 2 つのコアしか搭載していません。Android デバイスのほとんどは 2 から 4 つのコアを搭載しています。この手法が比較的適しているのは、ビルドターゲットがスタンドアロンやコンソールの場合です。
プロジェクトでスレッドを実装する場合は、ビルトインの C# Thread および ThreadPool クラス (msdn.microsoft.com を参照) を使用して、標準の C# 同期クラスとともにワーカースレッドを管理します。