06. 字符串和文本

这是摘自Unity官方文档有关优化的部分,原文链接:https://docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity.html
总共分为如下系列:

  1. 采样分析
  2. 内存部分
  3. 协程
  4. Asset审查
  5. 理解托管堆 【推荐阅读】
    5.1 上篇:原理,临时分配内存,集合和数组
    5.2 下篇:闭包,装箱,数组
  6. 字符串和文本
  7. 资源目录
  8. 通用的优化方案
  9. 一些特殊的优化方案

在Unity中处理字符串和文本很容易产生性能问题。在C#中,所有的字符串都是不可变的(immutable)。任何对字符串的操作都会分配一个新的字符串,这个开销还是比较大的。重复进行字符串拼接更容易造成性能问题,例如对很长的字符串、数据库进行操作,或者在循环中处理字符串拼接都很容易造成性能问题。

N个字符的拼接的过程中会产生N-1个中间字符串,这些字符串对托管堆也会有一定的压力。

如果需要每帧进行字符串操作,建议使用StringBuilder类来完成这些操作,StringBuilder不会反复分配新的内存,可以减少内存开销。

Microsoft提供了C#中如何进行字符串操作的最佳指导,可以参见MSDN网站

有关本地化的字符串比较问题

和字符串相关的代码中,最容易出现的问题就是使用效率很低的默认字符串相关API。这些API是为了商业目的设计,所以考虑了非常多的情况,包括在不同语言环境和文化下的字符问题。

例如,下面的代码在US-English的语言环境下返回true,但是在绝大多数的欧洲语言环境下返回false。

注意,在Unity 5.3和Unity 5.4中,Unity的脚本环境总是运行在US-English的环境下。

String.Equals("encyclopedia", “encyclopædia”);

对于大部分的Unity工程而言,这些原生API考虑的复杂情况根本用不上。如果采用类似于C和C++中比较字符串的方法,按照顺序逐个进行匹配,不需要考虑这些字符所处的语言环境是什么,差不多要快10倍左右。

如果需要这么做,只需要将StringComparison.Ordinal作为最后一个参数传递给String.Equal即可。

myString.Equals(otherString, StringComparison.Ordinal);

内置的低效API

除了需要将比较方法的参数设置成Ordinal,某些特定的C#字符串也是非常低效的,这些API包括String.Format,String.StartsWith,String.EndsWith。虽然String.Format很难被取代,但是还是可以通过一些细微的设置来提升一点性能。Microsoft推荐将StringComparison.Ordinal变量传递给任何不需要考虑本地化字符串的比较方法。

Unity的相关测评结果如下:

Method Time (ms) for 100k short strings
String.StartsWith, default culture 137
String.EndsWith, default culture 542
String.StartsWith, ordinal 115
String.EndsWith, ordinal 34
Custom StartsWith replacement 4.5
Custom EndsWith replacement 4.5

String.StartsWithString.EndsWith都可以被替换成自己实现的版本:

    public static bool CustomEndsWith(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 && a.Length >= b.Length) || 
                (ap < 0 && b.Length >= a.Length);
        }
    public static bool CustomStartsWith(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 && aLen >= bLen) || 
                (ap == aLen && bLen >= aLen);
    }

正则表达式

虽然正则表达式用来匹配字符串和操作字符串非常强大,但是如果使用不当对性能影响也会非常大。而且因为C#关于正则表达式的内部实现问题,即使是最简单的IsMatch查询操作都会分配很大的临时数据结构。这些临时分配会造成托管内存压力,尤其是在游戏启动的时候应该格外注意。

如果必须要使用正则表达式,强烈建议不要使用静态方法Regex.MatchRegex.Replace方法。虽然这些方法使用起来很简单,只需要传入代表正则表达式的字符串即可,但是这些方法只有在运行阶段才会对正则表达式进行解析,而且不会缓存生成的对象。

下面的代码看起来很简单,但是会造成性能问题

Regex.Match(myString, "foo");

这段代码每次运行的时候,都差不多要产生5KB的垃圾。
下面的版本是修改之后的版本:

var myRegExp = new Regex("foo");
myRegExp.Match(myString);

改进之后的版本,每次调用myRegExp.Match的时候,只会产生320字节的垃圾。虽然对于简单的匹配操作而言,开销还是比较大,但是和之前的版本相比还是提升了不少性能。

因此,如果正则表达式字符串不会改变,将它们作为第一个参数传递给某个Regex对象的构造器,这样提前被编译好的正则表达式就可以重复被利用。

XML,JSON和其他的长本文解析问题

解析文本总是加载过程中最耗时的操作。在某些案例中,解析文本耗费的时间甚至超过加载和初始化Asset

这个问题背后的原因是底层使用的解析器造成的。C#内置的XML解析器虽然很灵活,但是没有针对特定的布局文件进行优化。

很多第三方的解析器是基于反射设计的。虽然在开发阶段很容易进行调整,但是速度则是非常的慢。

Unity针对部分问题引入了内置的JSONUtility API,提供了针对Unity序列化系统的读取和输出JSON的方法。在大多数情况下,它比C#的解析器要快一点。但是它和其他的Unity序列化系统的API一样也有限制——如果不添加额外的代码的话不支持对复杂的数据结构类型进行序列化操作,比如说字典这种数据结构。
【可以参照ISerializationCallbackReceiver接口来实现对复杂数据类型进行序列化的操作。】

如果在Unity中解析文本出现性能问题可以参见如下的解决方案:

方案1:在打包的时候解析

避免文本解析开销最好的办法就是不在运行阶段对其进行解析,比如可以将文本化的数据在打包过程中处理成二进制文件格式。

大部分开发者会选择将他们的数据移动到ScriptableObject的继承类中,然后通过AssetBundle进行数据发布。对于ScriptableObject方法的使用,参见Youtube视频 Richard Fine’s Unite 2016 talk

方案2:拆分和延迟加载

第二个解决方案就是对数据进行拆分,然后按照分块的方式进行解析。拆分之后,消耗可以平摊到几帧当中。最理想的情况下,对数据进行划分,只需要当前玩家状态需要的数据,只对这些数据进行解析即可。

举个简单的例子,对于某个平台游戏而言,没有必要将所有的数据都加载进内存,如果每个关卡的数据都可以被拆分,所以只需要加载玩家目前所需要的关卡信息即可。

但是这个方案在实际操作过程中需要精力去开发工具,而且数据的组织架构可能也需要重新调整。

方案3:使用线程加载

如果解析的数据只会和C#进行交互,不会和Unity API进行交互,最好是将解析的过程放到子线程中。

这样对于多核机器而言非常有优势。iOS设备至少有两个核,Android设备则有2~4个核。对于单机和终端平台更有优势。当然,编程的时候需要注意防止死锁和线程竞争的情况出现。

当需要使用到线程的时候,基本上会采用C#的线程线程池相关的类。可以参见Microsoft提供的指导来管理线程和标准的C#同步类。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,656评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,122评论 25 707
  • 渐进增强与平稳退化是由于CSS3流出来的一个概念,由于低级浏览器不支持CSS3,但是CSS3本身又太优秀,不忍心放...
    wyb1995阅读 1,746评论 1 1
  • 李商隐 风雨 凄凉宝剑篇,羁泊欲穷年。 黄叶仍风雨,青楼自管弦。 新知遭薄俗,旧好隔良缘。 心断新丰酒,消愁又几千...
    64b79db7c71b阅读 520评论 0 1
  • 【翼媒体】2017年4月26日 星期三 【国内资讯】 1、微信成立搜索应用部,游戏中心升级为增值业务部 2、微信要...
    翼媒体阅读 171评论 0 1