惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
人人都是产品经理
人人都是产品经理
IT之家
IT之家
Cyberwarzone
Cyberwarzone
T
Troy Hunt's Blog
有赞技术团队
有赞技术团队
阮一峰的网络日志
阮一峰的网络日志
T
Threat Research - Cisco Blogs
S
SegmentFault 最新的问题
Apple Machine Learning Research
Apple Machine Learning Research
G
GRAHAM CLULEY
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
博客园 - 叶小钗
Last Week in AI
Last Week in AI
C
CERT Recently Published Vulnerability Notes
The Hacker News
The Hacker News
Jina AI
Jina AI
T
Tor Project blog
V
Vulnerabilities – Threatpost
酷 壳 – CoolShell
酷 壳 – CoolShell
Spread Privacy
Spread Privacy
博客园_首页
C
Cybersecurity and Infrastructure Security Agency CISA
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Simon Willison's Weblog
Simon Willison's Weblog
Security Latest
Security Latest
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
博客园 - 司徒正美
V2EX - 技术
V2EX - 技术
I
Intezer
The Cloudflare Blog
Cisco Talos Blog
Cisco Talos Blog
SecWiki News
SecWiki News
博客园 - 【当耐特】
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
L
Lohrmann on Cybersecurity
Scott Helme
Scott Helme
Google Online Security Blog
Google Online Security Blog
量子位
The Last Watchdog
The Last Watchdog
AI
AI
Application and Cybersecurity Blog
Application and Cybersecurity Blog
S
Security Affairs
P
Palo Alto Networks Blog
S
Secure Thoughts
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Attack and Defense Labs
Attack and Defense Labs

博客园 - 野生西瓜

[Unity 杂货铺] SpriteAtlas 和 SBP 打包 [Unity 杂货铺] 游戏字体选择 [Unity 杂货铺] 游戏项目的结构规划与初始化 [Unity 杂货铺] Git 配置与实践 [Unity 杂货铺] 引擎版本的选择 [Unity] 基础寻路算法 - 环境搭建 [Unity] 基础寻路算法 - 代码实践 [Unity] 资源工作流程 - AssetPostprocessor [Unity] 资源工作流程 - ScriptedImporter [Unity] 资源工作流程 - 辅助工具 [Lua游戏AI开发指南] 笔记零 - 框架搭建 [GAME] [Civilization] 文明6字体及字体大小修改 [Unity] 编辑器运行中动态编译执行C#代码 [Unity] 在软件标题栏显示工作路径 [GAMEDEV] 个人开发如何找到合适的图片素材? [施工中] 博客导航 2021 的书 [BACKUP] Visual Studio Code 配置 [theHunterCOTW] 猎人荒野的召唤-一点资料
[Unity] 引擎脚本相关的字符串优化
野生西瓜 · 2022-04-23 · via 博客园 - 野生西瓜

本文始发于: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.