.NET程序性能的基本要領

摘要:本文分享了性能優化的一些建議和思考,比如不要過早優化、好工具很重要、性能的關鍵,在于內存分配等。開發者不要盲目的沒有根據的優化,首先定位和查找到造成產生性能問題的原因點最重要。

【編者按】Bill Chiles(Roslyn編譯器的程序經理)寫了一篇文章《Essential Performance Facts and .NET Framework Tips》,知名博主寒江獨釣對該文進行了摘譯,文中分享了性能優化的一些建議和思考,比如不要過早優化、好工具很重要、性能的關鍵,在于內存分配等,并指出開發者不要盲目的沒有根據的優化,首先定位和查找到造成產生性能問題的原因點最重要。


全文如下:

本文提供了一些性能優化的建議,這些經驗來自于使用托管代碼重寫C# 和 VB編譯器,并以編寫C# 編譯器中的一些真實場景作為例子來展示這些優化經驗。.NET 平臺開發應用程序具有極高的生產力。.NET 平臺上強大安全的編程語言以及豐富的類庫,使得開發應用變得卓有成效。但是能力越大責任越大。我們應該使用.NET框架的強大能力,但同時如果我們需要處理大量的數據比如文件或者數據庫也需要準備對我們的代碼進行調優。

為什么來自新的編譯器的性能優化經驗也適用于您的應用程序

微軟使用托管代碼重寫了C#和Visual Basic的編譯器,并提供了一些列新的API來進行代碼建模和分析、開發編譯工具,使得Visual Studio具有更加豐富的代碼感知的編程體驗。重寫編譯器,并且在新的編譯器上開發Visual Studio的經驗使得我們獲得了非常有用的性能優化經驗,這些經驗也能用于大型的.NET應用,或者一些需要處理大量數據的APP上。你不需要了解編譯器,也能夠從C#編譯器的例子中得出這些見解。

Visual Studio使用了編譯器的API來實現了強大的智能感知(Intellisense)功能,如代碼關鍵字著色,語法填充列表,錯誤波浪線提示,參數提示,代碼問題及修改建議等,這些功能深受開發者歡迎。Visual Studio在開發者輸入或者修改代碼的時候,會動態的編譯代碼來獲得對代碼的分析和提示。

當用戶和App進行交互的時候,通常希望軟件具有好的響應性。輸入或者執行命令的時候,應用程序界面不應該被阻塞。幫助或者提示能夠迅速顯示出來或者當用戶繼續輸入的時候停止提示。現在的App應該避免在執行長時間計算的時候阻塞UI線程從而讓用戶感覺程序不夠流暢。

想了解更多關于新的編譯器的信息,可以訪問 .NET Compiler Platform ("Roslyn")

基本要領

在對.NET 進行性能調優以及開發具有良好響應性的應用程序的時候,請考慮以下這些基本要領:

要領一:不要過早優化

編寫代碼比想象中的要復雜的多,代碼需要維護,調試及優化性能。 一個有經驗的程序員,通常會對自然而然的提出解決問題的方法并編寫高效的代碼。 但是有時候也可能會陷入過早優化代碼的問題中。比如,有時候使用一個簡單的數組就夠了,非要優化成使用哈希表,有時候簡單的重新計算一下可以,非要使用復雜的可能導致內存泄漏的緩存。發現問題時,應該首先測試性能問題然后再分析代碼。

要領二:沒有評測,便是猜測

剖析和測量不會撒謊。測評可以顯示CPU是否滿負荷運轉或者是存在磁盤I/O阻塞。測評會告訴你應用程序分配了什么樣的以及多大的內存,以及是否CPU花費了很多時間在 垃圾回收上。

應該為關鍵的用戶體驗或者場景設置性能目標,并且編寫測試來測量性能。通過使用科學的方法來分析性能不達標的原因的步驟如下:使用測評報告來指導,假設可能出現的情況,并且編寫實驗代碼或者修改代碼來驗證我們的假設或者修正。如果我們設置了基本的性能指標并且經常測試,就能夠避免一些改變導致性能的回退(regression),這樣就能夠避免我們浪費時間在一些不必要的改動中。

要領三:好工具很重要

好的工具能夠讓我們能夠快速的定位到影響性能的最大因素(CPU,內存,磁盤)并且能夠幫助我們定位產生這些瓶頸的代碼。微軟已經發布了很多性能測試工具比如: Visual Studio Profiler, Windows Phone Analysis Tool, 以及 PerfView.

PerfView是一款免費且性能強大的工具,他主要關注影響性能的一些深層次的問題(磁盤 I/O,GC 事件,內存),后面會展示這方面的例子。我們能夠抓取性能相關的 Event Tracing for Windows(ETW)事件并能以應用程序,進程,堆棧,線程的尺度查看這些信息。PerfView能夠展示應用程序分配了多少,以及分配了何種內存以及應用程序中的函數以及調用堆棧對內存分配的貢獻。這些方面的細節,您可以查看隨工具下載發布的關于PerfView的非常詳細的幫助,Demo以及視頻教程(比如 Channel9上的視頻教程)

要領四:所有的都與內存分配相關

你可能會想,編寫響應及時的基于.NET的應用程序關鍵在于采用好的算法,比如使用快速排序替代冒泡排序,但是實際情況并不是這樣。編寫一個響應良好的app的最大因素在于內存分配,特別是當app非常大或者處理大量數據的時候。

在使用新的編譯器API開發響應良好的IDE的實踐中,大部分工作都花在了如何避免開辟內存以及管理緩存策略。PerfView追蹤顯示新的C# 和VB編譯器的性能基本上和CPU的性能瓶頸沒有關系。編譯器在讀入成百上千甚至上萬行代碼,讀入元數據活著產生編譯好的代碼,這些操作其實都是I/O bound 密集型。UI線程的延遲幾乎全部都是由于垃圾回收導致的。.NET框架對垃圾回收的性能已經進行過高度優化,他能夠在應用程序代碼執行的時候并行的執行垃圾回收的大部分操作。但是,單個內存分配操作有可能會觸發一次昂貴的垃圾回收操作,這樣GC會暫時掛起所有線程來進行垃圾回收(比如 Generation 2型的垃圾回收)

常見的內存分配以及例子

這部分的例子雖然背后關于內存分配的地方很少。但是,如果一個大的應用程序執行足夠多的這些小的會導致內存分配的表達式,那么這些表達式會導致幾百M,甚至幾G的內存分配。比如,在性能測試團隊把問題定位到輸入場景之前,一分鐘的測試模擬開發者在編譯器里面編寫代碼會分配幾G的內存。

裝箱

裝箱發生在當通常分配在線程棧上或者數據結構中的值類型,或者臨時的值需要被包裝到對象中的時候(比如分配一個對象來存放數據,活著返回一個指針給一個Object對象)。.NET框架由于方法的簽名或者類型的分配位置,有些時候會自動對值類型進行裝箱。將值類型包裝為引用類型會產生內存分配。.NET框架及語言會盡量避免不必要的裝箱,但是有時候在我們沒有注意到的時候會產生裝箱操作。過多的裝箱操作會在應用程序中分配成M上G的內存,這就意味著垃圾回收的更加頻繁,也會花更長時間。

在PerfView中查看裝箱操作,只需要開啟一個追蹤(trace),然后查看應用程序名字下面的GC Heap Alloc 項(記住,PerfView會報告所有的進程的資源分配情況),如果在分配相中看到了一些諸如System.Int32和System.Char的值類型,那么就發生了裝箱。選擇一個類型,就會顯示調用棧以及發生裝箱的操作的函數。

例1 string方法和其值類型參數

下面的示例代碼演示了潛在的不必要的裝箱以及在大的系統中的頻繁的裝箱操作。

[js] view plaincopy
  1. public?class?Logger??
  2. {??
  3. ????public?static?void?WriteLine(string?s)??
  4. ????{??
  5. ????????/*...*/??
  6. ????}??
  7. }??
  8. public?class?BoxingExample??
  9. {??
  10. ????public?void?Log(int?id,?int?size)??
  11. ????{??
  12. ????????var?s?=?string.Format("{0}:{1}",?id,?size);??
  13. ????????Logger.WriteLine(s);??
  14. ????}??
  15. }??

這是一個日志基礎類,因此app會很頻繁的調用Log函數來記日志,可能該方法會被調用millons次。問題在于,調用string.Format方法會調用其 重載的接受一個string類型和兩個Object類型的方法:

[js] view plaincopy
  1. String.Format?Method?(String,?Object,?Object)??

該重載方法要求.NET Framework 把int型裝箱為object類型然后將它傳到方法調用中去。為了解決這一問題,方法就是調用id.ToString()size.ToString()方法,然后傳入到string.Format 方法中去,調用ToString()方法的確會導致一個string的分配,但是在string.Format方法內部不論怎樣都會產生string類型的分配。

你可能會認為這個基本的調用string.Format 僅僅是字符串的拼接,所以你可能會寫出這樣的代碼:

[js] view plaincopy
  1. var?s?=?id.ToString()?+?':'?+?size.ToString();??

實際上,上面這行代碼也會導致裝箱,因為上面的語句在編譯的時候會調用:

[js] view plaincopy
  1. string.Concat(Object,?Object,?Object);??

這個方法,.NET Framework 必須對字符常量進行裝箱來調用Concat方法。

解決方法:

完全修復這個問題很簡單,將上面的單引號替換為雙引號即將字符常量換為字符串常量就可以避免裝箱,因為string類型的已經是引用類型了。

[js] view plaincopy
  1. var?s?=?id.ToString()?+?":"?+?size.ToString();??

例2 枚舉類型的裝箱

下面的這個例子是導致新的C# 和VB編譯器由于頻繁的使用枚舉類型,特別是在Dictionary中做查找操作時分配了大量內存的原因。

[js] view plaincopy
  1. public?enum?Color?{?Red,?Green,?Blue?}??
  2. public?class?BoxingExample??
  3. {??
  4. ????private?string?name;??
  5. ????private?Color?color;??
  6. ????public?override?int?GetHashCode()??
  7. ????{??
  8. ????????return?name.GetHashCode()?^?color.GetHashCode();??
  9. ????}??
  10. }??

問題非常隱蔽,PerfView會告訴你enmu.GetHashCode()由于內部實現的原因產生了裝箱操作,該方法會在底層枚舉類型的表現形式上進行裝箱,如果仔細看PerfView,會看到每次調用GetHashCode會產生兩次裝箱操作。編譯器插入一次,.NET Framework插入另外一次。

解決方法:

通過在調用GetHashCode的時候將枚舉的底層表現形式進行強制類型轉換就可以避免這一裝箱操作。

[js] view plaincopy
  1. ((int)color).GetHashCode()??

另一個使用枚舉類型經常產生裝箱的操作時enum.HasFlag。傳給HasFlag的參數必須進行裝箱,在大多數情況下,反復調用HasFlag通過位運算測試非常簡單和不需要分配內存。

要牢記基本要領第一條,不要過早優化。并且不要過早的開始重寫所有代碼。 需要注意到這些裝箱的耗費,只有在通過工具找到并且定位到最主要問題所在再開始修改代碼。

字符串

字符串操作是引起內存分配的最大元兇之一,通常在PerfView中占到前五導致內存分配的原因。應用程序使用字符串來進行序列化,表示JSON和REST。在不支持枚舉類型的情況下,字符串可以用來與其他系統進行交互。當我們定位到是由于string操作導致對性能產生嚴重影響的時候,需要留意string類的Format(),Concat(),Split(),Join(),Substring()等這些方法。使用StringBuilder能夠避免在拼接多個字符串時創建多個新字符串的開銷,但是StringBuilder的創建也需要進行良好的控制以避免可能會產生的性能瓶頸。

例3 字符串操作

在C#編譯器中有如下方法來輸出方法前面的xml格式的注釋。

[js] view plaincopy
  1. public?void?WriteFormattedDocComment(string?text)??
  2. {??
  3. ????string[]?lines?=?text.Split(new[]?{"\r\n",?"\r",?"\n"},??
  4. ????????StringSplitOptions.None);??
  5. ????int?numLines?=?lines.Length;??
  6. ????bool?skipSpace?=?true;??
  7. ????if?(lines[0].TrimStart().StartsWith("///"))??
  8. ????{??
  9. ????????for?(int?i?=?0;?i?<?numLines;?i++)??
  10. ????????{??
  11. ????????????string?trimmed?=?lines[i].TrimStart();??
  12. ????????????if?(trimmed.Length?<?4?||?!char.IsWhiteSpace(trimmed[3]))??
  13. ????????????{??
  14. ????????????????skipSpace?=?false;??
  15. ????????????????break;??
  16. ????????????}??
  17. ????????}??
  18. ????????int?substringStart?=?skipSpace???4?:?3;??
  19. ????????for?(int?i?=?0;?i?<?numLines;?i++)??
  20. ????????????Console.WriteLine(lines[i].TrimStart().Substring(substringStart));??
  21. ????}??
  22. ????else??
  23. ????{??
  24. ????????/*?...?*/??
  25. ????}??
  26. }??

可以看到,在這片代碼中包含有很多字符串操作。代碼中使用類庫方法來將行分割為字符串,來去除空格,來檢查參數text是否是XML文檔格式的注釋,然后從行中取出字符串處理。

WriteFormattedDocComment方法每次被調用時,第一行代碼調用Split()就會分配三個元素的字符串數組。編譯器也需要產生代碼來分配這個數組。因為編譯器并不知道,如果Splite()存儲了這一數組,那么其他部分的代碼有可能會改變這個數組,這樣就會影響到后面對WriteFormattedDocComment方法的調用。每次調用Splite()方法也會為參數text分配一個string,然后在分配其他內存來執行splite操作。

WriteFormattedDocComment方法中調用了三次TrimStart()方法,在內存環中調用了兩次,這些都是重復的工作和內存分配。更糟糕的是,TrimStart()的無參重載方法的簽名如下:

[js] view plaincopy
  1. namespace?System??
  2. {???
  3. ????public?class?String???
  4. ????{???
  5. ????????public?string?TrimStart(params?char[]?trimChars);??
  6. ????}??
  7. }??

該方法簽名意味著,每次對TrimStart()的調用都回分配一個空的數組以及返回一個string類型的結果。

最后,調用了一次Substring()方法,這個方法通常會導致在內存中分配新的字符串。

解決方法:

和前面的只需要小小的修改即可解決內存分配的問題不同。在這個例子中,我們需要從頭看,查看問題然后采用不同的方法解決。比如,可以意識到WriteFormattedDocComment()方法的參數是一個字符串,它包含了方法中需要的所有信息,因此,代碼只需要做更多的index操作,而不是分配那么多小的string片段。

下面的方法并沒有完全解,但是可以看到如何使用類似的技巧來解決本例中存在的問題。C#編譯器使用如下的方式來消除所有的額外內存分配。

[js] view plaincopy
  1. private?int?IndexOfFirstNonWhiteSpaceChar(string?text,?int?start)??
  2. {??
  3. ????while?(start?<?text.Length?&&?char.IsWhiteSpace(text[start]))???
  4. ????????start++;??
  5. ????return?start;??
  6. }??
  7. ??
  8. private?bool?TrimmedStringStartsWith(string?text,?int?start,?string?prefix)??
  9. {??
  10. ????start?=?IndexOfFirstNonWhiteSpaceChar(text,?start);???
  11. ????int?len?=?text.Length?-?start;???
  12. ????if?(len?<?prefix.Length)?return?false;??
  13. ????for?(int?i?=?0;?i?<?len;?i++)??
  14. ????{??
  15. ????????if?(prefix[i]?!=?text[start?+?i])???
  16. ????????????return?false;??
  17. ????}??
  18. ????return?true;??
  19. }??

WriteFormattedDocComment() 方法的第一個版本分配了一個數組,幾個子字符串,一個trim后的子字符串,以及一個空的params數組。也檢查了”///”。修改后的代碼僅使用了index操作,沒有任何額外的內存分配。它查找第一個非空格的字符串,然后逐個字符串比較來查看是否以”///”開頭。和使用TrimStart()不同,修改后的代碼使用IndexOfFirstNonWhiteSpaceChar方法來返回第一個非空格的開始位置,通過使用這種方法,可以移除WriteFormattedDocComment()方法中的所有額外內存分配。

例4 StringBuilder

本例中使用StringBuilder。下面的函數用來產生泛型類型的全名:

[js] view plaincopy
  1. public?class?Example???
  2. {???
  3. ????//?Constructs?a?name?like?"SomeType<T1,?T2,?T3>"???
  4. ????public?string?GenerateFullTypeName(string?name,?int?arity)???
  5. ????{???
  6. ????????StringBuilder?sb?=?new?StringBuilder();??
  7. ????????sb.Append(name);??
  8. ????????if?(arity?!=?0)??
  9. ????????{???
  10. ????????????sb.Append("<");??
  11. ????????????for?(int?i?=?1;?i?<?arity;?i++)??
  12. ????????????{??
  13. ????????????????sb.Append("T");?sb.Append(i.ToString());?sb.Append(",?");??
  14. ????????????}???
  15. ????????????sb.Append("T");?sb.Append(i.ToString());?sb.Append(">");??
  16. ????????}??
  17. ????????return?sb.ToString();???
  18. ????}??
  19. }??

注意力集中到StringBuilder實例的創建上來。代碼中調用sb.ToString()會導致一次內存分配。在StringBuilder中的內部實現也會導致內部內存分配,但是我們如果想要獲取到string類型的結果化,這些分配無法避免。

解決方法:

要解決StringBuilder對象的分配就使用緩存。即使緩存一個可能被隨時丟棄的單個實例對象也能夠顯著的提高程序性能。下面是該函數的新的實現。除了下面兩行代碼,其他代碼均相同

[js] view plaincopy
  1. //?Constructs?a?name?like?"Foo<T1,?T2,?T3>"???
  2. public?string?GenerateFullTypeName(string?name,?int?arity)??
  3. {??
  4. ????StringBuilder?sb?=?AcquireBuilder();?/*?Use?sb?as?before?*/???
  5. ????return?GetStringAndReleaseBuilder(sb);??
  6. }??

關鍵部分在于新的 AcquireBuilder()GetStringAndReleaseBuilder()方法:

[js] view plaincopy
  1. [ThreadStatic]??
  2. private?static?StringBuilder?cachedStringBuilder;??
  3. ??
  4. private?static?StringBuilder?AcquireBuilder()??
  5. {??
  6. ????StringBuilder?result?=?cachedStringBuilder;??
  7. ????if?(result?==?null)??
  8. ????{??
  9. ????????return?new?StringBuilder();??
  10. ????}???
  11. ????result.Clear();???
  12. ????cachedStringBuilder?=?null;???
  13. ????return?result;??
  14. }??
  15. ??
  16. private?static?string?GetStringAndReleaseBuilder(StringBuilder?sb)??
  17. {??
  18. ????string?result?=?sb.ToString();???
  19. ????cachedStringBuilder?=?sb;???
  20. ????return?result;??
  21. }??

上面方法實現中使用了 thread-static字段來緩存StringBuilder對象,這是由于新的編譯器使用了多線程的原因。很可能會忘掉這個ThreadStatic聲明。Thread-static字符為每個執行這部分的代碼的線程保留一個唯一的實例。

如果已經有了一個實例,那么AcquireBuilder()方法直接返回該緩存的實例,在清空后,將該字段或者緩存設置為null。否則AcquireBuilder()創建一個新的實例并返回,然后將字段和cache設置為null 。

當我們對StringBuilder處理完成之后,調用GetStringAndReleaseBuilder()方法即可獲取string結果。然后將StringBuilder保存到字段中或者緩存起來,然后返回結果。這段代碼很可能重復執行,從而創建多個StringBuilder對象,雖然很少會發生。代碼中僅保存最后被釋放的那個StringBuilder對象來留作后用。新的編譯器中,這種簡單的的緩存策略極大地減少了不必要的內存分配。.NET Framework 和 MSBuild中的部分模塊也使用了類似的技術來提升性能。

簡單的緩存策略必須遵循良好的緩存設計,因為他有大小的限制cap。使用緩存可能比之前有更多的代碼,也需要更多的維護工作。我們只有在發現這是個問題之后才應該采緩存策略。PerfView已經顯示出StringBuilder對內存的分配貢獻相當大。


LINQ和Lambdas表達式

使用LINQ 和Lambdas表達式是C#語言強大生產力的一個很好體現,但是如果代碼需要執行很多次的時候,可能需要對LINQ或者Lambdas表達式進行重寫。

例5 Lambdas表達式,List<T>,以及IEnumerable<T>

下面的例子使用 LINQ以及函數式風格的代碼來通過編譯器模型給定的名稱來查找符號。

[js] view plaincopy
  1. class?Symbol???
  2. {???
  3. ????public?string?Name?{?get;?private?set;?}?/*...*/??
  4. }??
  5. class?Compiler???
  6. {???
  7. ????private?List<Symbol>?symbols;???
  8. ????public?Symbol?FindMatchingSymbol(string?name)???
  9. ????{???
  10. ????????return?symbols.FirstOrDefault(s?=>?s.Name?==?name);???
  11. ????}??
  12. }??

新的編譯器和IDE 體驗基于調用FindMatchingSymbol,這個調用非常頻繁,在此過程中,這么簡單的一行代碼隱藏了基礎內存分配開銷。為了展示這其中的分配,我們首先將該單行函數拆分為兩行:

[js] view plaincopy
  1. Func<Symbol,?bool>?predicate?=?s?=>?s.Name?==?name;???
  2. return?symbols.FirstOrDefault(predicate);??

第一行中, lambda表達式“s=>s.Name==name” 是對本地變量name的一個 閉包。這就意味著需要分配額外的對象來為 委托對象predict分配空間,需要一個分配一個靜態類來保存環境從而保存name的值。編譯器會產生如下代碼:

[js] view plaincopy
  1. //?Compiler-generated?class?to?hold?environment?state?for?lambda???
  2. private?class?Lambda1Environment???
  3. {???
  4. ????public?string?capturedName;???
  5. ????public?bool?Evaluate(Symbol?s)???
  6. ????{???
  7. ????????return?s.Name?==?this.capturedName;??
  8. ????}???
  9. }??
  10. ??
  11. //?Expanded?Func<Symbol,?bool>?predicate?=?s?=>?s.Name?==?name;???
  12. Lambda1Environment?l?=?new?Lambda1Environment()???
  13. {???
  14. ????capturedName?=?name??
  15. };???
  16. var?predicate?=?new?Func<Symbol,?bool>(l.Evaluate);??

兩個new操作符(第一個創建一個環境類,第二個用來創建委托)很明顯的表明了內存分配的情況。

現在來看看FirstOrDefault方法的調用,他是IEnumerable<T>類的擴展方法,這也會產生一次內存分配。因為FirstOrDefault使用IEnumerable<T>作為第一個參數,可以將上面的展開為下面的代碼:

[js] view plaincopy
  1. //?Expanded?return?symbols.FirstOrDefault(predicate)?...???
  2. IEnumerable<Symbol>?enumerable?=?symbols;??
  3. IEnumerator<Symbol>?enumerator?=?enumerable.GetEnumerator();???
  4. while?(enumerator.MoveNext())??
  5. {???
  6. ????if?(predicate(enumerator.Current))???
  7. ????????return?enumerator.Current;???
  8. }???
  9. return?default(Symbol);??

symbols變量是類型為List<T>的變量。List<T>集合類型實現了IEnumerable<T>即可并且清晰地定義了一個 迭代器,List<T>的迭代器使用了一種結構體來實現。使用結構而不是類意味著通常可以避免任何在托管堆上的分配,從而可以影響垃圾回收的效率。枚舉典型的用處在于方便語言層面上使用foreach循環,他使用enumerator結構體在調用推棧上返回。遞增調用堆棧指針來為對象分配空間,不會影響GC對托管對象的操作。

在上面的展開FirstOrDefault調用的例子中,代碼會調用IEnumerabole<T>接口中的GetEnumerator()方法。將symbols賦值給IEnumerable<Symbol>類型的enumerable 變量,會使得對象丟失了其實際的List<T>類型信息。這就意味著當代碼通過enumerable.GetEnumerator()方法獲取迭代器時,.NET Framework 必須對返回的值(即迭代器,使用結構體實現)類型進行裝箱從而將其賦給IEnumerable<Symbol>類型的(引用類型) enumerator變量。

解決方法:

解決辦法是重寫FindMatchingSymbol方法,將單個語句使用六行代碼替代,這些代碼依舊連貫,易于閱讀和理解,也很容易實現。

[js] view plaincopy
  1. public?Symbol?FindMatchingSymbol(string?name)???
  2. {???
  3. ????foreach?(Symbol?s?in?symbols)??
  4. ????{???
  5. ????????if?(s.Name?==?name)???
  6. ????????????return?s;???
  7. ????}???
  8. ????return?null;???
  9. }??

代碼中并沒有使用LINQ擴展方法,lambdas表達式和迭代器,并且沒有額外的內存分配開銷。這是因為編譯器看到symbolList<T>類型的集合,因為能夠直接將返回的結構性的枚舉器綁定到類型正確的本地變量上,從而避免了對struct類型的裝箱操作。原先的代碼展示了C#語言豐富的表現形式以及.NET Framework 強大的生產力。該著后的代碼則更加高效簡單,并沒有添加復雜的代碼而增加可維護性。

Aync異步

接下來的例子展示了當我們試圖緩存一部方法返回值時的一個普遍問題:

例6 緩存異步方法

Visual Studio IDE 的特性在很大程度上建立在新的C#和VB編譯器獲取語法樹的基礎上,當編譯器使用async的時候仍能夠保持Visual Stuido能夠響應。下面是獲取語法樹的第一個版本的代碼:

[js] view plaincopy
  1. class?Parser???
  2. {??
  3. ????/*...*/???
  4. ????public?SyntaxTree?Syntax??
  5. ????{???
  6. ????????get;???
  7. ????}???
  8. ??????
  9. ????public?Task?ParseSourceCode()???
  10. ????{??
  11. ????????/*...*/???
  12. ????}???
  13. }??
  14. class?Compilation???
  15. {???
  16. ????/*...*/???
  17. ????public?async?Task<SyntaxTree>?GetSyntaxTreeAsync()???
  18. ????{???
  19. ????????var?parser?=?new?Parser();?//?allocation???
  20. ????????await?parser.ParseSourceCode();?//?expensive???
  21. ????????return?parser.Syntax;??
  22. ????}???
  23. }??

可以看到調用GetSyntaxTreeAsync() 方法會實例化一個Parser對象,解析代碼,然后返回一個Task<SyntaxTree>對象。最耗性能的地方在為Parser實例分配內存并解析代碼。方法中返回一個Task對象,因此調用者可以await解析工作,然后釋放UI線程使得可以響應用戶的輸入。

由于Visual Studio的一些特性可能需要多次獲取相同的語法樹, 所以通常可能會緩存解析結果來節省時間和內存分配,但是下面的代碼可能會導致內存分配:

[js] view plaincopy
  1. class?Compilation???
  2. {?/*...*/??
  3. ????private?SyntaxTree?cachedResult;??
  4. ????public?async?Task<SyntaxTree>?GetSyntaxTreeAsync()???
  5. ????{???
  6. ????????if?(this.cachedResult?==?null)???
  7. ????????{???
  8. ????????????var?parser?=?new?Parser();?//?allocation???
  9. ????????????await?parser.ParseSourceCode();?//?expensive???
  10. ????????????this.cachedResult?=?parser.Syntax;???
  11. ????????}???
  12. ????????return?this.cachedResult;??
  13. ????}??
  14. }??

代碼中有一個SynataxTree類型的名為cachedResult的字段。當該字段為空的時候,GetSyntaxTreeAsync()執行,然后將結果保存在cache中。GetSyntaxTreeAsync()方法返回SyntaxTree對象。問題在于,當有一個類型為Task<SyntaxTree> 類型的async異步方法時,想要返回SyntaxTree的值,編譯器會生出代碼來分配一個Task來保存執行結果(通過使用Task<SyntaxTree>.FromResult())。Task會標記為完成,然后結果立馬返回。分配Task對象來存儲執行的結果這個動作調用非常頻繁,因此修復該分配問題能夠極大提高應用程序響應性。

解決方法:

要移除保存完成了執行任務的分配,可以緩存Task對象來保存完成的結果。

[js] view plaincopy
  1. class?Compilation???
  2. {?/*...*/??
  3. ????private?Task<SyntaxTree>?cachedResult;??
  4. ????public?Task<SyntaxTree>?GetSyntaxTreeAsync()???
  5. ????{???
  6. ????????return?this.cachedResult????(this.cachedResult?=?GetSyntaxTreeUncachedAsync());???
  7. ????}??
  8. ????private?async?Task<SyntaxTree>?GetSyntaxTreeUncachedAsync()???
  9. ????{??
  10. ????????var?parser?=?new?Parser();?//?allocation???
  11. ????????await?parser.ParseSourceCode();?//?expensive???
  12. ????????return?parser.Syntax;???
  13. ????}???
  14. }??

代碼將cachedResult 類型改為了Task<SyntaxTree> 并且引入了async幫助函數來保存原始代碼中的GetSyntaxTreeAsync()函數。GetSyntaxTreeAsync函數現在使用 null操作符,來表示當cachedResult不為空時直接返回,為空時GetSyntaxTreeAsync調用GetSyntaxTreeUncachedAsync()然后緩存結果。注意GetSyntaxTreeAsync并沒有await調用GetSyntaxTreeUncachedAsync。沒有使用await意味著當GetSyntaxTreeUncachedAsync返回Task類型時,GetSyntaxTreeAsync 也立即返回Task, 現在緩存的是Task,因此在返回緩存結果的時候沒有額外的內存分配。

其他一些影響性能的雜項

在大的app或者處理大量數據的App中,還有幾點可能會引發潛在的性能問題。

字典

在很多應用程序中,Dictionary用的很廣,雖然字非常方便和高校,但是經常會使用不當。在Visual Studio以及新的編譯器中,使用性能分析工具發現,許多dictionay只包含有一個元素或者干脆是空的。一個空的Dictionay結構內部會有10個字段在x86機器上的托管堆上會占據48個字節。當需要在做映射或者關聯數據結構需要事先常量時間查找的時候,字典非常有用。但是當只有幾個元素,使用字典就會浪費大量內存空間。相反,我們可以使用List<KeyValuePair<K,V>>結構來實現便利,對于少量元素來說,同樣高校。如果僅僅使用字典來加載數據,然后讀取數據,那么使用一個具有N(log(N))的查找效率的有序數組,在速度上也會很快,當然這些都取決于的元素的個數。

類和結構

不甚嚴格的講,在優化應用程序方面,類和結構提供了一種經典的空間/時間的權衡(trade off)。在x86機器上,每個類即使沒有任何字段,也會分配12 byte的空間 (譯注:來保存類型對象指針和同步索引塊),但是將類作為方法之間參數傳遞的時候卻十分高效廉價,因為只需要傳遞指向類型實例的指針即可。結構體如果不撞向的話,不會再托管堆上產生任何內存分配,但是當將一個比較大的結構體作為方法參數或者返回值得時候,需要CPU時間來自動復制和拷貝結構體,然后將結構體的屬性緩存到本地便兩種以避免過多的數據拷貝。

緩存

性能優化的一個常用技巧是緩存結果。但是如果緩存沒有大小上限或者良好的資源釋放機制就會導致內存泄漏。在處理大數據量的時候,如果在緩存中緩存了過多數據就會占用大量內存,這樣導致的垃圾回收開銷就會超過在緩存中查找結果所帶來的好處。

結論

在大的系統,或者或者需要處理大量數據的系統中,我們需要關注產生性能瓶頸癥狀,這些問題再規模上會影響app的響應性,如裝箱操作、字符串操作、LINQ和Lambda表達式、緩存async方法、緩存缺少大小限制以及良好的資源釋放策略、使用Dictionay不當、以及到處傳遞結構體等。在優化我們的應用程序的時候,需要時刻注意之前提到過的四點:

  1. 不要進行過早優化——在定位和發現問題之后再進行調優。
  2. 專業測試不會說謊——沒有評測,便是猜測。
  3. 好工具很重要。——下載 PerfView,然后去看使用教程。
  4. 內存分配決定app的響應性。——這也是新的編譯器性能團隊花的時間最多的地方。

參考資料

  • 如果想觀看關于這一話題的演講,可以在 Channel 9上觀看。
  • VS Profiler基礎 http://msdn.microsoft.com/en-us/library/ms182372.aspx
  • .NET 英語程序性能分析工具一覽 http://msdn.microsoft.com/en-us/library/hh156536.aspx
  • Windows Phone性能分析工具 http://msdn.microsoft.com/en-us/magazine/hh781024.aspx
  • 一些C# 和VB性能優化建議 http://msdn.microsoft.com/en-us/library/ms173196(v=vs.110).aspx(注:原文中該鏈接無內容,連接地址應該使 http://msdn.microsoft.com/en-us/library/ms173196(v=vs.100).aspx)
  • 一些高級優化建議 http://curah.microsoft.com/4604/improving-your-net-apps-startup-performance
文章出自:? 寒江獨釣的博客

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/447332.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/447332.shtml
英文地址,請注明出處:http://en.pswp.cn/news/447332.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

redis.conf配置文件詳解

基本配置 daemonize no #是否以后臺進程啟動databases 16 #創建database的數量(默認選中的是database 0)save 900 1 #刷新快照到硬盤中&#xff0c;必須滿足兩者要求才會觸發&#xff0c;即900秒之后至少1個關鍵字發生變化save 300 10 #必須是300秒之后至少10個關鍵字發生變…

什么原因成就了一位優秀的程序員?

這些年我曾和很多程序員一起工作&#xff0c;他們之中的一些人非常厲害&#xff0c;而另一些人顯得平庸。不久前因為和一些技術非常熟練的程序員工作感覺很愉快&#xff0c;我花了一些時間在考慮我佩服他們什么呢&#xff1f;什么原因讓優秀的程序員那么優秀&#xff0c;糟糕的…

Redis的哨兵模式Sentinel

sentinel功能 redis的sentinel系統用于管理多個redis服務器&#xff0c;該系統主要執行三個任務&#xff1a;監控、提醒、自動故障轉移。 1、監控&#xff08;Monitoring&#xff09;&#xff1a; Redis Sentinel實時監控主服務器和從服務器運行狀態&#xff0c;并且實現自動…

csdn 到底怎么了?不準轉載?

我轉載了20多文章&#xff0c;很多人閱讀過&#xff0c;但是今天看到閱讀量是0&#xff0c; csdn 到底怎么了&#xff1f; 對用戶這樣&#xff1f;請大家看看是不是這樣&#xff1f;

python3之后版本讀取網頁的內容

import urllib.request url "http://helloworldbook2.com/data/message.txt" #直接通過url來獲取網頁數據 print(第一種) response urllib.request.urlopen(url) code response.getcode() html response.read() mystr html.decode("utf8") response.c…

三十功名塵與土——資深程序員生涯自白

摘要&#xff1a;作者Codist&#xff08;網名&#xff09;在程序員崗位上工作了三十多年&#xff0c;在這期間他悟出了一些真理&#xff0c;比如&#xff0c;成功來自對失敗的總結學習&#xff1b;條條大路通羅馬&#xff0c;羅馬并不在乎你用什么方式到達。你在生活中積累了哪…

linux/unix核心設計思想

1&#xff09; 程序應該小而專一&#xff0c;程序應該盡量的小&#xff0c;且只專注于一件事上&#xff0c;不要開發那些看起來有用但是90%的情況都用不到的特性&#xff1b; 2&#xff09; 程序不只要考慮性能&#xff0c; 程序的可移植性更重要&#xff0c;shell和perl&…

操作系統環境變量

在 Java中&#xff0c;許多類都是 Iterable &#xff0c;主要包括所有的 Collection 類&#xff08;但不包括各種 Maps &#xff09;。 例如&#xff0c;下面的代碼可以顯示所有的操作系統環境變量&#xff1a; // collections/EnvironmentVariables.java // {VisuallyInspect…

用Unix的設計思想來應對多變的需求

摘要&#xff1a;無論是Unix設計&#xff0c;還是面向對象設計&#xff0c;還是別的什么如SOA&#xff0c;ECB&#xff0c;消息&#xff0c;事件&#xff0c;MVC&#xff0c;網絡七層模型&#xff0c;數據庫設計&#xff0c;等等&#xff0c;他們都在干三件事——解耦&#xff…

學習較底層編程:動手寫一個C語言編譯器

動手編寫一個編譯器&#xff0c;學習一下較為底層的編程方式&#xff0c;是一種學習計算機到底是如何工作的非常有效方法。 編譯器通常被看作是十分復雜的工程。事實上&#xff0c;編寫一個產品級的編譯器也確實是一個龐大的任務。但是寫一個小巧可用的編譯器卻不是這么困難。…

Arrays.deepToString() 方法同時適用于基元數組和對象數組

Arrays.deepToString() 方法同時適用于基元數組和對象數組&#xff1a; import java.util.*;public class MultiDimWrapperArray {public static void main(String[] args) {Integer[][] a1 { // Autoboxing{ 1, 2, 3, },{ 4, 5, 6, },};Double[][][] a2 { // Autoboxing{ {…

高效程序員的7個共同特征

要想成為高效的程序員&#xff0c;你需要具備一定的綜合素質才能夠讓你用你所掌握的技能、經驗和知識編寫出有效的代碼。有一些開發人員在技術方面具備一定的技巧&#xff0c;但他們永遠無法成為高效的程序員&#xff0c;就是因為他們缺乏所需的其它幾項特質。本文將給出成為一…

java.util.Array中的方法

概述 asList(): 獲取任何序列或數組&#xff0c;并將其轉換為一個 列表集合 &#xff08;集合章節介紹了此方法&#xff09;。 copyOf()&#xff1a;以新的長度創建現有數組的新副本。 copyOfRange()&#xff1a;創建現有數組的一部分的新副本。 equals()&#xff1a;比較兩…

有關編程的12個猜想

摘要&#xff1a;編程世界的將來如何目前仍難預料&#xff0c;但可以肯定的一點是技術一直在加速發展。本文搜羅出12個獨特的編程視角猜想&#xff0c;一起來看看有哪些猜想在不久的將來就能變為現實。 編程世界的將來如何目前仍難預料&#xff0c;但可以肯定的一點是技術一直…

面試中如何剔除“魚目混珠”程序員?

公司招聘面試事宜是一個耗時耗錢的項目&#xff0c;從挑選簡歷開始&#xff0c;還要花更多的時間面試候選人。有的時候這些人才機構會向你保證這些人都是Java天才、SQL專家、堆棧開發者等等&#xff0c;但實際上真實情況遠不及你想想的。對于一個公司來說&#xff0c;執行招聘面…

InputStream 類型

輸入流類型 I/O-1 類功能構造器參數如何使用ByteArrayInputStream允許將內存的緩沖區當做 InputStream 使用緩沖區&#xff0c;字節將從中取出作為一種數據源&#xff1a;將其與 FilterInputStream 對象相連以提供有用接口StringBufferInputStream將 String 轉換成 InputStr…

java容器相關問題

同步類容器 1&#xff0c;這些復合操作在多線程并發地修改容器時&#xff0c;可能會表現出意外的行為&#xff0c;最經典的便是ConcurrentModificationException&#xff0c;原因是當容器迭代的過程中&#xff0c;被并發的修改了內容&#xff0c;這是由于早期迭代器設計的時候…

趣文:如果編程語言是車

C語言是全能手&#xff0c;小巧&#xff0c;強大&#xff0c;所向披靡&#xff0c;可靠&#xff0c;任何事情都能對付。 C是新的C&#xff0c;雙倍的能力&#xff0c;雙倍的尺寸&#xff0c;適應險惡的環境&#xff0c;但是你如果沒練好就去駕駛&#xff0c;很可能會撞車。 C#是…

Java 線程安全

線程安全 線程安全概念&#xff1a;當多個線程訪問某一個類&#xff08;對象或方法&#xff09;時&#xff0c;這個類始終都能表現出正確的行為&#xff0c;那么這個類&#xff08;對象或方法&#xff09;就是線程安全的。synchronized&#xff1a;可以在任意對象及方法上加鎖…

開發者應該了解的API技術清單!

摘要&#xff1a;有人說&#xff0c;有API的地方就有App&#xff0c;借助這些API開發者輕松構建出一款應用&#xff0c;極大地提高開發效率和開發質量。文中整理了一份API服務清單&#xff0c;內容涵蓋&#xff1a;監控/調試、 CDN 、數據庫、儀表盤、支付、通信等方面&#xf…