在.NET程式中小心使用String型別

iDotNetSpace發表於2010-03-19
在實際程式中,String型別用得非常廣泛,然而,由於.NET對String型別變數的獨特管理方式,使用不當,會嚴重影響程式的效能。我們分幾個方面來談這個問題:

1 瞭解String資料的記憶體分配方式


 編寫一個控制檯應用程式,輸入以下測試程式碼:

    class Program
    {
        static void Main(string[] args)
        {
            String s = "a";
            s = "abcd";
        }
    }

 使用.NET Framework 2.0 SDK提供的ildasm.exe工具檢視生成的MSIL指令:

01  .method private hidebysig static void  Main(string[] args) cil managed
02  {
03    .entrypoint
04    // 程式碼大小       14 (0xe)
05    .maxstack  1
06    .locals init ([0] string s)
07    IL_0000:  nop
08    IL_0001:  ldstr      "a"
09    IL_0006:  stloc.0
10    IL_0007:  ldstr      "abcd"
11    IL_000c:  stloc.0
12    IL_000d:  ret
13  } // end of method Program::Main

 簡要解釋一下上述MSIL指令程式碼:
 第06句給區域性變數s分配一個索引號(索引號從0開始,如函式中有多個區域性變數,其索引號按在函式中出現的順序加一)。
 在編譯時編譯器會將程式碼中的兩個字串“a”和“abcd”寫入到程式集的後設資料(metadata)中,此時,這兩個字串被稱為“字串字面量(string literal)”。
 第08句使用ldstr指令為字串物件“a”分配記憶體,並將此物件引用壓入到執行緒堆疊中。
 第09句使用stloc指令從執行緒堆疊頂彈出先前壓入的物件引用,將其傳給區域性變數s(其索引號為0)。
 同樣的過程對“abcd”重複進行一次,所以這兩句簡單的程式碼

            String s = "a";
            s = "abcd";

 將會導致CLR使用ldstr指令分配兩次記憶體。
 根據上述分析,讀者一定明白了String變數的內容是隻讀的,給其賦不同的值將會導致記憶體的重新分配。因此,為提高程式效能,程式設計時應儘量減少記憶體的分配操作。
 下面對程式碼中常見的字串用法進行分析,從中讀者可以知道如何避免嚴重影響程式效能的字串操作。

2 儘量少使用字串加法運算子


 請看以下兩段程式碼:

  (1)          String s1 = "ab";
                     s1+="cd";
  (2)          String s1="ab"+"cd";

 這兩段程式碼執行結果一樣,但速度一樣快嗎?
 請看第(1)段程式碼生成的MSIL指令:

  .locals init ([0] string s1)
  IL_0000:  nop
  IL_0001:  ldstr      "ab"
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  ldstr      "cd"
  IL_000d:  call       string [mscorlib]System.String::Concat(string,
                                                              string)
  IL_0012:  stloc.0
  IL_0013:  ret

 再看第(2)段程式碼生成的指令:

 .locals init ([0] string s1)
  IL_0000:  nop
  IL_0001:  ldstr      "abcd"
  IL_0006:  stloc.0
  IL_0007:  ret

 可以很清楚地看到,第(1)段程式碼將導致String類的Concat()方法被呼叫(實現字串加法運算)。對於第(2)段程式碼,由於C#編譯器聰明地在編譯時直接將兩個字串合併為一個字串字面量,所以程式執行時CLR只呼叫一次ldstr指令就完成了所有工作,其執行速度誰快就不言而喻了!


3 避免使用加法運算子連線不同型別的資料
 

請看以下程式碼:

 String str = "100+100=" + 200;
     Console.Writeline(str);

 生成的MSIL指令為:

  .maxstack  2
  .locals init ([0] string str)
  IL_0000:  nop
  IL_0001:  ldstr      "100+100="
  IL_0006:  ldc.i4     0xc8
  IL_000b:  box        [mscorlib]System.Int32
  IL_0010:  call       string [mscorlib]System.String::Concat(object,
                                                              object)
  IL_0015:  stloc.0
  IL_0016:  ldloc.0
  IL_0017:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_001c:  nop
  IL_001d:  ret

 可以清晰地看到,這兩句C#程式碼不僅導致了String類的Concat()方法被呼叫(IL_0010),而且還引發了裝箱操作(IL_000b)!
 Concat()方法會導致CLR為新字串分配記憶體空間,而裝箱操作不僅要分配記憶體,還需要建立一個匿名物件,物件建立之後還必須有一個資料複製的過程,代價不菲!
 改為以下程式碼:

            String str = "100+100=";
            Console.Write(str);
            Console.WriteLine(200);

 生成的MSIL指令為:

  .maxstack  1
  .locals init ([0] string str)
  IL_0000:  nop
  IL_0001:  ldstr      "100+100="
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  call       void [mscorlib]System.Console::Write(string)
  IL_000d:  nop
  IL_000e:  ldc.i4     0xc8
  IL_0013:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0018:  nop
  IL_0019:  ret

 可以看到,雖然多了一次方法呼叫(Console.Write)方法,但卻避免了複雜的裝箱操作,也避免了呼叫String.Concat()方法對記憶體的頻繁分配操作,效能更好。


4.在迴圈中使用StringBuilder代替String實現字串連線


 在某些場合需要動態地將多個子串連線成一個大字串,比如許多複雜的SQL命令都是通過迴圈語句生成的。這時,應避免使用String類的加法運算子,舉個簡單的例項:

            String str ="";
            for (int i = 1; i <= 10; i++)
            {
                str += i;
                if(i<10)
                    str += "+";
            }

 上述程式碼將生成一個字串:1+2+…+10。
 有了前面的知識,讀者一定知道這將導致進行10次裝箱操作,19次字串記憶體分配操作(由String.Concat()方法引發),由於生成的MSIL指令太長,此處不再列出,請讀者自行用ildasm.exe工具檢視上述程式碼生成的MSIL指令。
 改為以下程式碼,程式效能會好很多:

           //預先分配1K的記憶體空間
            StringBuilder sb = new StringBuilder(1024);
            for (int i = 1; i <= 10; i++)
            {
                sb.Append(i);
                if(i<10)
                    sb.Append("+");
            }
            String result = sb.ToString();

 通過使用ildasm.exe工具檢視生成的MSIL程式碼,發現雖然上述程式碼生成的MSIL指令比前面多了7條,但卻避免了耗時的裝箱操作,而且記憶體分配的次數也少了很多。當迴圈的次數很大時,兩段程式碼的執行效能差異很大。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-629902/,如需轉載,請註明出處,否則將追究法律責任。

相關文章