Performance considerations for strings in C# [1]
C# 字串效能說想
Written By Dr Herbie [2]
Translated By Allen Lee
Reviewed By Teddy Tam & Allen Lee [3]
Introduction
你在代碼中處理字串的方法可能會對效能產生令人吃驚的影響。在本文中,我需要考慮兩個由於使用字串而產生的問題:臨時字串變數的使用和字串串連。
Background
每個項目都有需要你為其考慮編碼通訊協定的時候。使用 FxCop 是一個好的開始。我最喜愛的一組 FxCop 規則是“效能”那組。
於是,我就用 FxCop 來檢查我的項目並發現一系列的字串問題。我必須承認一件事:我經常遇到與 C# 的不可變(immutable)的字串有關的問題。當我看到 myString.ToUpper() 時,我經常都會忘記它並不是改變 myString 的內容而是返回一整個全新的字串(這是由於 C# 中字串是不可變的)。
我對代碼進行一番修正以便去掉 FxCop 的警告,接著我就發現代碼的確比之前快了。我決定開展調查,而最終我會寫出上面那些測試的代碼的。
Using the code
測試的代碼很簡單。一個控制台程式調用四個測試方法,其中每個方法執行一種字串處理常式 1000 次(整個執行時間已經足夠長以便看出其中的效能差別了)。
這四個測試方法被分成兩組,每組兩個。第一組比較兩個方法,它們用於非大小寫敏感(case-insensitive)的字串比較。
String Comparison and Temporary String Creation
第一個測試常式是一個蹩腳的非大小寫敏感的字串比較。用於比較的常式的代碼是:
static bool BadCompare(string stringA, string stringB)
{
return (stringA.ToUpper() == stringB.ToUpper());
}
對於這段代碼,FxCop 給出如下的建議:
"StringCompareTest.BadCompare(String, String):Boolean calls String.op_Equality(String, String):Boolean after converting 'stack1', a local, to upper or lowercase. If possible, eliminate the string creation and call the overload of String.Compare that performs a case-insensitive comparison."
這項建議的意思是每次對 ToUpper() 的調用都會創造一個臨時字串,而這個臨時字串是由垃圾收集器來建立和管理的。這需要額外的時間和使用更多的記憶體。 String.Compare 方法(相對來說)更加高效。
第二個測試常式使用 String.Compare :
static bool GoodCompare( string stringA, string stringB)
{
return (string.Compare(stringA, stringB, true , System.Globalization.CultureInfo.CurrentCulture) == 0);
}
這個方法防止多餘的臨時字串的建立。
根據 nprof 的分析結果 , GoodCompare 的執行時間只佔代碼總執行時間的 1.69%, 而 BadCompare 的執行時間則佔總執行時間的 5.50% 。
因此 String.Compare 方法比 ToUpper 方法快了三倍有餘。如果你的代碼您執行了很多字串的比較(尤其是在迴圈裡面執行),使用 String.Compare 能(使你的代碼在效能上)有較大的改善。
String Concatenation inside a loop
最後那對測試常式設想字串的串連是在一個迴圈裡面進行的。
“蹩腳”的測試常式的代碼如下:
static string BadConcatenate(string [] items)
{
string strRet = string .Empty;
foreach (string item in items)
{
strRet += item;
}
return strRet;
}
當 FxCop 看到這段代碼,它就會很憤怒,甚至用紅色標記這項被破的規條! FxCop 這樣說道:
"Change StringCompareTest.BadConcatenate(String[]):String to use StringBuilder instead of String.Concat or +="
“優良”的測試常式的代碼如下:
static string GoodConcatenate(string [] items)
{
System.Text.StringBuilder builder = new System.Text.StringBuilder();
foreach (string item in items)
{
builder.Append(item);
}
return builder.ToString();
}
這段代碼幾乎被用作展示 System.Text.StringBuilder 的用法的首選例子。蹩腳的代碼的問題是建立了過多的臨時字串。由於字串的不可變特性,串連操作符(+=)實際上用原來那兩個字串來建立一個新的字串,然後把原來的字串執行個體指向這個新的字串。
但是,依據 nprof 來研究代碼效能,我們發現運行 BadConcatenate 只需總執行時間的 5.67% ,而 GoodConcatenate 則是 22.09% 。也就是說:
使用 StringBuilder 耗費的時間幾乎是簡單的字串串連的四倍!
為什麼呢?
部分原因在於這個測試的設計——串連常式僅僅串連了十個簡短的字串。 StringBuilder 是一個比簡單的不可變的字串類更複雜的類,因此建立一個 StringBuilder 比起進行十個簡單的字串串連在效能上是昂貴很多的。
我重複地做不同數目的字串串連的測試,並且發現以下結果:
注意:這裡所顯示的數值是測試常式的執行時間佔總執行時間的百分比(%)。 GoodConcatenate 實際上並沒有快很多,但與 BadConcatenate 比卻相對地快了。
因此, StringBuilder 通常只有在你要串連的字串數目超過 600 時才會顯示出真正的效能優勢。
當然,另外一個使用 StringBuilder 的原因就是是記憶體的分配。使用 CLRProfiler 產生下面這個串連 100 個簡單字串時記憶體使用量情況的時序圖:
標記為“A”的地區顯示了 BadConcatenate 在記憶體配置和釋放上的效果。被分配記憶體的最大值迅速增加,並伴有大數量的垃圾收集的發生(該地區有大約 215 次垃圾收集)。
緊隨在“A”區後面的地區顯示了 GoodConcatenate 的記憶體輪廓。被分配記憶體的最大值增量較少,且伴隨著非常少的垃圾收集(該地區有大致 60 次垃圾收集)。
所以在某些情況下使用 StringBuilder 類並不會(使你的代碼運行得)更快 , 但它對垃圾收集器是友好的。
Conclusions
使用 String.Compare 方法進行非大小寫敏感的字串比較。這樣更快。而且代碼優雅和簡單。
僅當你在一個迴圈裡進行超過 600 次的字串串連時,使用 StringBuilder 來獲得更好的速度。這裡需要提醒的是,你所處理的字串的長度也會影響最終的速度,同樣會影響垃圾收集器的效果,所以你應該根據你實際的代碼具體問題具體分析。
Points of Interest
令我驚訝的是,在真實世界運用正確的代碼字串操作方法的還是很不同(雖然我們已在當前的項目中進行了很多字串的比較和串連)。
FxCop 的效能規則是發現潛在低效能代碼的好起點,並能指導你進行一些簡易修正來改善代碼效能。這裡所討論的兩個問題都被 FxCop 標記為“NON-BREAKING”,這是指改動不應破壞依賴於被改動代碼的代碼。認為為改善效能而做的改動都是“NON-BREAKING”則是沒頭腦的想法。
Further Considerations By Allen Lee
使用 StringBuilder 來處理字串的串連應該是絕大多數 .NET 開發人員的共識了。但你有否曾經懷疑過這一經驗原則的適用性是否真如想象中那麼廣泛呢?讀過本文後,或許你已經意識到這是個適度的問題。對小規模的字串串連使用 StringBuilder 所帶來的改善根本不足以抵償因 StringBuilder 本身的複雜性所產生的開銷;只有當串連規模達到臨界規模,兩者才能相互抵償從而達至平衡。
對於實際的代碼,一個可供使用的臨界規模值可能是必需的,尤其是在受限系統上進行開發。你可能因為對影響臨界規模的因素有所瞭解而懷疑作者在這裡所給出的數字。或許本文用於測試的設計顯得有點簡單以至於未必能使更多的人信服,但你的確透過本文瞭解到 StringBuilder 並不是任何情況都適用的。由於影響臨界規模的因素總有可能發生變化,你不可能找到一個對任何情況都適用的確定的臨界規模值。你應該為你的代碼量身訂造一個,並隨時做好調整的準備(因為變化總是存在的),只要你真的那麼在意這方面的效能影響。作為一個開始,你可以以作者在本文所提到的那個數字作為一個參照基礎,並就具體的情況進行微調,直到你滿意為止。
History
- 2005-06-03:完成本文翻譯的初稿。
- 2005-06-05:在 Teddy Tam 的協助下完成本文翻譯的三次審校。
- 2005-06-07:確定本文的最終譯稿並完成後期加工製作。
- [1] 著作權聲明:本譯文僅供學習研究之用,其中所有來自原文(點擊這裡查看原文)的內容(包括文字、資料和圖表等)的著作權歸原作者所有。未經許可不得以任何形式轉載其中的部分或全部。
- [2] 關於本文的作者 Dr Herbie :點擊這裡查看作者的其他文章以及作者的簡介。
- [3] 非常感謝 Teddy Tam,他在本文的整個翻譯過程中給了我很大的支援和協助,並就本文的翻譯提供了很多專業的意見!