Introduction
你在代碼中處理字符串的方法可能會對性能產(chǎn)生令人吃驚的影響。在本文中,我需要考慮兩個由于使用字符串而產(chǎn)生的問題:臨時字符串變量的使用和字符串連接。
Background
每個項目都有需要你為其考慮編碼標準的時候。使用 FxCop 是一個好的開始。我最喜愛的一組 FxCop 規(guī)則是“性能”那組。
于是,我就用 FxCop 來檢查我的項目并發(fā)現(xiàn)一系列的字符串問題。我必須承認一件事:我經(jīng)常遇到與 C# 的不可變(immutable)的字符串有關(guān)的問題。當我看到 myString.ToUpper() 時,我經(jīng)常都會忘記它并不是改變 myString 的內(nèi)容而是返回一整個全新的字符串(這是由于 C# 中字符串是不可變的)。
我對代碼進行一番修正以便去掉 FxCop 的警告,接著我就發(fā)現(xiàn)代碼的確比之前快了。我決定開展調(diào)查,而最終我會寫出上面那些測試的代碼的。
Using the code
測試的代碼很簡單。一個控制臺程序調(diào)用四個測試方法,其中每個方法執(zhí)行一種字符串處理例程 1000 次(整個執(zhí)行時間已經(jīng)足夠長以便看出其中的性能差別了)。
這四個測試方法被分成兩組,每組兩個。第一組比較兩個方法,它們用于非大小寫敏感(case-insensitive)的字符串比較。
String Comparison and Temporary String Creation
第一個測試例程是一個蹩腳的非大小寫敏感的字符串比較。用于比較的例程的代碼是:
{
return (stringA.ToUpper()==stringB.ToUpper());
}
對于這段代碼,F(xiàn)xCop 給出如下的建議:
"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() 的調(diào)用都會創(chuàng)造一個臨時字符串,而這個臨時字符串是由垃圾收集器來創(chuàng)建和管理的。這需要額外的時間和使用更多的內(nèi)存。 String.Compare 方法(相對來說)更加高效。
第二個測試例程使用 String.Compare :
{
return ( string .Compare(stringA,stringB, true ,System.Globalization.CultureInfo.CurrentCulture)==0);
}
這個方法防止多余的臨時字符串的創(chuàng)建。
根據(jù) nprof 的分析結(jié)果 , GoodCompare 的執(zhí)行時間只占代碼總執(zhí)行時間的 1.69%, 而 BadCompare 的執(zhí)行時間則占總執(zhí)行時間的 5.50% 。
因此 String.Compare 方法比 ToUpper 方法快了三倍有余。如果你的代碼您執(zhí)行了很多字符串的比較(尤其是在循環(huán)里面執(zhí)行),使用 String.Compare 能(使你的代碼在性能上)有較大的改善。
String Concatenation inside a loop
最后那對測試例程設(shè)想字符串的連接是在一個循環(huán)里面進行的。
“蹩腳”的測試例程的代碼如下:
{
string strRet= string .Empty;
foreach ( string item in items)
{
strRet+=item;
}
return strRet;
}
當 FxCop 看到這段代碼,它就會很憤怒,甚至用紅色標記這項被破的規(guī)條! FxCop 這樣說道:
"Change StringCompareTest.BadConcatenate(String[]):String to use StringBuilder instead of String.Concat or +="
“優(yōu)良”的測試例程的代碼如下:
{
System.Text.StringBuilderbuilder= new System.Text.StringBuilder();
foreach ( string item in items)
{
builder.Append(item);
}
return builder.ToString();
}
這段代碼幾乎被用作展示 System.Text.StringBuilder 的用法的首選例子。蹩腳的代碼的問題是創(chuàng)建了過多的臨時字符串。由于字符串的不可變特性,連接操作符(+=)實際上用原來那兩個字符串來創(chuàng)建一個新的字符串,然后把原來的字符串實例指向這個新的字符串。
但是,依據(jù) nprof 來研究代碼性能,我們發(fā)現(xiàn)運行 BadConcatenate 只需總執(zhí)行時間的 5.67% ,而 GoodConcatenate 則是 22.09% 。也就是說:
使用 StringBuilder 耗費的時間幾乎是簡單的字符串連接的四倍!
為什么呢?
部分原因在于這個測試的設(shè)計——連接例程僅僅連接了十個簡短的字符串。 StringBuilder 是一個比簡單的不可變的字符串類更復雜的類,因此創(chuàng)建一個 StringBuilder 比起進行十個簡單的字符串連接在性能上是昂貴很多的。
我重復地做不同數(shù)目的字符串連接的測試,并且發(fā)現(xiàn)以下結(jié)果:
注意:這里所顯示的數(shù)值是測試例程的執(zhí)行時間占總執(zhí)行時間的百分比(%)。 GoodConcatenate 實際上并沒有快很多,但與 BadConcatenate 比卻相對地快了。
因此, StringBuilder 通常只有在你要連接的字符串數(shù)目超過 600 時才會顯示出真正的性能優(yōu)勢。
當然,另外一個使用 StringBuilder 的原因就是是內(nèi)存的分配。使用 CLRProfiler 生成下面這個連接 100 個簡單字符串時內(nèi)存使用情況的時序圖:
標記為“A”的區(qū)域顯示了 BadConcatenate 在內(nèi)存分配和釋放上的效果。被分配內(nèi)存的最大值迅速增加,并伴有大數(shù)量的垃圾收集的發(fā)生(該區(qū)域有大約 215 次垃圾收集)。
緊隨在“A”區(qū)后面的區(qū)域顯示了 GoodConcatenate 的內(nèi)存輪廓。被分配內(nèi)存的最大值增量較少,且伴隨著非常少的垃圾收集(該區(qū)域有大致 60 次垃圾收集)。
所以在某些情況下使用 StringBuilder 類并不會(使你的代碼運行得)更快 , 但它對垃圾收集器是友好的。
Conclusions
使用 String.Compare 方法進行非大小寫敏感的字符串比較。這樣更快。而且代碼優(yōu)雅和簡單。
僅當你在一個循環(huán)里進行超過 600 次的字符串連接時,使用 StringBuilder 來獲得更好的速度。這里需要提醒的是,你所處理的字符串的長度也會影響最終的速度,同樣會影響垃圾收集器的效果,所以你應(yīng)該根據(jù)你實際的代碼具體問題具體分析。
Points of Interest
令我驚訝的是,在真實世界運用正確的代碼字符串操作方法的還是很不同(雖然我們已在當前的項目中進行了很多字符串的比較和連接)。
FxCop 的性能規(guī)則是發(fā)現(xiàn)潛在低性能代碼的好起點,并能指導你進行一些簡易修正來改善代碼性能。這里所討論的兩個問題都被 FxCop 標記為“NON-BREAKING”,這是指改動不應(yīng)破壞依賴于被改動代碼的代碼。認為為改善性能而做的改動都是“NON-BREAKING”則是沒頭腦的想法。
Further Considerations By Allen Lee
使用 StringBuilder 來處理字符串的連接應(yīng)該是絕大多數(shù) .NET 開發(fā)人員的共識了。但你有否曾經(jīng)懷疑過這一經(jīng)驗原則的適用性是否真如想象中那么廣泛呢?讀過本文后,或許你已經(jīng)意識到這是個適度的問題。對小規(guī)模的字符串連接使用 StringBuilder 所帶來的改善根本不足以抵償因 StringBuilder 本身的復雜性所產(chǎn)生的開銷;只有當連接規(guī)模達到臨界規(guī)模,兩者才能相互抵償從而達至平衡。
對于實際的代碼,一個可供使用的臨界規(guī)模值可能是必需的,尤其是在受限系統(tǒng)上進行開發(fā)。你可能因為對影響臨界規(guī)模的因素有所了解而懷疑作者在這里所給出的數(shù)字?;蛟S本文用于測試的設(shè)計顯得有點簡單以至于未必能使更多的人信服,但你的確透過本文了解到 StringBuilder 并不是任何情況都適用的。由于影響臨界規(guī)模的因素總有可能發(fā)生變化,你不可能找到一個對任何情況都適用的確定的臨界規(guī)模值。你應(yīng)該為你的代碼量身訂造一個,并隨時做好調(diào)整的準備(因為變化總是存在的),只要你真的那么在意這方面的性能影響。作為一個開始,你可以以作者在本文所提到的那個數(shù)字作為一個參照基礎(chǔ),并就具體的情況進行微調(diào),直到你滿意為止。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
