長久以來,一直有個模糊且不正確的觀念,雖然知道 StringBuilder效能比String好,但並不清楚是在什麼樣的情況下比較好,加上最近有同事提到這部份的效能問題,於是就寫了一些程式來測看 看,而這次反組譯程式也拿出來用了,主要是看原本程式的寫法在經過Compiler後會變為什麼樣子.
這次的測法所用的計時方式,就會用到System.Diagnostics.Stopwatch來計時,所會測的次數除了額外註明的之外,其它均為1萬 次,因為有些跑太多次,我會等到睡著.CPU等級為Intel Dual E2180 2.00 GHz,RAM 2G,由於是String與StringBuilder的比較,所以務求比較寫法的執行結果必需相同,而測試的幾個寫法也是平時較為常見的.
首先所要提到的幾個方式,是字串在設計階段就已經固定,在執行階段也不會有任何改變的做法,這種情況之下,String跟StringBuilder的效能表現,是否StringBuilder還是比String的做法還來的好.
String 固定字串組合做法1 (十萬次) :
字串是用+的方式,把每一個String給串接在一起.
public string GetStaticStr1()

...{
string a = "abcdefghijklmnopqrstuvwxyz : " +
"abcdefghijklmnopqrstuvwxyz : " +
"abcdefghijklmnopqrstuvwxyz";
return a;
}這種做法在Compiler後,就有很明顯的不同.
Public string GetStaticStr1()

...{
Return "abcdefghijklmnopqrstuvwxyz : abcdefghijklmnopqrstuvwxyz : abcdefghijklmnopqrstuvwxy";
}連變數a也沒有,直接return已組好的字串,所以在執行時,就直接使用組好的字串,沒有再花費其它動作,也因此在效能上表現最好,耗時2毫秒.
String 固定字串組合做法2 (十萬次) :
另一種字串的串法也是很常用的做法,使用+=的方式來串接.
public string GetStaticStr2()

...{
string a = "abcdefghijklmnopqrstuvwxyz : ";
a += "abcdefghijklmnopqrstuvwxyz : ";
a += "abcdefghijklmnopqrstuvwxyz";
return a;
}這種方式在Compiler後,原本以為會跟前一個做法一樣,但結果是有差異的.
public string GetStaticStr2()

...{
string a="abcdefghijklmnopqrstuvwxyz : ";
return (a+" abcdefghijklmnopqrstuvwxyz : "+" abcdefghijklmnopqrstuvwxyz");
}因為還有一些變數宣告及串接的動作,所以效能的表現較第一個做法差了一點,耗時32毫秒.
String 固定字串組合做法3 (十萬次) :
再來的做法是使用StringBuilder的方式把字串給串起來
public string GetStaticStrBu()

...{
StringBuilder s = new StringBuilder();
s.Append("abcdefghijklmnopqrstuvwxyz : ");
s.Append("abcdefghijklmnopqrstuvwxyz : ");
s.Append("abcdefghijklmnopqrstuvwxyz");
return s.ToString();
}
所以當字串是已知的固定不變文字時,用String來組字串會比StringBuilder還快.
做法1 : 耗時2毫秒
做法2 : 耗時32毫秒
做法3 : 耗時60毫秒
接下來,字串可能會有經過if的判斷式或for迴圈及string.Format的方式去串的做法,其Stringbuilder的效能表現是否就能發揮出來.
String 動態少量字串做法 1 (十萬次) :
這段字串在組合之前,會先經過if判斷,所以就有可能發生要組或不用組的情況,不過在這個例子下,所做的測試是都要組的,而字串的串接是使用+=的方式.
Compiler後,程式碼並沒有改變,而耗時為14毫秒.
String 動態少量字串做法 2 (十萬次) :
測試寫法跟做法1一樣,只不過是使用StringBuilder把它串起來
public string GetDynStrBu()

...{
StringBuilder s = new StringBuilder();
s.Append("abcdefghijklmnopqrstuvwxyz : ");
if (z == x)

...{
s.Append("abcdefghijklmnopqrstuvwxyz : ");
}
return s.ToString();
}Compiler後的程式碼也是一樣,耗時37毫秒.怎麼動態字串的Stringbuilder的表現一樣不如String?
在這個地方先做個小結論,因為這個Case有一個特別的地方,應該是說,我的if的判斷不多,所以字串在串接的次數很少,僅會串接一次,所以如果只有少 量的串接,string與Stringbuilder的效能差異並看不太出來,而String的效能可能還優於Stringbuilder,但當把同樣的 if判斷增加為6個時,其測試結果就不同了,Stringbuilder(121毫秒)的效能就高於string(144毫秒),增加愈多,差異愈大.
補充 :
或許用迴圈的方式,容易讓人有種錯覺,感覺自己的程式怎麼可能跑迴圈那麼多次,那就用String就好了,其實應該是要看變數裡的”資料量”,只是範例裡用的資料量都不多,所以才用大量迴圈的方式去凸顯出它的效能差異.如果今天資料量很大,不需要用迴圈,一次就可以看出它的差異,就以上面兩個程式做一些修改調整.
String 動態大量字串做法 1 (1次) :
String 動態大量字串做法 2 (1次) :
此時String的做法耗時9毫秒,Stringbuilder為3毫秒,所以當資料量愈大時,String在組字串的效能愈差,而Stringbuiler的效能會比String好更多.
再下來,所要測的是Format的串接方式,這種做法也有很多人在用,有些是為了程式閱讀性較好,所以就來看看,String與Stringbuilder在用Format的大量串接字串時,各個的表現如何.
String 動態大量字串使用Format的做法 1 :
這種做法也很常見,因為程式碼很簡潔,直接把Format後的值放到變數內,同樣達成字串串接的目的.
string tmps = "";
for (int i = 0; i < 10000; i++)

...{
tmps = string.Format("...{0}{1}{2}",tmps, "T", i);
}這做法看來簡潔,但跑出來的結果嚇人,竟然用了1758毫秒.
String 動態大量字串使用Format的做法 2 :
這次改一點地方,使用+=的方式來串.
string tmps = "";
for (int i = 0; i < 10000; i++)

...{
tmps += string.Format("...{0}{1}", "T", i);
}跑出來的結果是好多了,但仍然很差,用了687毫秒.
String 動態大量字串使用Format的做法 3 :
那麼把這些改為Stringbuilder的方式來串看看.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++)

...{
sb.AppendFormat("...{0}{1}","T", i);
}看起來差異是不大,但效能卻是相當的好,差了幾百倍以上,只耗用了6毫秒.
結論 : 果然Stringbuilder在用Format組大量字串時,效能是最好的.
最後要比較的是最簡單的大量字串串接,不使用Format的方式,string直接用+,而Stringbuilder則是用Append的方式來串.
String 動態大量字串做法 1 :
string tmps = "";
for (int i = 0; i < 10000; i++)

...{
tmps = tmps+ "T"+i;
}結果如預期的差,但至少比Format的效能還來的好,耗時680毫秒.
String 動態大量字串做法 2 :
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++)

...{
sb.Append("T");
sb.Append(i);
} 這做法就是最快的,耗時3毫秒,跟String差了2百多倍.所以如非必要性,就不要用Format的方式來串字串,單純的Append的效能反而更好.而大量動態字串在串接時,也千萬別用string,效能真的很差.
以上的所有寫法,就大概示範出在那種情況下,要用那種寫法,當字串在設計階段時就固定,那麼就用string,用+的方式來串接的效能最好,因為在Compiler後,就會將字串直接組合起來,不需要在run time再組一次,如果是在少量且資料量不大的動態字串串接時,string跟stringbuilder的差異並不大,甚至string可能還比Stringbuilder的表現還來的好,但超過一定數量後或當字串數量很大時,string 就每況愈下,而Stringbuilder就漸入佳境,而Format的方式,效能並不會比直接Append來的好,所以非必要就用Append即可,當 然,最後在大量動態字串串接變現最好的就是Stringbuilder.Append了,主要是因為StringBuilder會在一開始先配置較大的記 憶體位置(可以自行指定Capacity,如果沒指定就採預設16),供作字串相組 之用,如果不夠,才會再重新配置一塊,而String在一開始並沒有預留相組字串用的記憶體空間,所以在組字串時,每次均需重新配置記憶體位置,所以在動 態組字串例如用for迴圈在組時,StringBuilder的表現會優於String. 那麼如果是SQL語法內的變數Parameters呢?例如"select * from TestTable where testid=@tid",以這個例子來說,由於變數是資料庫端在轉換使用的,並不是在程式端就把變數先轉好串接傳進去的,所以這個SQL語法的字串是固定的,並不會因為傳入的變數不同而異動到字串,也因此用string的方式就好.
註 : MSDN 效能解說如下.
Concat 和 AppendFormat 方法都會將新的資料串連到現有的 String 或 StringBuilder 物件。String 物件串連作業永遠都會從現有的字串和新資料建立新的物件。StringBuilder 物件會維護一個緩衝區,以容納新資料的串連。如果有可用的空間,新的資料會附加至緩衝區的尾端,否則,會配置較大的新緩衝區,而原始緩衝區的資料會複製到 新的緩衝區,然後新的資料會附加至新的緩衝區。
String 或 StringBuilder 物件之串連作業的效能是根據記憶體的配置頻率而定。String 串連作業永遠都會配置記憶體,而 StringBuilder 串連作業只有在 StringBuilder 物件緩衝區太小而無法容納新資料時,才會配置記憶體。因此,如果要串連固定數目的 String 物件,最好使用 String 類別的串連作業。在這種情況下,編譯器 (Compiler) 甚至可能將個別的串連作業結合成一個單一作業。如果要串連任意數目的字串 (例如,如果迴圈串連任意數目的使用者輸入字串),則對於串連作業來說最好使用 StringBuilder 物件。
參考資料 :
書籍 : Run!PC 2008 2月刊