標籤:
字串應該是所有程式設計語言中使用最頻繁的一種基礎資料類型。如果使用不慎,我們就會為一次字串的操作所帶來的額外效能開銷而付出代價。本條建議將從兩個方面來探討如何規避這類效能開銷:
1. 確保盡量少的裝箱
2. 避免分配額外的記憶體空間。
第一個方面:確保盡量少的裝箱
對於裝拆箱,我們應該不陌生,實值型別轉換成參考型別即為裝箱, 參考型別轉換成實值型別即為拆箱。 在自己編寫的代碼中,應當儘可能的避免編寫不必要的裝箱代碼。裝箱之所以會帶來效能損耗,因為它需要完成下面三個步驟:
• 首先,會為實值型別在託管堆中分配記憶體。除了實值型別本身所分配的記憶體外,記憶體總量還要加上類型對象指標和同步塊索引所佔用的記憶體。
• 然後,將實值型別的值複製到新分配的堆記憶體中。
• 最後,返回已經成為參考型別的對象的地址。
下面是一行最簡單的裝箱代碼
1 object obj = 1;
這行語句將整型常量1賦給object類型的變數obj; 眾所周知常量1是實值型別,實值型別是要放在棧上的,而object是參考型別,它需要放在堆上;要把實值型別放在堆上就需要執行一次裝箱操作。
這行語句的IL代碼如下,請注意注釋部分說明:
.locals init ([0] object objValue) //以上三行IL表示聲明object類型的名稱為objValue的局部變數 IL_0000: nopIL_0001: ldc.i4.s 9 //表示將整型數9放到棧頂IL_0003: box [mscorlib]System.Int32 //執行IL box指令,在記憶體堆中申請System.Int32類型需要的堆空間IL_0008: stloc.0 //彈出堆棧上的變數,將它儲存到索引為0的局部變數中
以上就是裝箱所要執行的操作了,執行裝箱操作時不可避免的要在堆上申請記憶體空間,並將堆棧上的實值型別資料複製到申請的堆記憶體空間上,這肯定是要消耗記憶體和cpu資源的。我們再看下拆箱操作是怎麼回事:
請看下面的C#代碼:
object objValue = 4;int value = (int)objValue;
上面的兩行代碼會執行一次裝箱操作將整形數字常量4裝箱成參考型別object變數objValue;然後又執行一次拆箱操作,將儲存到堆上的引用變數objValue儲存到局部整形實值型別變數value中。
同樣我們需要看下IL代碼:
.locals init ([0] object objValue,[1] int32 ‘value‘) //上面IL聲明兩個局部變數object類型的objValue和int32類型的value變數IL_0000: nopIL_0001: ldc.i4.4 //將整型數字4壓入棧IL_0002: box [mscorlib]System.Int32 //執行IL box指令,在記憶體堆中申請System.Int32類型需要的堆空間IL_0007: stloc.0 //彈出堆棧上的變數,將它儲存到索引為0的局部變數中IL_0008: ldloc.0//將索引為0的局部變數(即objValue變數)壓入棧IL_0009: unbox.any [mscorlib]System.Int32 //執行IL 拆箱指令unbox.any 將參考型別object轉換成System.Int32類型IL_000e: stloc.1 //將棧上的資料存放區到索引為1的局部變數即value
拆箱操作的執行過程和裝箱操作過程正好相反,是將儲存在堆上的參考型別值轉換為實值型別並給實值型別變數。
裝箱操作和拆箱操作是要額外耗費CPU和記憶體資源的。那如何避免裝箱和拆箱操作呢?有以下方法:
1. 用泛型集合取代ArrayList。
2. 用C#內建的轉換方法,將實值型別轉換為參考型別。
下面我們看下使用泛型和不使用泛型引發裝箱拆箱的情況。
1.使用非泛型集合時引發的裝箱和拆箱操作
看下面的一段代碼:
var array = new ArrayList();array.Add(1);array.Add(2);foreach (int value in array){Console.WriteLine(“value is {0}”,value);}
代碼聲明了一個ArrayList對象,向ArrayList中添加兩個數字1,2;然後使用foreach將ArrayList中的元素列印到控制台。
在這個過程中會發生兩次裝箱操作和兩次拆箱操作,在向ArrayList中添加int類型元素時會發生裝箱,在使用foreach枚舉ArrayList中的int類型元素時會發生拆箱操作,將object類型轉換成int類型,在執行到Console.WriteLine時,還會執行兩次的裝箱操作;這一段代碼執行了6次的裝箱和拆箱操作;如果ArrayList的元素個數很多,執行裝箱拆箱的操作會更多。
你可以通過使用ILSpy之類的工具查看IL代碼的box,unbox指令查看裝箱和拆箱的過程
2. 使用泛型集合的情況
請看如下代碼:
1 var list = new List<int>();2 list.Add(1);3 list.Add(2);4 5 foreach (int value in list)6 {7 Console.WriteLine("value is {0}", value);8 }
代碼和1中的代碼的差別在於集合的類型使用了泛型的List,而非ArrayList;我們同樣可以通過查看IL代碼查看裝箱拆箱的情況,上述代碼只會在Console.WriteLine()方法時執行2次裝箱操作,不需要拆箱操作。
可以看出泛型可以避免裝箱拆箱帶來的不必要的效能消耗;當然泛型的好處不止於此,泛型還可以增加程式的可讀性,使程式更容易被複用等等。
但是我們注意到,在使用泛型集合的時候,Console.WriteLine()方法時仍然執行2次裝箱操作。能否將這兩次裝箱操作也最佳化掉呢?這就使用到了第二個方法,用C#內建的轉換方法,將實值型別轉換為參考型別。如下:
var list = new List<int>();list.Add(1);list.Add(2);foreach (int value in list){Console.WriteLine(string.Format("value is {0}", value.ToString()));}
再查看IL代碼時可以發現,裝箱操作已經被徹底消除了。它實際調用的是整形的ToString方法。ToString方法的原型為:
public override string ToString(){return Number.FormatInt32(m_value, null, NumberFormatInfo.CurrentInfo);}
它是通過直接操作記憶體來完成從int到string的轉換,效率要比裝箱高很多。所以,在使用其他實值型別到字串的轉換並完成拼接時,應當避免使用操作符“+”來完成,而應該使用實值型別提供的ToString方法。
第二個方面:避免分配額外的記憶體空間。
對CLR來說,string對象是個很特殊的對象,它一旦被賦值就不可改變。在運行時調用System.String類中的任何方法或進行任何運算(如“=”賦值,“+”拼接等),都會在記憶體中建立一個新的字串對象,這也意味著要為該新對象分配新的記憶體空間。像下面的代碼就會帶來運行時的額外開銷。
private static void Test6(){string s1 = "abc";s1 = "123" + s1 + "456"; // 以上兩行代碼建立了3個String對象,並執行了一次String.Contact方法string s2 = 9 + "456"; // 該代碼發生一次裝箱,並調用一次String.Concact方法}private static void Test7(){string s1 = "123" + "abc" + "456"; // 該代碼等效於string s1 = "123abc456"}
由於使用String類會在某些場合帶來明顯的效能損耗,所以微軟另外提供了一個類型StringBuilder來彌補String的不足。
StringBuilder並不會重新建立一個String對象,它的效率源於預先以非託管的方式分配記憶體。如果StringBuilder沒有先定義長度,則預設分配的長度為16,當StringBuilder字元長度小於等於16時,StringBuilder不會重新分配記憶體。當StringBuilder字元長度大於16時小於32時,StringBuilder又會重新分配記憶體,使之成為16的倍數。在上面的代碼中,如果預先判斷字串的長度將大於16,則可以為其設定一個更加合適的長度。
微軟還提供了另外一個方法來簡化這種操作,即使用string.Format方法。string.Format方法在內部使用StringBuilder進行字串的格式化。
private static void Test9(){string a = "t";string b = "e";string c = "s";string d = "t";StringBuilder sb = new StringBuilder();sb.Append(a);sb.Append(b);sb.Append(c);sb.Append(d);Console.WriteLine(sb.ToString());}private static void Test10(){string a = "t";string b = "e";string c = "s";string d = "t";Console.WriteLine(string.Format("{0}{1}{2}{3}", a, b, c, d));}
最後總結:如何正確操作字串:
1. 確保盡量少的拆裝箱操作:使用泛型,使用ToString()將實值型別轉換為參考型別
2. 避免分配額外的記憶體空間:不用+=, +操作符, 使用StringBuilder, String.Format()連結多個String
參考引用列表:?
http://www.cnblogs.com/yukaizhao/archive/2011/10/18/csharp_box_unbox_1.html
http://www.cnblogs.com/yukaizhao/archive/2011/10/19/csharp_box_unbox_2.html
?《編寫高品質代碼:改善C#程式的157個建議》
C#中如何正確的操作字串?