CLR中字串不變性的最佳化http://www.blogcn.com/user8/flier_lu/index.html?id=1269085&run=.06FE977
自從有程式設計語言以來,如何處理字串就一直是一個爭論不休的問題。從C/C++用字元數組表示字串,讓使用者完全控制其生命週期;到Delphi/VB通過編譯器內建支援,使用引用計數自動維護字串生命週期;再到Java/C#通過不可變字串以及記憶體回收管理生命週期。不同的策略有著不同的傾向性,也有各自的缺點和優點。這兒我不想評論多種策略之間的優劣,只是想針對C#的實現做一點點較為深入的探討。
CLR中選擇了和Java類似的不可變字串策略,以簡化生命期維護以及多線程同步問題的處理,但同時也付出了一定的效率和空間上的代價,故而不得不通過編譯器一級定製來最佳化。
Chris Brumme和Yun Jin在其BLog上討論了需要保障字串不變性(immutability)的原因,並指出通過PInvoke以及unsafe代碼直接修改字串內容可能帶來的危害。
Interning Strings & immutability
Dangerous PInvokes - string modification
為了提高效率和節約空間,CLR內部實際上維護了一個不可變字串表。在堆中分配的字串可以通過String.Intern函數確保其被加入此表;通過String.IsInterned函數判斷自己是否在表中。如果在表中,則可以通過引用來直接對字串進行比較,大大提高字串比較效率。MSDN上的例子如下
以下為引用:
// Sample for String.Intern(String)
using System;
using System.Text;
class Sample {
public static void Main() {
String s1 = "MyTest";
String s2 = new StringBuilder().Append("My").Append("Test").ToString();
String s3 = String.Intern(s2);
Console.WriteLine("s1 == '{0}'", s1);
Console.WriteLine("s2 == '{0}'", s2);
Console.WriteLine("s3 == '{0}'", s3);
Console.WriteLine("Is s2 the same reference as s1?: {0}", (Object)s2==(Object)s1);
Console.WriteLine("Is s3 the same reference as s1?: {0}", (Object)s3==(Object)s1);
}
}
/**//*
This example produces the following results:
s1 == 'MyTest'
s2 == 'MyTest'
s3 == 'MyTest'
Is s2 the same reference as s1?: False
Is s3 the same reference as s1?: True
*/
|
如果熟悉CLR的Metadata檔案結構的朋友可能立刻會想到,在Metadata表中實際上本來就有#String流和#US流,分別儲存程式中固化的字串和使用者字串。例如上面的"MyTest"字串就會被放入流中直接載入,而CLR動態維護的字串表就是在此基礎上擴充的。
動態建立的字串,如前面例子中通過StringBuilder構造的字串,則預設放在堆中,只有使用者顯式調用了String.Intern函數,才會被加入到靜態字串表中。查看Rotor的代碼,會發現String.Intern實際上是調用當前線程所在AppDomain的GetOrInternString函數;而進一步調用此AppDomain的字串映射表的GetInternedString函數。
以下為引用:
String.Intern(String str) (bclsystemstring.cs:1194)
Thread.GetDomain().GetOrInternString(str)
AppDomain.GetOrInternString(String str) (bclsystemappdomain.cs:1558)
InternalCall
BaseDomain::GetOrInternString(STRINGREF *pString) (vmappdomain.cpp:856)
m_pStringLiteralMap->GetInternedString(pString, )
AppDomainStringLiteralMap::GetInternedString() (vmstringliteralmap.cpp:196)
|
在GetOrInternString函數中:首先會根據字串的內容計算出其HashCode;然後使用此HashCode在當前AppDomain的字串映射表(m_StringToEntryHashTable)中搜尋;如果沒有找到則進一步在CLR的全域字串映射表(SystemDomain::GetGlobalStringLiteralMap())中搜尋;如果還是沒有找到,則根據參數決定是否將此字串以HashCode為索引加入全域字串映射表(GetInternedString函數中根據參數bAddIfNotFound判斷是否添加);如果當前AppDomain可能被卸載,則還會將此字串以HashCode為索引加入到當前AppDomain的局部字串映射表中。虛擬碼如下:
以下為引用:
STRINGREF *AppDomainStringLiteralMap::GetInternedString(STRINGREF *pString, BOOL bAddIfNotFound, BOOL bAppDomainWontUnload) { StringLiteralEntry *Data; DWORD dwHash = m_StringToEntryHashTable->GetHash(字串資料); if (m_StringToEntryHashTable->GetValue(&StringData, &Data, dwHash)) { return Data->GetStringObject(); } else { StringLiteralEntry *pEntry = SystemDomain::GetGlobalStringLiteralMap()->GetInternedString(pString, dwHash, bAddIfNotFound); if(pEntry) { if (!bAppDomainWontUnload) { m_StringToEntryHashTable->InsertValue(&StringData, (LPVOID)pEntry, FALSE); } } else { return pEntry->GetStringObject(); } } }
|
另外一個函數String.IsInterned實際上調用路徑完全一樣,只是在GetInternedString沒有在字串映射表搜尋到字串時不自動加入(bAddIfNotFound = false)。
由此我們可以得出一些結論:
1.Intern String的範圍是整個CLR,雖然每個AppDomain有獨立的優先緩衝機制。這樣既可以保障查詢效率,又可以保障在不同層級(如CLR/AppDomain)載入的共用的Assembly中字串的一致性。
2.Intern String中的內容直接決定其HashCode,進而決定其在字串表中的儲存和索引,直接內容修改可能導致未知問題。直接修改內容後再使用String.IsInterned,就會返回一個和以前完全不同的索引項目。
3.Intern String可以通過其引用直接比較。因為在隱式(固化在Metadata的#String或#US流中)或顯示(調用String.Intern)將字串Intern的時候,內容相同的字串都會被定位到字串索引表的同一入口,返回相同的對象引用。