字符串和文本

原文鏈接:https://docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity5.html

? ? ? ? 在Unity的工程中處理字符串和文本是造成性能問題的常見原因。在C#中,所有的字符串是不可變的。一個字符串的任何操作造成的結(jié)果都是一個完全新的字符串的內(nèi)存分配。這是相對昂貴的,并且當(dāng)有大量字符串、大量數(shù)據(jù)集、或是在緊湊的循環(huán)中,重復(fù)的字符串拼接會發(fā)展成為性能問題。

? ? ? ? 進(jìn)一步說,N個字符串的連接需要N-1個中間的字符串,一些列的字符串拼接也是造成托管內(nèi)存壓力的主要原因。

? ? ? ? 對于那種每幀都要在緊湊的循環(huán)中進(jìn)行字符串拼接的情況來說,使用StringBuilder來執(zhí)行實際的拼接操作。StringBuilder實例也可以被重用以來最小化無必要的內(nèi)存分配。

? ? ? ? 微軟維護(hù)著一個在C#中進(jìn)行字符串工作的最佳實踐列表,可以在MSDN網(wǎng)站上找到:https://docs.microsoft.com/en-us/dotnet/standard/base-types/best-practices-strings


語言環(huán)境強(qiáng)制和順序比較

? ? ? ? 經(jīng)常在字符串相關(guān)代碼中發(fā)現(xiàn)的核心性能問題之一是使用了慢速默認(rèn)的字符串API。這些API是為商業(yè)應(yīng)用程序構(gòu)建的,并且嘗試對文本中字符發(fā)現(xiàn)的對許多不同的文化和語言的規(guī)則進(jìn)行處理。

? ? ? ? 比如,下面的示例代碼當(dāng)在US-English環(huán)境下運(yùn)行返回true,但是在其他歐洲語言環(huán)境下會返回false。

? ? ? ? 請注意:在Unity5.3和5.4,Unity的腳本運(yùn)行時總是在US English (en-US)語言環(huán)境下運(yùn)行:

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

? ? ? ? 對于絕大多數(shù)Unity項目來說,這完全是不必要的。使用順序的比較類型大概要快十倍,這種比較字符串的方式類似于C和C++編程者:只是簡單的比較字符串的連續(xù)字節(jié),二部考慮對應(yīng)的字節(jié)表示的是什么。

? ? ? ? 通過簡單的使用StringComparison.Ordinal作為String.Equals最后一個參數(shù)來切換到順序的字符串比較:

myString.Equals(otherString, StringComparison.Ordinal);


低效的內(nèi)置字符串API

? ? ? ? 出了切換到順序比較法外,某些C#的string API已知是非常低效的。其中String.Format, String.StartsWith和String.EndsWith. String.Format是非常難以替換的,但是低效的字符串比較函數(shù)被簡單的優(yōu)化掉了。

? ? ? ? 雖然微軟的建議是傳遞StringComparison.Ordinal到所有字符串比較中,不需要適應(yīng)本地化,但是Unity的標(biāo)準(zhǔn)檢查程序顯示與自定義實現(xiàn)相比較,這個影響是相對最小的。

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.StartsWith和String.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);

?? ?}


正則表達(dá)式

? ? ? ? 雖然正則表達(dá)式是一個匹配和操作字符串很強(qiáng)大的方式,但是它是極度性能密集型的。進(jìn)一步講,由于C#的庫實現(xiàn)了正則表達(dá)式,即使簡單的IsMatch布爾查詢也會在底層造成短暫的大量數(shù)據(jù)結(jié)構(gòu)分配。除了在初始化時,這種短暫的托管內(nèi)存流失應(yīng)該被認(rèn)為是不可接受的。

? ? ? ? 如果正則表達(dá)式是必須的,那么強(qiáng)烈的建議不要使用Regex.Match或Regex.Replace這兩個靜態(tài)函數(shù),它們接受正則表達(dá)式作為一個字符串參數(shù)。這些函數(shù)在運(yùn)行中編譯正則表達(dá)式并且不會緩存生成的對象。

? ? ? ? 下面是一行無傷大雅的代碼例子:

Regex.Match(myString, "foo");

? ? ? ? 然而每次它被執(zhí)行,都會生成5KB的垃圾。用一個簡單的重構(gòu)可以消除其大部分的垃圾。

var myRegExp = new Regex("foo");

myRegExp.Match(myString);

? ? ? ? 在這個例子中,每次調(diào)用myRegExp.Match“只”會產(chǎn)生320B的垃圾。雖然這對于一個簡單的比較操作仍然昂貴,但它可觀的提高了之前例子的性能。

? ? ? ? 因此,如果正則表達(dá)式是不變的字符串文字,那么可以通過將這些字符串作為正則表達(dá)式構(gòu)造函數(shù)第一個參數(shù)傳遞來可觀的提高效率。這些預(yù)先編譯的正則應(yīng)該在接下來被復(fù)用。


XML, JSON以及其他長格式的文本解析

? ? ? ? 解析文本通常是發(fā)生在加載時的一項繁重的操作。在一些情況下,解析文本所花費(fèi)的時間會超過加載和實例化資源的時間。

? ? ? ? 這后面的原因是由于特定的解析器使用。C#內(nèi)置的XML解析器非常的靈活,但是這卻造成了對特定數(shù)據(jù)布局無法優(yōu)化。

? ? ? ? 很多第三方的解析器是基于反射構(gòu)建的。雖然反射在開發(fā)中是一個非常好的選擇(因為它允許解析器迅速的適應(yīng)不斷改變的數(shù)據(jù)布局),但是它是眾所周知的慢。

? ? ? ? Unity推薦使用其內(nèi)置的JSONUtility API來作為一部分的解決方案,它提供了一個Unity的序列化系統(tǒng)讀取和輸出JSON文件的接口。在大多數(shù)標(biāo)準(zhǔn)檢查程序中,它都比純粹的C# JSON解析器快,但是就像Unity序列化系統(tǒng)中的其他接口一樣它也有其限制性。沒有額外代碼的話它不能序列化許多復(fù)雜的數(shù)據(jù)類型,比如說字典。(請注意:查看 ISerializationCallbackReceiver接口在Unity的序列化過程中來找到一個簡單的方法增加一個額外必要的執(zhí)行過程來轉(zhuǎn)化成或是轉(zhuǎn)化一個復(fù)雜的數(shù)據(jù)類型)。

? ? ? ? 當(dāng)在文本數(shù)據(jù)解析中發(fā)生性能問題時,考慮三個可選擇的解決方案。


選擇1:在構(gòu)建時解析

? ? ? ? 避免文本解析消耗最好的辦法是徹底在運(yùn)行時消除文本解析??傮w來說,這意味著將文本的數(shù)據(jù)通過一些構(gòu)建的步驟“烘焙”到二進(jìn)制格式中。

? ? ? ? 大多數(shù)選擇這種方式的開發(fā)者移動他們的數(shù)據(jù)到一些ScriptableObject衍生的類層級中,并且通過AssetBundle分配這些數(shù)據(jù)。對于使用ScriptableObject非常好的講解,請去youtube上看Richard Fine’s Unite 2016 talk(https://www.youtube.com/watch?v=VBA1QCoEAX4)。

? ? ? ? 這個策略提供了性能的最好可能性,但是只適用于數(shù)據(jù)并不會動態(tài)生成的情況。它最好適用于游戲設(shè)計參數(shù)和其他內(nèi)容。


選擇2:拆分和延遲加載

? ? ? ? 第二個可選擇的方法是拆分要解析的數(shù)據(jù)到更小的塊兒中。一旦拆分,解析數(shù)據(jù)的消耗就將分散到多個幀中。在理想狀態(tài)下,識別那些指定的需要按需求體驗呈現(xiàn)給用戶的數(shù)據(jù)部分,并值加載這些部分。

? ? ? ? 在一個簡單的例子中,如果項目是一個平臺游戲,那么就沒有必要將所有關(guān)卡的數(shù)據(jù)序列化到一個巨大的數(shù)據(jù)團(tuán)中。如果將數(shù)據(jù)按每個關(guān)卡拆分到單獨(dú)的資源中,并且也許可以將關(guān)卡拆分為區(qū)域,當(dāng)玩家接近它時數(shù)據(jù)才會被解析。

? ? ? ? 雖然這聽起來容易,但這實際上大量的投資到工具代碼中并且也許會需要重新組織數(shù)據(jù)結(jié)構(gòu)。


選擇3:線程

? ? ? ? 對于那些要完全解析成普通C#對象,并且無需與Unity的API交互的的數(shù)據(jù)來說,可以把解析操作移動到工作線程中。

? ? ? ? 這個選項在擁有大量核心的平臺上非常強(qiáng)大(請注意:iOS設(shè)備最多有兩個核心,大多數(shù)安卓設(shè)備有2-4個。這個技術(shù)最適用于構(gòu)建桌面和控制臺目標(biāo)應(yīng)用。)但是,它需要仔細(xì)的編程來避免造成死鎖和競爭條件。

? ? ? ? 選擇線程實現(xiàn)的項目通常使用C#內(nèi)置的Thread和ThreadPool類(請參閱msdn.microsoft.com)來管理其工作線程以及標(biāo)準(zhǔn)C#同步類。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容