- Unity3D高級編程:主程手記
- 陸澤西
- 907字
- 2022-01-07 14:46:21
2.8.4 字符串導致的性能問題
本質上,字符串性能問題在大部分語言中都是比較難解決的,C#中尤其如此。在C#中,string是引用類型,每次動態創建一個string,C#都會在堆內存中分配一個內存用于存放字符串。我們來看看它到底有多么“恐怖”,其源碼如下:
string strA = "test"; for(int i = 0 ; i<100 ; i++) { string strB = strA + i.ToString(); string[] strC = strB.Split('e'); strB = strB + strC[0]; string strD = string.Format("Hello {0}, this is {1} and {2}.",strB, strC[0], strC[1]); }
這是一段“恐怖”的程序,循環中每次都會將strA字符串和i整數字符串連接,strB所得到的值是從內存中新分配的字符串,然后將strB切割成兩半,使其成為strC,這兩半又重新分配兩段新的內存,再將strB與strC[0]連接起來,這又申請了一段內存,這段內存裝上strB和strC[0]連接的內容,并賦值給strB,strB原來的內容因為沒有變量指向就找不到了,最后用string.Format的形式將4個字符串串聯起來,新分配的內存中裝有4者的連接內容。
這里要注意一點,字符串常量是不會被丟棄的,比如這段程序中的"test"和"Hello {0}, this is {1} and {2}."這兩個常量,它們常駐于內存,即使下次沒有變量指向它們,它們也不會被回收,下次使用時也不需要重新分配內存。關于原因,我們放到計算機執行原理中介紹。
每次循環都向內存申請了5次內存,并且拋棄了一次strA+i.ToString()的字符串內容,這是因為沒有變量指向這個字符串。這還不是最“恐怖”的,最“恐怖”的是,每次循環結束都會將前面所有分配的內存內容拋棄,再重新分配一次,就這樣不斷地拋棄和申請,總共向內存申請了500次內存段,并全部拋棄,內存被浪費得很厲害。
為什么會這樣呢?究其原因是,C#語言對字符串并沒有任何緩存機制,每次使用都需要重新分配string內存,據我所知,很多語言都沒有字符串的緩存機制,因此字符串連接、切割、組合等操作都會向內存申請新的內存,并且拋棄沒有變量指向的字符串,等待GC單元回收。我們知道,GC單元執行一次會消耗很多CPU空間,如果不注意字符串的問題,不斷浪費內存,則將導致程序不定時卡頓,并且,隨著程序運行時間的加長,各程序模塊不良代碼的運行積累,程序卡頓次數會逐步增加,運行效率也將越來越低。
解決字符串問題有兩種方法。
第一種方法是自建緩存機制,可以用一些標志性的Key值來一一對應字符串,比如游戲項目中常用ID來構造某個字符串,偽代碼如下:
int ID = 101; ResData resData = GetDataById(ID); string strName = "This is " + resData.Name; return strName;
一個ID變量對應一個字符串,這種形式下可以建立一個字典容器將它緩存起來,下次用的時候就不需要重新申請內存了,偽代碼如下:
Dictionary<int,string>strCache; string strName = null; if(!strCache.TryGetValue(id, out strName)) { ResData resData = GetDataById(ID); string strName = "This is " + resData.Name; strCache.Add(id, strName); } return strName;
我們用Dictionary字典容器將字符串緩存起來,每次先查詢字典中的內容是否存在,若有,則直接使用,若沒有,則創建一個并將其植入字典容器中,以便下次使用。
第二種方法需要用到C#中一些“不安全”的native方法,也就是類似C++的指針方式來處理string類。
由于string類本身一定會申請新的內存,因此需要突破這個瓶頸,直接使用指針來改變string中字符串的值,這樣就能重復利用string,而不需要重新分配內存。
C#雖然委托了大部分內存內容,但它也允許我們使用非委托的方式來訪問和改變內存內容,這對C#來說是不安全的(C#中有unsafe關鍵字)。下面通過非委托的方式來改變string中的內容,使它能夠被我們再利用,代碼如下:
string strA = "aaa"; string strB = "bbb" + "b"; fixed(char* strA_ptr = strA) { fixed(char* strB_ptr = strB) { memcopy((byte*)strB_ptr, (byte*)strA_ptr, 3*sizeof(char)); } } print(strB); // 此時strB的內容為“aaab”
注意,這里用“bbb”+“b”的方式生成新字符串,是因為我們不打算改變常量字符串內存塊,所以新分配了內存來做實驗。
我們把strB的前3個字符的內容變成了strA中的內容,但并沒有增加其他內存,因為我們使用了不安全的非托管方法來控制內存。通過這樣的方式再利用已經申請的字符串內存,可將已有的字符串緩存起來再利用。我們看看再利用的例子,其源碼如下:
Dictionary<int,string>cacheStr; public unsafe string Concat(string strA, string strB) { int a_length = a.Length; int b_length = b.Length; int sum_length = a_Length + b_Length; string strResult = null; if(!cacheStr.TryGetValue(sum_length, out strResult)) { // 如果不存在sum_length長度的緩存字符串,那么直接連接后存入緩存 strResult = strA + strB; cacheStr.Add(sum_length, strResult); return strResult; } // 將緩存字符串再利用,用指針方式直接改變它的內容 fixed(char* strA_ptr = strA) { fixed(char* strB_ptr = strB) { fixed(char* strResult_ptr = strResult) { // 將strA中的內容復制到strResult中 memcopy((byte*)strResult_ptr, (byte*)strA_ptr, a_length*sizeof(char)); // 將strB中的內容復制到strResult的a_Length長度后的內存中 memcopy((byte*)strResult_ptr+a_Length, (byte*)strB_ptr, b_length*sizeof(char)); } } } return strResult; }
當需要將多個字符串連接起來時,先看看緩存中是否有可用長度的字符串,如果沒有,就直接連接并緩存,如果有,則取出來,使用指針的方式改變緩存字符串的值。其中memcopy并不是系統函數,因此需要自己編寫,寫法很簡單,拿到兩個指針根據長度遍歷并賦值即可。源碼如下:
public unsafe void memcopy(byte* dest, byte* src, int len) { while((--len)>=0) { dest[len] = src[len]; } }
- Dynamics 365 for Finance and Operations Development Cookbook(Fourth Edition)
- Offer來了:Java面試核心知識點精講(原理篇)
- SQL Server 2016數據庫應用與開發習題解答與上機指導
- Mastering Apache Spark 2.x(Second Edition)
- Haxe Game Development Essentials
- C語言程序設計上機指導與習題解答(第2版)
- Julia for Data Science
- Building Serverless Architectures
- IDA Pro權威指南(第2版)
- Python期貨量化交易實戰
- Java RESTful Web Service實戰
- Scratch少兒編程高手的7個好習慣
- Python Natural Language Processing
- 移動智能系統測試原理與實踐
- Learning Unity Physics