Unity3D:字符串和文本
推荐:将NSDT场景编辑器加入你的3D工具链
3D工具集:NSDT简石数字孪生
字符串和文本
字符串和文本的处理不当是 Unity 项目中性能问题的常见原因。 在 C# 中,所有字符串均不可变。 对字符串的任何操作均会导致分配一个完整的新字符串。 这种操作的代价相对比较高,而且在大型字符串上、大型数据集上或紧凑循环中执行时,接连不断的重复的字符串可能发展成性能问题。
此外,由于 N 个字符串连接需要分配 N–1 个中间字符串,串行连接也可能成为托管内存压力的主要原因。
如果必须在紧凑循环中或每帧期间对字符串进行连接,请使用 StringBuilder 执行实际连接操作。 为最大限度减少不必要的内存分配,可重复使用 StringBuilder 实例。
Microsoft 整理了一份处理 C# 中的字符串的最佳做法清单,可在这里的 MSDN 网站上找到该清单:msdn.microsoft.com。
区域约束与序数比对
在与字符串相关的代码中经常出现的核心性能问题之一是无意间使用了缓慢的默认字符串 API。 这些 API 是为商业应用程序构建的,可根据与文本字符有关的多种不同区域性和语言规则来处理字符串。
例如,以下示例代码在美国英语区域设置下运行时返回 true,但对于许多欧洲区域设置返回 false。
注:从 Unity 5.3 和5.4开始Unity的脚本运行时始终在美国英语(en-US)语言环境下运行:
String.Equals("encyclopedia", "encyclopædia");
对于大多数 Unity 项目,这是完全没有必要的。使用序号比较类型大约快十倍,它以 C 和 C++ 程序员熟悉的方式比较字符串:只需比较字符串的每个连续字节,而不考虑该字节表示的字符。
切换至序数比对的方式非常简单,只需将作为最终参数提供给:StringComparison.OrdinalString.Equals
myString.Equals(otherString, StringComparison.Ordinal);
低效的内置字符串 API
除了切换到序号比较之外,已知某些 C# API 效率极低。其中包括 和。很难替换,但低效的字符串比较方法被简单地优化掉了。StringString.FormatString.StartsWithString.EndsWithString.Format
虽然微软的建议是进入任何不需要针对本地化进行调整的字符串比较,但 Unity 基准测试表明,与自定义实现相比,这样做的影响相对较小。StringComparison.Ordinal
方法 | 100k 短字符串的时间(毫秒) |
---|---|
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
如果需要正则表达式,强烈建议不要使用 static 或方法,它们接受正则表达式作为字符串参数。这些方法动态编译正则表达式,并且不缓存生成的对象。Regex.MatchRegex.Replace
以下示例代码为无害的单行代码。
Regex.Match(myString, "foo");
但是,该代码每次执行时会产生5KB 的垃圾。 通过简单的重构即可消除其中的大部分垃圾:
var myRegExp = new Regex("foo");
myRegExp.Match(myString);
在此示例中,每次调用“仅”会导致320 字节的垃圾。虽然这对于简单的匹配操作来说仍然很昂贵,但与前面的示例相比,这是一个相当大的改进。myRegExp.Match
因此,如果正则表达式是固定字符串文本,则通过将它们作为 Regex 对象构造函数的第一个参数传递来预编译它们会效率高得多。然后应重用这些预编译的正则表达式。
XML、JSON 和其他长格式文本解析
解析文本通常是加载时发生的最繁重的操作之一。有时,解析文本所花费的时间可能会超过加载和实例化资产所花费的时间。
这背后的原因取决于所使用的特定解析器。C# 的内置 XML 解析器非常灵活,但因此,它无法针对特定数据布局进行优化。
许多第三方解析器都是基于反射构建的。虽然反射是开发过程中的绝佳选择(因为它允许解析器快速适应不断变化的数据布局),但它的速度是出了名的慢。
Unity 引入了带有内置 JSONUtility API 的部分解决方案,该 API 为 Unity 的序列化系统提供了一个读取/发出 JSON 的接口。在大多数基准测试中,它比纯 C# JSON 解析器更快,但它与 Unity 序列化系统的其他接口具有相同的限制 - 它无法在没有额外代码的情况下序列化许多复杂的数据类型,例如字典。
注意:请参阅 ISerializationCallbackReceiver 接口,了解在 Unity 序列化过程中添加与复杂数据类型转换所需的额外处理的一种方法。
当遇到文本数据解析所引起的性能问题时,请考虑三种替代解决方案。
方案 1:在构建时解析
避免文本解析成本的最佳方法是完全取消运行时文本解析。 通常,这意味着通过某种构建步骤将文本数据“烘焙”成二进制格式。
大多数选择使用该方法的开发者会将其数据移动到某种 ScriptableObject 衍生的类层级视图中,然后通过 AssetBundle 分配数据。 有关使用 ScriptableObjects 的精彩讨论,请参阅 youtube 上 Richard Fine 的 Unite 2016 讲座。
此策略可提供最佳性能,但仅适用于不需要动态生成的数据。它最适合游戏设计参数和其他内容。
方案 2:拆分和延迟加载
第二种可行的方法是将必须解析的数据拆分为较小的数据块。 拆分后,解析数据的成本可分摊到多个帧。 在理想的情况下,可识别出为用户提供所需体验而需要的特定数据部分,然后只加载这些部分。
举一个简单的例子:如果项目为平台游戏,则没必要将所有关卡的数据一起序列。 如果将数据拆分为每个关卡的独立资源,并且将关卡划分到区域中,则可以在玩家闯关到相应位置时再解析数据。
虽然这听起来不难,但实际上需要在工具编码方面投入大量精力,并可能需要重组数据结构。
方案 3:线程
对于完全解析为纯 C# 对象且不需要与 Unity API 进行任何交互的数据,可以将分析操作移动到工作线程。
此选项在具有大量内核的平台上可能非常强大。但是,它需要仔细编程以避免创建死锁和争用条件。
注意:iOS 设备最多有 2 个内核。大多数安卓设备都有 2 到 4。在为独立和控制台构建目标构建时,此技术更令人感兴趣。
选择实现线程处理的项目使用内置的 C# 线程和 ThreadPool 类(请参阅 msdn.microsoft.com)来管理其工作线程以及标准 C# 同步类。
此文由3D建模学习工作室整理翻译,转载请注明出处!