字符串拼接在高級語言中事件很讓人頭疼的事情,無論是內存申請的額外耗時還是產生臨時內存造成的大量gc都是很難優(yōu)化的宴杀,最近在c#中采用了一種優(yōu)化方案癣朗,在此記錄一下。
造成性能災難的原因
首先我們要明白c#對字符串的處理是怎么樣的:
c#對一些通常的字符串操作旺罢,是做了緩存的優(yōu)化的旷余,string作為一個引用類型,不同的變量只要內容相同扁达,實質上都是指向同一個引用正卧,我們可以用下述方法獲取變量內存地址來一探究竟:
public string getMemory(object o) // 獲取引用類型的內存地址方法
{
GCHandle h = GCHandle.Alloc(o, GCHandleType.Pinned);
IntPtr addr = h.AddrOfPinnedObject();
return "0x" + addr.ToString("X");
}
string s1 = "111";
string s2 = "11" + "1";
string s3 = new string('1', 3);
Debug.Log("s1:" + s1);
Debug.Log("s2:" + s2);
Debug.Log("s3:" + s3);
Debug.Log("s1 memory:" + getMemory(s1));
Debug.Log("s2 memory:" + getMemory(s2));
Debug.Log("s3 memory:" + getMemory(s3));
觀察輸出可以發(fā)現,字符串s1,s2雖然是通過不同方式拼接的跪解,但是因為其內容一致炉旷,引用指向的內存地址都是相同的。
c#在常用的字符串操作中都采用了這樣的優(yōu)化方式,但是可以通過new string的方式來構建一個新的string引用窘行,可以看到上面的字符串s3雖然內容與s1,s2都相同饥追,但是地址卻是不一樣的。
在字符串拼接時罐盔,實質上并沒有對原有字符串做任何改變但绕,而是檢查是否全局已經緩存了相同字符串,如果緩存了就返回引用翘骂,沒有的話便重新創(chuàng)建一個新的string壁熄,直到下一次gc被回收,這樣也就解釋了為什么我們拼接不同結果字符串的時候會產生大量的內存垃圾碳竟。
string的本質
想要優(yōu)化字符串拼接的話,我們需要進一步探究string的本質:
c#中的string是一個引用類型的class狸臣,其本質是一個含有一些描述信息的char數組莹桅,但是我們對本質的探究到這里并沒有停止。
對業(yè)務層而言烛亦,我們需要的字符串到底是什么诈泼?按最終用途可以分成兩類:
最終變?yōu)槎M制流的數據
業(yè)務中一大部分的字符串,最終實際上都會轉變?yōu)閿祿髅呵荩热缇W絡消息和寫文件铐达。實際上我們并不需要產生string類,使用自己維護的char數組來記錄字符數據檬果,然后直接輸出為二進制流瓮孙,之后再對char數組進行回收重用,可以徹底解決string類產生的gc和內存壓力选脊。
需要注意的是杭抠,如果需要對字符流編碼,可以使用encoder的原生方法:
Encoder mEncoder = Encoding.UTF8.GetEncoder();
private unsafe int GetBytes(char[] chars,byte[] bytes,int offset,int length,int outOffset)
{
fixed (char* c = chars)
{
//最后一個參數代表是否刷新encode緩存內容
int count = mEncoder.GetByteCount(c + offset, length, false);
//注意byte數組要開辟足夠的空間
fixed (byte* b = bytes)
{
mEncoder.GetBytes(c + offset, length, b + outOffset, count, false);
return count;
}
}
}
最終需要傳遞給系統(tǒng)API的string
對于業(yè)務中需要傳遞給不可控接口的string恳啥,我們需要對接口需要的string進行判斷偏灿,然后通過修改string內容的方式,對string這一類進行重用钝的。要知道雖然c#沒有提供string內容修改的接口翁垂,但是string本質上也還是char數組,對其內容修改相當容易:
private unsafe void SetChar(string s, char c, int index)
{
if (index >= s.Length)
throw new ArgumentOutOfRangeException("index");
fixed (char *p = s) {
// Set the character.
p[index] = c;
}
}
緩存的策略類似普通的緩存池硝桩,需要新字符串時沿猜,從string池中取出一個長度足夠的(不夠就新建),從頭填充正確的數據亿柑,最后對多余的尾部按需修改填充邢疙,用完放回池子。
渲染類
對于渲染類的string一般都好處理,重用string時尾部填充‘\0'就可以疟游,例如unity中的Text組件就可以采用這樣的策略呼畸。(unity在讀到\0后就不讀了,所以實際上只要把不需要的第一個位置填上\0就好)
json類
對于json這樣的字符串標記語言來說就要麻煩一些了颁虐,如果項目中的json解析類是可以修改的蛮原,還可以用\0來表示終止,但是遇到外部庫這樣的另绩,不知道是否對\0做處理的話儒陨,只能在數據傳輸時約定一個特殊key,內容填空字符串笋籽,通過這樣的方式來補全string多余的無用位置了蹦漠。