Loading

[Unity] 引擎脚本相关的字符串优化

本文始发于:https://www.cnblogs.com/wildmelon/p/16180980.html

一、参考资料

  1. .Net源代码,https://referencesource.microsoft.com/#mscorlib/system/string.cs
  2. 字符串和文本,https://docs.unity.cn/cn/current/Manual/BestPracticeUnderstandingPerformanceInUnity5.html
  3. Concatenating Strings Efficiently,https://jonskeet.uk/csharp/stringbuilder.html

二、序数比对

根据 官方文档优化建议 中提到的:

在与字符串相关的代码中经常出现的核心性能问题之一,是无意间使用了缓慢的默认字符串 API。这些 API 是为商业应用程序构建的,可根据与文本字符有关的多种不同区域性和语言规则来处理字符串。

这里提到的区域性,即指与 CultureInfo 有关的处理,在 string.cs 中,大约有以下方法与 CultureInfo 有关:

  1. Equals
  2. Compare
  3. StartsWith
  4. EndsWith
  5. ToUpper
  6. ToLower

除了 ToUpperToLower 之外,都是与字符串对比有关的。绝大多数情况下我们不需要认定字符'e'与'æ'之类的字符是相同的。所以需要改用带 StringComparison 形参的方法,切换到 StringComparison.Ordinal,直接按序数进行比对。否则可能产生数十到上百倍的性能差距:

// 耗时对比
public void TestString1() {
    System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
    int loopCount = 1000000;
    string strA = "test1";
    string strB = "test2";
    stopwatch.Start();
    for (int i = 0; i < loopCount; i++) {
        string.Compare(strA, strB);
        //strA.Equals(strB, StringComparison.CurrentCulture);
    }
    stopwatch.Stop();
    Debug.Log("Compare method 1: " + stopwatch.Elapsed.TotalMilliseconds);

    stopwatch.Reset();
    stopwatch.Start();
    for (int i = 0; i < loopCount; i++) {
        string.CompareOrdinal(strA, strB);
        //strA.Equals(strB, StringComparison.Ordinal);
    }
    stopwatch.Stop();
    Debug.Log("Compare method 2: " + stopwatch.Elapsed.TotalMilliseconds);
}

// 某次不严谨的测试结果:
// Compare method 1: 574.2263
// Compare method 2: 7.5011

不过与 Unity文档讲的不同,Equals 接口似乎默认就是使用的 StringComparison.Ordinal 序数比对。

另外类似 a.StartsWith("b", System.StringComparison.Ordinal) 本身效率已经足够,似乎也没有必要自己重新实现一个 CustomStartsWith,有可能会忽略掉某些提前退出的情况,反而使得性能下降。

三、字符串拼接

(一)加号操作符

基本上在 StringBuilder 的相关教程上,都能看到类似如下的示例:

public string TestString2() {
    // 会产生多个临时对象,产生 GC
    string str = "";
    str = str + "a";
    str = str + "b";
    str = str + "c";
    return str;
}

使用 ILSpy 工具打开编译之后的 dll 文件,可见 C# 源码被编译为以下 IL 代码,可以看到 string 的加号操作符,实际上调用的是 string.Concat 方法:

.method public hidebysig 
    instance string TestString2 () cil managed 
{
    IL_0000: ldstr ""
    IL_0005: ldstr "a"
    IL_000a: call string [mscorlib]System.String::Concat(string, string)
    IL_000f: ldstr "b"
    IL_0014: call string [mscorlib]System.String::Concat(string, string)
    IL_0019: ldstr "c"
    IL_001e: call string [mscorlib]System.String::Concat(string, string)
    IL_0023: ret
} // end of metho

阅读 Concat 方法的源码,会发现底层是在统计所需长度后,调用 FastAllocateString 一次性分配内存,然后 FillStringChecked 对源字符串进行拷贝,实际上是一个非常效率的接口:

[System.Security.SecuritySafeCritical]  // auto-generated
private static String ConcatArray(String[] values, int totalLength) {
    String result =  FastAllocateString(totalLength);
    int currPos=0;

    for (int i=0; i<values.Length; i++) {
        Contract.Assert((currPos <= totalLength - values[i].Length), 
                        "[String.ConcatArray](currPos <= totalLength - values[i].Length)");

        FillStringChecked(result, currPos, values[i]);
        currPos+=values[i].Length;
    }

    return result;
}

(二)拼接优化

那么对于刚才的示例,是否有必要改成如下代码呢:

StringBuilder builder = new StringBuilder();
builder.Append ("a");
builder.Append ("b");
builder.Append ("c");
string result = builder.ToString();   

对于已知数量的字符串拼接,其实直接改成 string str = "a"+"b"+"c"; 即可。单行的连续加号运算会编译成 String Concat(params String[] values) 方法调用,既保持可读性又不会生成多个中间字符串。

严格来讲上述说明不太准确。string str = "a"+"b"+"c"; 生成的 IL 代码如下所示:

.method public hidebysig 
    instance string TestString2 () cil managed 
{
    .maxstack 8

    IL_0000: ldstr "abc"
    IL_0005: ret
} 

可见如果代码里用的是常量表达式,在编译时就能确认字符串内容的话,会进行优化直接跳过拼接的调用。但使用 StringBuilder 的话则是无法提前确认进行优化的。

(三)使用总结

StringBuilder 和 string.Concat 效率是相近的。

对于预先组织好的、固定数量的字符串拼接(能直接确认总长度),string.Concat 效率稍高一些,可读性也更高。

对于不确定数量的或者需要缓存中间结果的字符串拼接,则使用 StringBuilder。

顺带一提,String.Format 是效率更低的字符串接口,看似只用了一个额外字符串,实际上会逐字符遍历解析占位符。如果只是用作拼接目的,先考虑是否能用其他接口替代。

四、C# 0GC 字符串方案

StringBuilder 和 Concat,每次调用都会分配新的内存来保存新字符串,在项目中如果存在大量的字符串拼接,就会导致频繁的 GC Alloc。

可以考虑使用 https://github.com/871041532/zstring

参考以下文章说明:

  1. Unity中的string gc优化,https://www.cnblogs.com/zhaoqingqing/p/13928469.html
  2. ZString — Zero Allocation StringBuilder for .NET Core and Unity.
posted @ 2022-04-23 09:26  野生西瓜  阅读(427)  评论(0编辑  收藏  举报