String與Stringbuilder組字串的效能比較

Stringbuilder組字串的效能一定比String好?那可不一定.

  長久以來,一直有個模糊且不正確的觀念,雖然知道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判斷,所以就有可能發生要組或不用組的情況,不過在這個例子下,所做的測試是都要組的,而字串的串接是使用+=的方式.
        int z=0;x=0;
        public string GetDynStr()
        {
            string a = "abcdefghijklmnopqrstuvwxyz : ";
            if (z == x)
            {
                a += "abcdefghijklmnopqrstuvwxyz";
            }

            return a;
        }
 
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) :


private void btn_DynStrOnce_Click(object sender, EventArgs e)
{
    System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
    string a = new string('a', 1000000);
    sw.Start();
    DynStrOnce(a);
    sw.Stop();
    MessageBox.Show(string.Format("耗時(毫秒) : {0} ", sw.ElapsedMilliseconds.ToString("#,##0")));
}


public string DynStrOnce(string a)
{          
    if (z == x)
    {
        a += "abcdefghijklmnopqrstuvwxyz";
    }

    if (z == x)
    {
        a += "abcdefghijklmnopqrstuvwxyz";
    }

    if (z == x)
    {
        a += "abcdefghijklmnopqrstuvwxyz";
    }

    return a;
}

 

String 動態大量字串做法 2 (1) :


private void btn_DynStrBuOnce_Click(object sender, EventArgs e)
{
    System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
    string a = new string('a', 1000000);
    sw.Start();
    DynStrBuOnce(a);
    sw.Stop();
    MessageBox.Show(string.Format("耗時(毫秒) : {0} ", sw.ElapsedMilliseconds.ToString("#,##0")));
}


public string DynStrBuOnce(string a)
{
    StringBuilder s = new StringBuilder(a);
    if (z == x)
    {
        s.Append("abcdefghijklmnopqrstuvwxyz");
    }

    if (z == x)
    {
        s.Append("abcdefghijklmnopqrstuvwxyz");
    }

    if (z == x)
    {
        s.Append("abcdefghijklmnopqrstuvwxyz");
    }

    return s.ToString();
}

  此時String的做法耗時9毫秒,Stringbuilder3毫秒,所以當資料量愈大時,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月刊