Unity 游戏的String interning优化

简介:

◆◆
问题描述

在此之前,先说一下这个问题为什么很容易被忽视吧。
正常情况下,我们通常难以注意到运行着的Unity引擎内String的实例化情况。比如这些字符串创建、销毁的时机是否合理,是否存在有重复 (相同内容的字符串)、冗余 (存有已不再有意义的垃圾字符)、低效 (capacity远大于length),以及泄漏 (没有在期望的时机及时销毁) 的情况。由于String无法随时像普通的Unity对象那样通过调用 Object.GetInstanceID() 来查看实例ID,我们不太容易感知字符串对象的实际内存开销。若非偶然在工具里发现了大量的此类情况,笔者也没想到看起来颇单纯的immutable string里居然隐藏着这么多秘密。

一次只说一件事,这次我们只讨论重复字符串的问题。


◆◆
优化步骤

使用自制工具ResourceTracker,可以发现Unity项目运行时 mono(il2cpp) 内有大量重复的字符串,如下所示:

请输入图片描述
1. 手动 Intern()
对 .Net 特性有了解的同学,应该知道C#同Java一样,提供了一套内建的String interning机制,能够在后台维护一个字符串池,从而保证让同样内容的字符串始终复用同一个对象。这么做有两个好处,一个是节省了内存 (重复字符串越多,内存节省量越大),另一个好处是降低了字符串比较的开销 (如果两个字符串引用一致,就不用逐字符比较内容了)。

但是为什么上面的Unity引擎内仍然有大量的重复字符串呢?
查看他们的地址,发现彼此各不相同,说明的确没有引用到同一块内存区域。由于C# 语言实现以静态的特性为主,笔者推测,也许只有编译期可以捕捉到的字符串 (也就是通常用字面字符串literal string来构建时) 才会interning。

做个实验吧

string foobar = "foobar";
string foobar2 = new StringBuilder().Append("foo").Append("bar").ToString();

Debug.Log(foobar == foobar2); 
Debug.Log(System.Object.ReferenceEquals(foobar, foobar2)); 

运行上面的代码,输出结果分别是 True 和 False。也就是说,即使运行时内容一样 (== 返回True),手动在运行时拼出来的字符串也不会自动复用已有的对象。查看游戏代码,发现很多重复字符串是通过解析binary stream或text stream构造出来的,这样就解释得通了。

手动 Intern 一下试试吧

string foobar0 = "foobar";
string foobar1 = new StringBuilder().Append("foo").Append("bar").ToString();
string foobar2 = string.Intern(foobar1);
string foobar3 = new StringBuilder().Append("f").Append("oo").Append("b").Append("ar").ToString();
string foobar4 = string.Intern(foobar3);

Debug.Log(foobar0 == foobar1);   // True
Debug.Log(foobar0 == foobar2);   // True
Debug.Log(foobar0 == foobar3);   // True
Debug.Log(foobar0 == foobar4);   // True
Debug.Log(System.Object.ReferenceEquals(foobar0, foobar1)); // False
Debug.Log(System.Object.ReferenceEquals(foobar0, foobar2)); // True
Debug.Log(System.Object.ReferenceEquals(foobar0, foobar3)); // False
Debug.Log(System.Object.ReferenceEquals(foobar0, foobar4)); // True

注意,C# 并没有提供“清除已经Intern 的字符串”的接口。也就是说,如果不由分说地把产生的字符串都扔进去,会造成大量短生命期字符串(如某个地图上特有的特效名)在全局池内的堆积。解决这个问题并不难,手写一个可清除的版本就可以了。

2. 可清除的 Interning - UniqueString
下面的UniqueString 类除了提供两个与string.Intern() 和string.IsInterned() 一致的接口外,还提供了Clear() 接口用于周期性地释放整个字符串池,可在地图切换等时机调用。这个类通过判断参数来确认,是将字符串放入全局的系统池,还是支持周期性清理的用户池。

public class UniqueString
{
    // 'removable = false' means the string would be added to the global string pool
    //   which would stay in memory in the rest of the whole execution period.
    public static string Intern(string str, bool removable = true)  

    // Why return a ref rather than a bool? 
    //   return-val is the ref to the unique interned one, which should be tested against `null`
    public static string IsInterned(string str)     

    // should be called on a regular basis
    public static void Clear();
}

通过参数removable我们可以指定使用默认intern还是removable-intern。显式地指定后者的字符串将可被随后的 UniqueString.Clear() 清理。

UniqueString 的实现及更新可以参考:https://link.zhihu.com/?target=https%3A//github.com/PerfAssist/PA_Common/blob/master/UniqueString.cs


◆◆◆
效果和小结

使用上面的机制在关键点加了几行代码简单地优化后,内存中的字符串从88000条降低到 34000 条左右 (仍有很多重复存在)。
请输入图片描述

通过上述的试验测试,笔者得到的结论如下:

  1. 直接写在代码里的常量字符串 (即所谓的literal string) 会在启动时被系统自动Intern到系统字符串池;而通过拼接、解析、转换等方式在运行时动态产生的字符串则不会。

  2. 避免在C# 代码里写多行的巨型literal string,避免无谓的内存浪费。常见的情况是很大的Lua 代码块,很密集的生成路径,大块 xml/json 等等,大家可以参考下面的例子。

  3. 已经被自动或手动 Intern 的字符串在之后的整个生命期中常驻内存无法移除,但可以使用上面提供的 UniqueString 类实现周期性的清理。

下面是一些不合理的常见代码内的常量字符串的情况 (都是常驻内存无法释放的)

string query = @"SELECT foo, bar
    FROM table
    WHERE id = 42";

string lua_code_block = @"
    local ns = foo.bar(self.nID)
        for i,v in ipairs(self.imgs) do
        if (i - 1) < ns then
            Obj.SetActive(self.imgs[i], true)
        else
            Obj.SetActive(self.imgs[i], false)
        end
    end
";

string[] resFiles = new string[] { 
    "Assets/Scenes/scene_01.unity", 
    "Assets/Scenes/scene_02.unity", 
    "Assets/Scenes/scene_03.unity", 
    "Assets/Scenes/scene_04.unity", 
    "Assets/Scenes/scene_05.unity"
};





原文出处:侑虎科技
本文作者:admin
转载请与作者联系,同时请务必标明文章原始出处和原文链接及本声明。

目录
相关文章
|
2月前
|
大数据 API 图形学
Unity优化——批处理的优势
Unity优化——批处理的优势
|
2月前
|
存储 人工智能 Java
Unity优化——脚本优化策略4
Unity优化——脚本优化策略4
|
3月前
|
安全 Java 图形学
Unity3D 导出的apk进行混淆加固、保护与优化原理(防止反编译)
Unity3D 导出的apk进行混淆加固、保护与优化原理(防止反编译)
30 0
|
4月前
|
存储 自然语言处理 监控
【Unity 实用工具篇】| 游戏多语言解决方案,官方插件Localization 实现本地化及多种语言切换
Unity的多语言本地化是一个很实用的功能,它可以帮助游戏支持多种语言,让不同语言的玩家都能够更好地体验游戏。 而实现本地化的方案也有很多种,各个方案之间也各有优劣,后面也会对多个方案进行介绍学习。 本文就来介绍一个专门作用于多语言本地化的Unity官方插件:Localization 。 这个插件方便进行游戏的多语言本地化,让游戏支持多种语言,下面就来看看该插件的使用方法吧!
|
2月前
|
人工智能 安全 API
Unity优化——加速物理引擎1
Unity优化——加速物理引擎1
|
2月前
|
存储 人工智能 缓存
Unity优化——脚本优化策略3
Unity优化——脚本优化策略3
|
2月前
|
存储 缓存 Java
Unity优化——脚本优化策略2
Unity优化——脚本优化策略2
|
2月前
|
存储 XML 缓存
Unity优化——脚本优化策略1
Unity优化——脚本优化策略1
|
3月前
|
定位技术 C# 图形学
Unity和C#游戏编程入门:创建迷宫小球游戏示例
Unity和C#游戏编程入门:创建迷宫小球游戏示例
72 2
|
4月前
|
运维 API C#
【Unity游戏破解】外挂原理分析
【Unity游戏破解】外挂原理分析
156 0
【Unity游戏破解】外挂原理分析