如何改善Managed Code的Performance和Scalability系列之二:深入理解string和如何高效地使用string

 無論你所使用的是哪種編程語言,我們都不得不承認這樣一個共識:string是我們使用最爲頻繁的一種對象。但是string的常用性並不意味着它的簡單性,而且我認爲,正是由於string的頻繁使用纔會促使其設計人員在string的設計上花大量的功夫。所以正是這種你天天見面的string,蘊含了很多精妙的設計思想。

一個月以前我寫了一篇討論字符串的駐留(string interning)的文章,我今天將會以字符串的駐留爲基礎,進一步來討論.NET中的string。string interning的基本前提是string的恆定性(immutability),即string一旦被創建將不會改變。我們就先來談談string的恆定性。

一、      string是恆定的(immutable)

和其他類型比較,string最爲顯著的一個特點就是它具有恆定不變性:我們一旦創建了一個string,在managed heap 上爲他分配了一塊連續的內存空間,我們將不能以任何方式對這個string進行修改使之變長、變短、改變格式。所有對這個string進行各項操作(比如調用ToUpper獲得大寫格式的string)而返回的string,實際上另一個重新創建的string,其本身並不會產生任何變化。

String的恆定性具有很多的好處,它首先保證了對於一個既定string的任意操作不會造成對其的改變,同時還意味着我們不用考慮操作string時候出現的線程同步的問題。在string恆定的這些好處之中,我覺得最大的好處是:它成就了字符串的駐留。

CLR通過一個內部的interning table保證了CLR只維護具有不同字符序列的string,任何具有相同字符序列的string所引用的均爲同一個string對象,同一段爲該string配分的內存快。字符串的駐留極大地較低了程序執行對內存的佔用。

對於string的恆定性和字符串的駐留,還有一點需要特別指出的是:string的恆定性不單單是針對某一個單獨的AppDomain,而是針對一個進程的。

二、      String可以跨AppDomain共享的(cross-appDomain)

我們知道,在一個託管的環境下,Appdomain是託管程序運行的一個基本單元。AppDomain爲託管程序提供了良好的隔離機制,保證在同一個進程中的不同的Appdomain不可以共享相同的內存空間。在一個Appdomain創建的對象不能被另一個Appdomain直接使用,對象在AppDomain之間傳遞需要有一個Marshaling的過程:對象需要通過by reference或者by value的方式從一個Appdomain傳遞到另一個Appdomain。具體內容可以參照我的另一篇文章:用Coding證明Appdomain的隔離性

但是這裏有一個特例,那就是string。Appdomain的隔離機制是爲了防止一個Application的對內存空間的操作對另一個Application 內存空間的破壞。通過前面的介紹,我們已經知道了string是恆定不變的、是隻讀的。所以它根本不需要Appdomain的隔離機制。所以讓一個恆定的、只讀的string被同處於一個進程的各個Application共享是沒有任何問題的。

String的這種跨AppDomain的恆定性成就了基於進程的字符串駐留:一個進程中各個Application使用的具有相同字符序列的string都是對同一段內存的引用。我們將在下面通過一個Sample來證明這一點。

三、      證明string垮AppDomain的恆定性

在寫這篇文章的時候,我對如何證明string跨AppDomain的interning,想了好幾天,直到我偶然地想到了爲實現線程同步的lock機制。

我們知道在一個多線程的環境下,爲了避免併發操作導致的數據的不一致性,我們需要對一個對象加鎖來阻止該對象被另一個線程 操作。相反地,爲了證明兩個對象是否引用的同一個對象,我們只需要在兩個線程中分別對他們加鎖,如果程序執行的效果和對同一個對象加鎖的情況完全一樣的話,那麼就可以證明這兩個被加鎖的對象是同一個對象。基於這樣的原理我們來看看我們的Sample:

None.gifusing System;
None.gif
using System.Collections.Generic;
None.gif
using System.Text;
None.gif
using System.Threading;
None.gif
None.gif
namespace Artech.ImmutableString
ExpandedBlockStart.gif
{
InBlock.gif    
class Program
ExpandedSubBlockStart.gif    
{
InBlock.gif        
static void Main(string[] args)
ExpandedSubBlockStart.gif        
{
InBlock.gif            AppDomain appDomain1 
= AppDomain.CreateDomain("Artech.AppDomain1");
InBlock.gif            AppDomain appDomain2 
= AppDomain.CreateDomain("Artech.AppDomain2");
InBlock.gif
InBlock.gif            MarshalByRefType marshalByRefObj1 
= appDomain1.CreateInstanceAndUnwrap("Artech.ImmutableString""Artech.ImmutableString.MarshalByRefType"as MarshalByRefType;
InBlock.gif            MarshalByRefType marshalByRefObj2 
= appDomain2.CreateInstanceAndUnwrap("Artech.ImmutableString""Artech.ImmutableString.MarshalByRefType"as MarshalByRefType;
InBlock.gif
InBlock.gif            marshalByRefObj1.StringLockHelper 
= "Hello World";
InBlock.gif            marshalByRefObj2.StringLockHelper 
= "Hello World";
InBlock.gif
InBlock.gif            Thread thread1 
= new Thread(new ParameterizedThreadStart(Execute));
InBlock.gif            Thread thread2 
= new Thread(new ParameterizedThreadStart(Execute));
InBlock.gif
InBlock.gif            thread1.Start(marshalByRefObj1);
InBlock.gif            thread2.Start(marshalByRefObj2);
InBlock.gif
InBlock.gif            Console.Read();            
ExpandedSubBlockEnd.gif        }

InBlock.gif
InBlock.gif        
static void Execute(object obj)
ExpandedSubBlockStart.gif        

InBlock.gif            MarshalByRefType marshalByRefObj 
= obj as MarshalByRefType;
InBlock.gif            marshalByRefObj.ExecuteWithStringLocked();
ExpandedSubBlockEnd.gif        }

ExpandedSubBlockEnd.gif    }

InBlock.gif
InBlock.gif    
class MarshalByRefType : MarshalByRefObject
ExpandedSubBlockStart.gif    
{
ContractedSubBlock.gif        
Private Fields
InBlock.gif
ContractedSubBlock.gif        
Public Properties
InBlock.gif
ContractedSubBlock.gif        
Public Methods
ExpandedSubBlockEnd.gif    }

ExpandedBlockEnd.gif}

我們來簡單地分析一下上面的coding.

我們創建了一個繼承自MarshalByRefObject,因爲我需要讓它具有跨AppDomain傳遞的能力。在這個Class中定義了兩個爲實現線程同步的helper字段,一個是string類型的_stringLockHelper和object類型的_objectLockHelper,併爲他們定義了相應的Property。此外定義了兩個方法:ExecuteWithStringLocked和ExecuteWithStringLocked,他們的操作類似:在先對_stringLockHelper和_objectLockHelper加鎖的前提下,輸出出操作執行的AppDomain和確切時間。我們通過調用Thread.Sleep模擬10s的時間延遲。

在Main方法中,首先創建了兩個AppDomain,名稱分別爲Artech.AppDomain1和Artech.AppDomain2。隨後在這兩個AppDomain中創建兩個MarshalByRefType對象,併爲它們的StringLockHelper屬性賦上相同的值:Hello World。最後,我們創建了兩個新的線程,並在它們中分別調用在兩個不同AppDomain 中創建的MarshalByRefType對象的ExecuteWithStringLocked方法。我們來看看運行後的輸出結果:

 

從上面的輸出結果中可以看出,兩個分別在不同線程中執行操作對應的AppDomain的name分別爲Artech.AppDomain1和Artech.AppDomain2。執行的時間(確切地說是操作成功地對MarshalByRefType對象的_stringLockHelper字段進行加鎖的時間)相隔10s,也就是我們在程序中定義的時間延遲。

爲什麼會出現這樣的結果呢?我們只是對兩個處於不同AppDomain的不同的MarshalByRefType對象的stringLockHelper字段進行加鎖。由於我們是同時開始他們對應的線程,照理說它們之間不會有什麼關聯,顯示出來的時間應該是相同的。唯一的解釋就是:雖然這兩個在不同的AppDomain中創建的對象是兩個完全不同的對象,由於他們的stringLockHelper字段具有相同的字符序列,它們引用的是同一個string。這就證明了我們提出的跨AppDomain進行string interning的結論。

爲了進一步印證我們的結論,我們是使兩個MarshalByRefObject對象的stringLockHelper字段具有不同的值,看看結果又如何。於是我們把其中一個對象的stringLockHelper字段改爲”Hello World!”(多加了一個!) 。

None.gifmarshalByRefObj1.StringLockHelper = "Hello World";
None.gifmarshalByRefObj2.StringLockHelper 
= "Hello World!";
None.gif

看看現在的輸出結果,現在的時間是一樣了。


上面我們做的是對string類型字段加鎖的試驗。那麼我們對其他類型的對象進行加鎖,又會出現怎麼的情況呢?我們現在就來做這樣試驗:在各自的線程中調用兩個對象的ExecuteWithObjectLocked方法。我們修改Execute方法和Main()。

None.gifstatic void Execute(object obj)
ExpandedBlockStart.gif        

InBlock.gif            MarshalByRefType marshalByRefObj 
= obj as MarshalByRefType;
InBlock.gif            marshalByRefObj. ExecuteWithObjectLocked ();
ExpandedBlockEnd.gif}

None.gifstatic void Main(string[] args)
ExpandedBlockStart.gif        
{
InBlock.gif            AppDomain appDomain1 
= AppDomain.CreateDomain("Artech.AppDomain1");
InBlock.gif            AppDomain appDomain2 
= AppDomain.CreateDomain("Artech.AppDomain2");
InBlock.gif
InBlock.gif            MarshalByRefType marshalByRefObj1 
= appDomain1.CreateInstanceAndUnwrap("Artech.ImmutableString""Artech.ImmutableString.MarshalByRefType"as MarshalByRefType;
InBlock.gif            MarshalByRefType marshalByRefObj2 
= appDomain2.CreateInstanceAndUnwrap("Artech.ImmutableString""Artech.ImmutableString.MarshalByRefType"as MarshalByRefType;
InBlock.gif
InBlock.gif            
object obj = new object();
InBlock.gif            marshalByRefObj1.ObjectLockHelper 
= obj;
InBlock.gif            marshalByRefObj2.ObjectLockHelper 
= obj;
InBlock.gif
InBlock.gif            Thread thread1 
= new Thread(new ParameterizedThreadStart(Execute));
InBlock.gif            Thread thread2 
= new Thread(new ParameterizedThreadStart(Execute));
InBlock.gif
InBlock.gif            thread1.Start(marshalByRefObj1);
InBlock.gif            thread2.Start(marshalByRefObj2);
InBlock.gif
InBlock.gif            Console.Read();            
ExpandedBlockEnd.gif        }

None.gif

我們先來看看運行後的輸出結果:


我們發現兩個時間是一樣的,那麼就是說兩個對象的ObjectLockHelper引用的不是同一個對象。雖然上面的程序很簡單,我覺得裏面涉及的規程卻很值得一說。我們來分析下面3段代碼。

None.gifobject obj = new object();
None.gifmarshalByRefObj1.ObjectLockHelper 
= obj;
None.gifmarshalByRefObj2.ObjectLockHelper 
= obj;
None.gif

簡單看起來,兩個MarshalByRefObject對象的ObjectLockHelper都是引用的同一個對象obj。但是背後的情況沒有那麼簡單。代碼第一行創建了一個新的對象obj,這個對象是在當前AppDomain 中創建的。二對於當前的AppDomain來說,marshalByRefObj1和marshalByRefObj2僅僅是一個Transparent proxy而已,它們包含一個在Artech.AppDomain1和Artech.AppDomain2中創立的MarshalByRefObject對象的引用。我們爲它的ObjectLockHelper複製,對於Transparent proxy對象的賦值調用會傳到真正對象所在的AppDomain,由於obj是當前AppDomain的對象,它不能直接賦給另一個AppDomain的對象。所以它必須經歷一個Marshaling的過程才能被傳遞到另外一個AppDomain。實際上當複製操作完成之後,真正的ObjectLockHelper屬性對應的對象是根據原數據重建的對象,和在當前AppDomain中的對象已經沒有任何的關係。所以兩個MarshalByRefObject對象的ObjectLockHelper屬性引用的並不是同一個對象,所以對它進行加鎖對彼此不要產生任何影響。

四、      從Garbage Collection的角度來看string

我們知道在一個託管的環境下,一個對象的生命週期被GC管理和控制。一個對象只有在他不被引用的時候,GC纔會對他進行垃圾回收。而對於一個string來說,它始終被interning table引用,而這個interning table是針對一個Process的,是被該Process所有AppDomain共享的,所以一個string的生命週期相對比較長,只有所有的AppDomain都不具有對該string的引用時,他纔有可能被垃圾回收。

五、      從多線程的角度來看string

一方面由於string的恆定性,我們不用考慮多線程的併發操作產生的線程同步問題。另一方面由於字符串的駐留,我們在對一個string對象進行加鎖操作的時候,極有可能拖慢這個Application的performance,就像我們的Sample中演示的那樣。而且很有可能影響到處於同一進程的其他Application,以致造成死鎖。所以我們在使用鎖的時候,除非萬不得已,切忌對一個string進行加鎖。

六、      如何高效地使用string

下面簡單介紹一些高效地使用string的一些小的建議:

1. 儘量使用字符串(literal string)相加來代替字符串變量和字符創相加,因爲這樣可以使用現有的string操作指令進行操作和利用字符串駐留。

比如:

None.gifstring s = "abc" + "def";

優於

None.gifstring s = "abc";
None.gif
= s + "def";
None.gif

2. 在需要的時候使用StringBuilder對string作頻繁的操作:

由於string的恆定性,在我們對一個string進行某些操作的時候,比如調用ToUpper()或者ToLower()把某個string每個字符轉化成大寫或者小寫;調用SubString()取子串;會創建一個新的string,有時候會創建一些新的臨時string。這樣的操作會增加內存的壓力。所有在對string作頻繁操作的情況下,我們會考慮使用StringBuilder來高效地操作string。StringBuilder之所以能對string操作帶來更好的performance,是因爲在它的內部維護一個字符數組,而不是一個string來避免string操作帶來的新的string的創建。

StringBuilder是一個很好的字符累加器,我們應該充分地利用這一個功能:

None.gifStringBuilder sb = new StringBuilder();
None.gifsb.Append(str1 
+ str2);
None.gif

最好寫成

None.gifStringBuilder sb = new StringBuilder();
None.gifsb.Append(str1);
None.gifsb.Append(str2);
None.gif

避免創建一個新的臨時string來保存str1 + str2。

再比如下面的Code

None.gifStringBuilder sb = new StringBuilder();
None.gifsb.Append(WorkOnString1());
None.gifsb.Append(WorkOnString2());
None.gifsb.Append(WorkOnString3());
None.gif

最好寫好吧WorkOnString1,WorkOnString2,WorkOnString3定義成:

None.gifWorkOnString1(StringBuilder sb)
None.gifWorkOnString2(StringBuilder sb)
None.gifWorkOnString3(StringBuilder sb)
None.gif

3. 高效地進行string的比較操作

我們知道,對象之間的比較有比較Value和比較Reference之說。一般地對Reference進行比較的速度最快。對於string,在字符串駐留的前提下,我們可以把對Value的比較用Reference的比較來代替從而會的Performance的提升。

此外,對於忽略大小寫的比較,我們最好使用string的static方法Compare(string strA, string strB, bool ignoreCase)。也就是說:

None.gifif(str1.ToLower()==str2.ToLower())

最好寫成

None.gifIf(string. Compare(str1,str2,true))

原文:http://www.cnblogs.com/artech/archive/2007/05/06/737130.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章