myString.Equals(otherString, StringComparison.Ordinal);
低效的内置字符串 API
Beyond switching to ordinal comparisons, certain C# String
APIs are known to be extremely inefficient. Among these are String.Format
, String.StartsWith
and String.EndsWith
. String.Format
is difficult to replace, but the inefficient string comparison methods are trivially optimized away.
While Microsoft’s recommendation is to pass StringComparison.Ordinal
into any string comparison that doesn’t need to be adjusted for localization, Unity benchmarks show that the impact of this is relatively minimal compared to a custom implementation.
正则表达式
While Regular Expressions are a powerful way to match and manipulate strings, they can be extremely performance-intensive. Further, due to the C# library’s implementation of Regular Expressions, even simple Boolean IsMatch
queries allocate large transient datastructures “under the hood.” This transient managed memory churn should be deemed unacceptable, except during initialization.
If regular expressions are necessary, it’s strongly recommended to not use the static Regex.Match
or Regex.Replace
methods, which accept the regular expression as a string parameter. These methods compile the regular expression on-the-fly and don’t cache the generated object.
以下示例代码为无害的单行代码。
Regex.Match(myString, "foo");
但是,该代码每次执行时会产生 5 KB 的垃圾。通过简单的重构即可消除其中的大部分垃圾:
var myRegExp = new Regex("foo");
myRegExp.Match(myString);
In this example, each call to myRegExp.Match
“only” results in 320 bytes of garbage. While this is still expensive for a simple matching operation, it’s a considerable improvement over the previous example.
Therefore, if the regular expressions are invariant string literals, it’s considerably more efficient to precompile them by passing them as the first parameter of the Regex object’s constructor. These precompiled Regexes should then be reused.
XML、JSON 和其他长格式文本解析
Parsing text is often one of the heaviest operations that occurs at loading time. Sometimes, the time spent parsing text can outweigh the time spent loading and instantiating Assets.
The reasons behind this depend on the specific parser used. C#’s built-in XML parser is extremely flexible, but as a result, it’s not optimizable for specific data layouts.
Many third-party parsers are built on reflection. While reflection is an excellent choice during development (because it allows the parser to rapidly adapt to changing data layouts), it’s notoriously slow.
Unity has introduced a partial solution with its built-in JSONUtility API, which provides an interface to Unity’s serialization system that reads/emits JSON. In most benchmarks, it’s faster than pure C# JSON parsers, but it has the same limitations as other interfaces to Unity’s serialization system – it can’t serialized many complex data types, such as Dictionaries, without additional code.
Note: See the ISerializationCallbackReceiver interface for one way to add the additional processing necessary to convert to/from complex data types during Unity’s serialization process.
当遇到文本数据解析所引起的性能问题时,请考虑三种替代解决方案。
方案 1:在构建时解析
避免文本解析成本的最佳方法是完全取消运行时文本解析。通常,这意味着通过某种构建步骤将文本数据“烘焙”成二进制格式。
大多数选择使用该方法的开发者会将其数据移动到某种 ScriptableObject 衍生的类层级视图中,然后通过 AssetBundle 分配数据。有关使用 ScriptableObjects 的精彩讨论,请参阅 youtube 上 Richard Fine 的 Unite 2016 讲座。
This strategy offers the best possible performance, but is only suitable for data that doesn’t need to be generated dynamically. it’s best suited for game design parameters and other content.
方案 2:拆分和延迟加载
第二种可行的方法是将必须解析的数据拆分为较小的数据块。拆分后,解析数据的成本可分摊到多个帧。在理想的情况下,可识别出为用户提供所需体验而需要的特定数据部分,然后只加载这些部分。
举一个简单的例子:如果项目为平台游戏,则没必要将所有关卡的数据一起序列。如果将数据拆分为每个关卡的独立资源,并且将关卡划分到区域中,则可以在玩家闯关到相应位置时再解析数据。
虽然这听起来不难,但实际上需要在工具编码方面投入大量精力,并可能需要重组数据结构。
方案 3:线程
For data that’s parsed entirely into plain C# objects, and doesn’t require any interaction with Unity APIs, it’s possible to move the parsing operations to worker threads.
This option can be extremely powerful on platforms with a significant number of cores. However, it requires careful programming to avoid creating deadlocks and race conditions.
Note: iOS devices have at most 2 cores. Most Android devices have from 2 to 4. This technique of more interest when building for standalone and console build targets.
Projects that choose to implement threading use the built-in C# Thread and ThreadPool classes (see msdn.microsoft.com) to manage their worker threads, along with the standard C# synchronization classes.