“請對我的代碼指手划腳”是我們群內搞的一個不定期的常規性活動,以代碼審閱和細節重構為主線,大家可以自由發表自己的意見和建議,也算得上是一種思維風暴。感覺到這個活動很有意義,有必要總結並記錄下來。今天我發起了4短代碼,都有一定的代表性。今天我就其中的一個代碼片段的重構做一個簡單的總結和分享。
首先我們看看目標代碼:
1 public static string TestA(List<string> items) 2 { 3 var builder = new StringBuilder(); 4 5 foreach (var item in items) 6 { 7 if (builder.Length > 0) 8 { 9 builder.Append("|"); 10 builder.Append(item); 11 } 12 else builder.Append(item); 13 } 14 15 return builder.ToString(); 16 }
這里我使用C#來做示例,實際上,語言是相通的,我們將要談論的優化技巧在大多數編程環境中都是通用的。針對如上代碼,總計收集到了如下優化建議。
建議一:代碼重用性
我們可以看到,if...else...子句中有一段“builder.Append(item);”代碼是重復的,改變流程可以讓它們只出現一次,重構后結果如下:
1 foreach (var item in items) 2 { 3 if (builder.Length > 0) builder.Append("|"); // 去掉了大括號 4 5 builder.Append(item); 6 }
建議二:性能優化
我們知道StringBuilder類的構造函數中有一個capacity參數,這個參數意味着StringBuilder對象初始化時預分配的內存大小。如果能夠適當的設定一個值,那么對提升性能應該會很有幫助。因為這可以減少內存分配的次數,StringBuilder默認情況下是以2的N次方的形式不斷翻倍來調整內存需求的(對於我們來說,這個過程是自動的)。
建議三:還是性能優化
建議將foreach拆分為一次手工Append和一個for循環,如此可以避免在foreach內部的if判斷。在數據量大的時候可以大大的減少CPU的時鍾周期的占用,這個建議很不錯!重構后代碼如下:
1 public static string TestA(List<string> items) 2 { 3 // 這里是個capacity優化的假設值,實際運行中需要不斷的測試調優 4 var builder = new StringBuilder(100000); 5 6 builder.Append(items[0]); 7 8 for (var i = 1; i < items.Count; i++) 9 { 10 var item = items[i]; 11 12 builder.Append("|"); 13 builder.Append(item); 14 } 15 16 return builder.ToString(); 17 }
建議四:內存優化
其實是綜合了建議二和建議三,如下:
1 public static string TestA(List<string> items) 2 { 3 var length = items.Sum(t => t.Length); 4 5 length += (items.Count - 1); 6 7 if (length == 0) return String.Empty; 8 9 // 先計算出capacity的值 10 var builder = new StringBuilder(length); 11 12 builder.Append(items[0]); 13 14 for (var i = 1; i < items.Count; i++) 15 { 16 builder.Append("|"); 17 builder.Append(items[i]); // 消滅了之前的一個局部變量,減少內存分配 18 } 19 20 return builder.ToString(); 21 }
我的答復
其實,我出這個題目並不是為了考察性能優化、內存優化等問題,不過猴子們能想出各種招數來解題,我真的很欣慰!至少大家都在參與,都在動腦筋!這是好事!
通讀這篇文章之后,我相信您已經發現了題目原本的業務邏輯是想把一個string集合中的字符串使用“|”字符串聯起來,而且不能在結果字符串的兩邊出現“|”。因此,我期望能有童鞋想出如下的重構建議:
1 public static string TestB(List<string> items) 2 { 3 return String.Join("|", items); 4 }
您會不會感覺到我這么說很坑爹呢?
是的!代碼的細節重構不僅僅是各種優化和代碼的寫法、編碼體驗、編碼規范等,還有個重要的地方就是業務邏輯!編程是什么?編程是處理數據的手段和過程,同樣的結果可能會有很多途徑抵達,對我們來說,要從這些途徑中挑選出最簡單易用的,性能差異不要太大就可以了。
性能測試往往具有很強的隨機性,所以我們的測試必須要在不同的數量級下反復測試多遍,然后收集一個平均結果(最好是去掉最大值和最小值)來對比。至於性能差異的大小,在同一個數量級之內的,我們都認為“差異不大”或“沒有差異”,超出兩個數量級的時候一定要警惕!
本文附帶了我編寫的測試代碼,大家可以下載后運行對比一下。我隨機選擇了一個測試結果供大家參考:
測試數據准備完成,請按任意鍵繼續……
itemCount StringBuilder String.Join
1 1.851500 0.318600
10 0.027500 0.064400
100 0.225500 0.261600
1000 10.104700 2.324100
10000 19.039900 20.094800
100000 216.185100 251.624600
1000000 2364.580300 3401.948900
10000000 22862.921600 33593.679800
測試完畢!
可以看出,我們的建議四和String.Join方法的性能差異其實很小,可以忽略不計,通常我們誰會去處理一個幾百萬上千萬這么大的集合呢?
至於這兩種方法為什么差異不大,其實,我們只需要看看String.Join方法的實現就知道了,通過 .NET Reflector反編譯后我們發現它的實現也使用了類似於建議四的方案:
1 [SecuritySafeCritical] 2 public static unsafe string Join(string separator, string[] value, int startIndex, int count) 3 { 4 if (value == null) throw new ArgumentNullException("value"); 5 if (startIndex < 0) throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_StartIndex")); 6 if (count < 0) throw new ArgumentOutOfRangeException("count", Environment.GetResourceString("ArgumentOutOfRange_NegativeCount")); 7 if (startIndex > (value.Length - count)) throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_IndexCountBuffer")); 8 if (separator == null) separator = Empty; 9 if (count == 0) return Empty; 10 11 var length = 0; 12 var num2 = (startIndex + count) - 1; 13 14 for (var i = startIndex; i <= num2; i++) if (value[i] != null) length += value[i].Length; 15 16 length += (count - 1)*separator.Length; 17 18 if ((length < 0) || ((length + 1) < 0)) throw new OutOfMemoryException(); 19 if (length == 0) return Empty; 20 21 string str = FastAllocateString(length); 22 23 fixed (char* chRef = &str.m_firstChar) 24 { 25 var buffer = new UnSafeCharBuffer(chRef, length); 26 buffer.AppendString(value[startIndex]); 27 28 for (var j = startIndex + 1; j <= num2; j++) 29 { 30 buffer.AppendString(separator); 31 buffer.AppendString(value[j]); 32 } 33 } 34 35 return str; 36 }
代碼就不再解讀了,大家可以慢慢體會。
本文想告訴大家的是:代碼細節重構不要只停留在語言表面上,深入業務邏輯有時候會得到意想不到的結果!