高性能托管應用程序設計入門

發表于:2007-05-25來源:作者:點擊數: 標簽:應用程序設計高性能托管入門
摘要: 從性能的角度來學習 .NET Framework 公共語言運行庫。學習如何找出托管代碼性能的最佳方法,以及如何測量托管應用程序的性能。 下載 CLR Profiler 。(330KB) 軟件開發和玩雜耍 我們可以將軟件開發過程比作玩雜耍。通常,人們在玩雜耍時至少需要三件物


  摘要:性能的角度來學習 .NET Framework 公共語言運行庫。學習如何找出托管代碼性能的最佳方法,以及如何測量托管應用程序的性能。
  下載 CLR Profiler。(330KB)

  軟件開發和玩雜耍

  我們可以將軟件開發過程比作玩雜耍。通常,人們在玩雜耍時至少需要三件物品,而對于可以使用的物品數量并沒有任何限制。剛開始學習如何玩雜耍時,您會發現在接球和拋球時您看到的是單個的球。隨著熟練程度的增加,您會開始將注意力集中在這些球的運動上,而不是集中在每個單個的球上。當您掌握了玩雜耍的方法時,您就會再次將注意力集中在單個球上,一邊努力使這個球保持平衡,一邊繼續拋出其他的球。憑直覺您就可以知道這些球的運動方向,并且也總是能將手放在正確的地方接球和拋球??墒?,這與軟件開發又有什么類似之處呢?

  不同的角色在軟件開發的過程中會“耍弄”不同的三件物品:項目和程序管理人員耍弄的是功能、資源和時間,而軟件開發人員則耍弄正確性、性能和安全性。人們在玩雜耍時總是試圖使用更多的物品,但只要是學過雜耍的人都會知道,即使只添加一個球都會讓在空中保持所有球的平衡變得更加困難。從技術上講,如果耍弄的球少于三個,那根本就不叫雜耍。作為軟件開發人員,如果您不考慮正在編寫的代碼的正確性、性能和安全性,則說明您根本就沒有投入到您的工作中。當您剛剛開始考慮正確性、性能和安全性時,您會發現您每次只能將注意力集中在一個方面。當它們成為您日常工作的一部分時,您會發現您再也不需要將注意力集中在某一個特定的方面,因為此時它們早已融入了您的工作。而一旦掌握了它們,您就能夠憑直覺進行權衡,并相應地調整自己的注意點。這就跟玩雜耍一樣,關鍵在于實踐。

  編寫高性能的代碼本身也存在三件物品:設置目標、測量和了解目標平臺。如果您不知道代碼必須達到的運行速度,那么您又如何知道您完成了呢?如果您不測量和分析代碼,那么您又如何知道您已經達到了目標,或為什么沒有實現目標呢?如果您不了解目標平臺,那么當您沒有實現目標時,又如何知道要對什么進行優化呢?這些原則對所有的高性能代碼開發過程幾乎都適用,不管您以哪種平臺作為目標平臺都一樣。要完成一篇有關編寫高性能代碼的文章,就必須提到以上這三個方面。雖然這三個方面都同等重要,但是本文的重點在于后兩個方面,因為它們適用于編寫以 Microsoft? .NET Framework 作為目標平臺的高性能應用程序。

  在任意平臺上編寫高性能代碼的基本原則為:

  1、設置性能目標
  2、測量,測量,再測量
  3、了解應用程序的目標硬件和軟件平臺



  .NET 公共語言運行庫

  .NET Framework 的核心是公共語言運行庫 (CLR)。CLR 為您的代碼提供所有的運行時服務:實時[Denver1]編譯、內存管理、安全性和大量其他服務。CLR 的設計考慮了高性能的要求。也就是說,您既可以充分利用其性能,也可以不發揮這些性能。

  本文將從性能的角度概要介紹公共語言運行庫,找出托管代碼性能的最佳辦法,還將展示如何測量托管應用程序的性能。本文并不打算對 .NET Framework 的性能特點進行全面討論。根據本文的目的,文中提到的性能方面的內容將會包括吞吐量、可擴展性、啟動時間和內存使用量等。

  托管數據和垃圾回收器

  在以性能為中心的應用程序中使用托管代碼時,開發人員最關心的問題之一就是 CLR 內存管理的開銷 - 這種管理由垃圾回收器 (GC) 執行。內存管理的開銷由與類型實例關聯的內存的分配開銷、在實例的整個生命周期中內存的管理開銷以及當不再需要時內存的釋放開銷計算得出。

  托管分配的開銷通常都非常小,在大多數情況下,比 C/C++ malloc 或 new 所需的時間還要少。這是因為 CLR 不需要通過掃描可用列表來查找下一個足以容納新對象的可用連續內存塊,而是一直都將指針保持指向內存中的下一個可用位置。我們可以將對托管堆的分配看作“類似于?!?。如果 GC 需要釋放內存才能分配新對象,那么分配可能會引發回收操作。在這種情況下,分配的開銷就會比 malloc 和 new 大。固定的對象也會對分配的開銷造成影響。固定的對象是指那些 GC 接到指令在回收操作期間不能移動其位置的對象,通常是由于這些對象的地址已傳遞到本機 API 中。

  與 malloc 或 new 不同的是,在對象的整個生命周期中管理內存都會帶來開銷。CLR GC 區分不同的代,這就意味著它不是每次都回收整個堆。但是,GC 仍然需要了解剩余堆中的活動對象的根是否是堆中正在回收的那部分對象。如果內存中包含的對象具有對其下一代對象的引用,那么在這些對象的生命周期中,管理內存的開銷將會非常大。

  GC 是代標記和空閑內存的回收器。托管堆包含三代:第 0 代包含所有的新對象,第 1 代包含存活期較長的對象,第 2 代包含長期存活的對象。在釋放足夠的內存以維持應用程序繼續運行的前提下,GC 將盡可能從堆中回收最小的部分。代的回收操作包括對所有下一代的回收,在這種情況下,第 1 代回收也會回收第 0 代。第 0 代的大小會根據處理器緩存的大小和應用程序的分配率動態變化,它用于回收的時間通常都不會超過 10 毫秒。第 1 代的大小根據應用程序的分配率動態變化,它用于回收的時間通常介于 10 到 30 毫秒之間。第 2 代的大小取決于應用程序的分配配置文件,它用于回收的時間也取決于此文件。對管理應用程序內存的性能開銷影響最大的正是這些第 2 代的回收操作。

  提示:GC 具備自我調節能力,它會根據應用程序內存的要求對自身進行調節。多數情況下,通過編程方式調用 GC 時,它的調節功能都未啟用。通過調用 GC.Collect 來“幫助”GC 通常無法提高您的應用程序的性能。

  GC 可以在回收對象期間重定位存活的對象。如果這些對象比較大,那么重定位的開銷也會較大,因此這些對象都將分配到堆中的一個稱為大對象堆 (Large Object Heap) 的特殊區域中。大對象堆可以被回收,但不能壓縮,例如,這些大對象不能進行重定位。大對象是指那些大于 80k 的對象。請注意,在將來發行的 CLR 版本中此概念可能會有所變化。需要回收大對象堆時,將強制進行完全回收,而且是在回收第 2 代時才回收它們。大對象堆中對象的分配率和死亡率都會對管理應用程序內存的性能開銷產生很大的影響。

  分配配置文件

  托管應用程序的全局分配配置文件定義了垃圾回收器對與應用程序相關的內存進行管理的工作量有多大。GC 管理內存的工作量越大,GC 所經歷的 CPU 周期數就越多,而 CPU 運行應用程序代碼所花費的時間也就越短。分配配置文件由已分配對象數、對象的大小及其生命周期計算得出。緩解 GC 壓力的一種最明顯的方法就是減少分配的對象數量。使用面向對象設計技術將應用程序設計為具有可擴展性、模塊化和可復用的特性,往往會導致分配的數量增多。抽象和“精確”都會導致性能下降。

  GC 友好的分配配置文件中將包含一些在開始執行應用程序時分配的對象,這些對象的生命周期與應用程序一致,而其他對象的生命周期都比較短。存活時間較長的對象很少或不包含對存活時間較短的對象的引用。當分配配置文件偏離此原則時,GC 就必須努力工作以管理應用程序的內存。

  GC 不友好的分配配置文件中將包含一些可以存活到第 2 代但隨后就會死去的對象,或者將包含一些要分配到大對象堆的存活時間較短的對象。那些存活時間長得足以進入第 2 代而隨后就會死去的對象,是管理開銷最大的對象。就像我在前面提到的,如果在執行 GC 期間上一代中的對象包含對新一代對象的引用,也會增加回收的開銷。

  典型的實際分配配置文件介于上面提到的兩種分配配置文件之間。分配配置文件的一個重要度量標準是 CPU 花在 GC 上的時間占其總時間的百分比。您可以通過 .NET CLR Memory:% Time in GC 性能計數器獲得這一數字。如果此計數器的平均值大于 30%,則您可能需要對您的分配配置文件進行一次仔細的檢查。這并不一定意味著您的分配配置文件有問題,在某些占用大量內存的應用程序中,GC 達到這種水平是必然的,也是正常的。當您遇到性能問題時,首先應該檢查此計數器,它將立即顯示出您的分配配置文件是否出現了問題。

  提示:如果 .NET CLR Memory:% Time in GC 性能計數器指示您的應用程序花在 GC 上的平均時間高于它的總時間的 30%,則表明您需要對您的分配配置文件進行一次仔細的檢查。

  提示:GC 友好的應用程序中包含的第 0 代對象遠遠多于第 2 代對象。此比率可以通過比較 NET CLR Memory:# Gen 0 Collections 和 NET CLR Memory:# Gen 2 Collections 性能計數器的結果來得出。


  用于分析的API和CLR分析器

  CLR 中包含一個功能強大的用于分析的 API,第三方可用它來為托管應用程序編寫自定義分析器。CLR 分析器是一種分配分析示例工具,由 CLR Product Team 編寫,但不提供技術支持,該分析器使用的就是這種用于分析的 API。開發人員可以使用 CLR 分析器查看其管理應用程序的分配配置文件。


圖 1:CLR 分析器主窗口

  CLR 分析器包括大量非常有用的分配配置文件視圖,包括:已分配類型的柱狀圖、分配和調用圖表、顯示不同代的 GC 和進行這些回收之后托管堆的結果狀態的時間線,以及顯示各個方法分配和程序集加載情況的調用樹。


圖 2:CLR 分析器分配圖表

  提示:有關如何使用 CLR 分析器的詳細信息,請參閱 Zip 文件中包含的自述文件。
請注意,CLR 分析器具有高性能的特點,可以顯著地改變應用程序的性能特點。如果您在運行應用程序時也運行了 CLR 分析器,因壓力而產生的問題很可能都會消失。

  集成服務器 GC

  有兩種不同的垃圾回收器可供 CLR 使用:工作站 GC 和服務器 GC??刂婆_和 Windows 窗體應用程序中集成了工作站 GC,而 ASP.NET 中集成了服務器 GC 。服務器 GC 針對吞吐量和多處理器的可擴展性進行了優化。服務器 GC 在整個回收期間(包括標記階段和清除階段)會暫停所有運行托管代碼的線程,并且 GC 會在可供高優先級的 CPU 專用線程中的進程使用的所有 CPU 上并行執行。如果線程在執行 GC 期間運行了本機代碼,那么只有當本機調用返回時這些線程才會暫停。如果您要構建的服務器應用程序將要在多處理器計算機上運行,強烈建議您使用服務器 GC。如果您的應用程序不是由 ASP.NET 提供的,那么您就必須編寫一個本機應用程序來顯式集成 CLR。

  工作站 GC 經過優化,其滯后時間非常短,非常適合客戶端應用程序。沒有人會希望客戶端應用程序在執行 GC 期間出現明顯的停頓,這是因為客戶端的性能通常都不是通過原始吞吐量,而是通過反應性能來測量的。工作站 GC 是并發 GC,這意味著它會在托管代碼運行的同時執行標記階段。工作站 GC 僅在需要執行清除階段時才會暫停運行托管代碼的線程。在工作站 GC 中,由于 GC 僅在一個線程上運行,因而它只在一個 CPU 上運行。

  終結 (Finalization)

  CLR 提供一種在釋放與類型實例關聯的內存之前自動進行清除的機制。這一機制稱為終結 (Finalization)。通常,終結用于釋放本機資源,在這種情況下,則釋放由對象使用的數據庫連接或操作系統句柄。

  終結是一個開銷很大的功能,而且它還會加大 GC 的壓力。GC 會跟蹤 Finalizable 隊列中需要執行終結操作的對象。如果在回收期間,GC 發現了一個不再存活且需要終結的對象,它就會將該對象在 Finalizable 隊列中的條目移至 FReachable 隊列中。終結操作在一個稱為終結器線程 (Finalizer Thread) 的獨立線程中執行。因為在終結器的執行過程中,可能需要用到該對象的所有狀態,因此該對象或其指向的所有對象都會升級至下一代。與對象或對象圖相關的內存僅在執行后續的 GC 時才會釋放。

  需要釋放的資源應該包裝在一個盡可能小的可終結對象中,例如,如果您的類需要引用托管資源和非托管資源,那么您應該在新的可終結類中包裝非托管資源,并使該類成為您的類的成員。父類不能是可終結類。這意味著只有包含非托管資源的類會被升級(假如您沒有在包含非托管資源的類中引用父類)。另外還要記住只有一個終結線程。如果有終結器導致此線程被阻塞,就不能調用后續的終結器,也不能釋放資源,而且您的應用程序會導致資源泄漏。

  提示:應該盡可能使終結器保持簡單,且永遠不會阻塞。

  提示:僅將需要清除的非托管對象的包裝類設為可終結。

  可以將終結認為是引用計數的一種替換形式。執行引用計數的對象將跟蹤有多少其他對象對其進行引用(這會導致一些非常有名的問題),以便在引用計數為零時釋放其資源。CLR 沒有實現引用計數,因此它需要提供一種機制,以便在不存在對象的引用時自動釋放資源。終結就是這種機制。通常,終結僅需要在要清除的對象的生命周期不明確的情況下執行。


  清理模式 (Dispose Pattern)

  當對象的生命周期不明確時,應該盡快釋放與該對象關聯的非托管資源。這一過程稱為“清理”對象。清理模式通過 IDisposable 接口實現(盡管您自己實現也很容易)。如果您希望對類應用終結,例如,要使類實例可清理,就需要讓對象實現 IDisposable 接口并執行 Dispose 方法。使用 Dispose 方法您可以調用終結器中的同一段清除代碼,并通知 GC 不需要通過調用 GC.SuppressFinalization 方法來終結該對象。最好同時使用 Dispose 方法和終結器來調用通用終結函數,這樣就只需要維護一份清除代碼。而且,如果對象的語義為:Close 方法比 Dispose 方法更符合邏輯,那么還應實現 Close 方法,在這種情況下,數據庫連接或套接字邏輯上都被“關閉”。Close 可以只是簡單地調用 Dispose 方法。

  使用終結器為類提供 Dispose 方法始終是一種很好的做法,因為永遠沒有人能確切地知道使用類的方法,例如,是否可以明確知道它的生命周期。如果您使用的類實現了清理模式,而且您也確切地知道何時清理好對象,最好明確地調用 Dispose。

  提示:請為所有可終結的類提供 Dispose 方法。

  提示:請將終結操作隱藏在 Dispose 方法中。

  提示:請調用通用清除函數。

  提示:如果您使用的對象實現了 IDisposable,并且您知道已不再需要該對象,請調用 Dispose 方法。

  C# 提供了一種非常方便的自動清理對象的方法。使用關鍵字 using 來標記代碼塊,之后,將對大量可清理對象調用 Dispose。

  C# 的 using 關鍵字

using(DisposableType T)
{
//對 T 執行一些操作
}
//自動調用 T.Dispose()

  弱引用注釋

  在堆棧中、寄存器中、其他對象上或某一其他 GC 根對象上,對對象的任何引用都會使得該對象在執行 GC 期間保持存活。一般來說,這是一件好事,因為這通常都表示應用程序不是借助該對象來執行的。然而,有些時候您會需要引用某個對象,但又不想影響其生命周期。在這種情況下,CLR 為實現這一目的而提供了一種稱為“弱引用”的機制。任何強引用(例如,以對象為根的引用)都可以轉換成弱引用。例如,當您需要創建可以遍歷數據結構的外部游標對象,但又不影響該對象的生命周期時,可能需要使用弱引用。又例如,當您需要創建一個存在內存壓力時就會刷新的緩存時也可能會需要使用弱引用,例如,發生 GC 時。

  在 C# 中創建弱引用

MyRefType mrt = new MyRefType();
//...

//創建弱引用
WeakReference wr = new WeakReference(mrt);
mrt = null; //對象不再是根對象
//...

//對象是否已回收?
if(!wr.IsAlive)
{
//獲得該對象的強引用
mrt = wr.Target;
//對象為根對象而且可以再次使用
}
else
{
//重新創建該對象
mrt = new MyRefType();
}

  托管代碼和 CLR JIT

  托管程序集是托管代碼的分發單位,它由 Microsoft 中間語言(MSIL 或 IL)構成,適用于所有的處理器。CLR 的實時 (JIT) 功能可將 IL 編譯成優化的本機 X86 指令。JIT 是一種執行優化操作的編譯器,但是由于編譯是在軟件運行時進行的,并且僅當第一次調用方法時才會進行,因此進行優化的次數需要與執行編譯所花費的時間保持平衡。通常,這對于服務器應用程序并不重要,因為啟動時間和響應對于它們來說通常都不構成問題;但對于客戶端應用程序來說,卻十分重要。請注意,安裝時可以通過使用 NGEN.exe 執行編譯來加快啟動時間。

  許多由 JIT 執行的優化操作都沒有與其關聯的編程模式,例如,您無法對它們進行顯式編碼,但是也有一些優化操作具有關聯的編程模式。下一節將討論后者中的部分操作。

  提示:使用 NGEN.exe 實用程序在安裝時編譯應用程序,可以加快客戶端應用程序的啟動時間。

  方法內聯

  所有的方法調用都會帶來開銷。例如,需要將參數推入棧中或存儲在寄存器中,需要執行的方法起頭 (prolog) 和結尾 (epilog) 等。只需要將被調用方法的方法主體移入調用方的主體,就可以避免某些方法的調用開銷。這一操作稱為方法內聯。JIT 使用大量的探測方法來確定是否應內聯某個方法。下面是其中一些比較重要的探測方法的列表(請注意這并不是一個詳盡的列表):

   IL 超過 32 字節的方法不會內聯。

   虛函數不會內聯。

  包含復雜流程控制的函數不會內聯。復雜流程控制是除 if/then/else 以外的任意流程控制,在這種情況下,為 switch 或 while。

  包含異常處理塊的方法不會內聯,但是引發異常的方法可以內聯。

  如果某個方法的所有定參都為結構,則該方法不會內聯。

  我會認真考慮一下對這些探測方法進行顯式編碼的問題,因為在以后的 JIT 版本中它們可能會有所變化。請不要為了確保方法可以內聯而放棄方法的正確性。您也許已經注意到了一個有趣的現象,C++ 中的關鍵字 inline 和 __inline 不能保證編譯器將一種方法內聯(盡管 __forceinline 可以)。

  一般情況下,屬性的 Get 和 Set 方法都非常適合內聯,因為它們主要用于初始化私有數據成員。

  提示:請不要為了試圖保證內聯而放棄方法的正確性。

  去除范圍檢查

  托管代碼有許多優點,其中一項是可以自動進行范圍檢查。每次使用 array[index] 語義訪問數組時,JIT 都會進行檢查以確保索引在數組范圍中。在具有大量迭代和少量由每個迭代執行的指令的循環環境中,范圍檢查的開銷可能會很大。在某些情況下,JIT 也可能會在檢測到這些范圍檢查不必要時將其從循環體中刪除,即僅在循環開始執行之前對其進行一次檢查。在 C# 中有一種編程模式,用來確保這些范圍檢查會被刪除:對“for”語句中數組的長度進行的顯式測試。請注意,只要此模式中存在細微的偏差都會導致檢查無法去除,在這種情況下,需向索引中添加一個值。

  在 C# 中去除范圍檢查

//范圍檢查將被去除
for(int i = 0; i < myArray.Length; i++)
{
Console.WriteLine(myArray[i].ToString());
}

//范圍檢查將無法去除
for(int i = 0; i < myArray.Length + y; i++)
{
Console.WriteLine(myArray[i+x].ToString());
}

  搜索大型不規則數組時,優化操作特別明顯,因為此時將同時刪除內循環和外循環范圍檢查。

  要求進行變量使用情況跟蹤的優化操作

  大量 JIT 編譯器優化操作都要求 JIT 跟蹤定參和局部變量的使用情況,例如,它們在方法主體中的最早以及最近一次使用的時間。在 CLR 1.0 和 1.1 版中,JIT 可以跟蹤使用情況的變量總數限制在 64 個之內。例如“寄存操作”就是一個需要進行使用情況跟蹤的優化操作。寄存操作是指將變量存儲在處理器寄存器中,而不是??蚣苤校ɡ?,在內存中)。與對??蚣苤械淖兞窟M行訪問的時間相比,對寄存變量的訪問要快得多,即便框架中的變量位于處理器的緩存中也一樣。只能對 64 個變量進行寄存,所有其他變量都將推至棧中。除寄存操作以外,另外也有一些優化操作需要進行使用情況跟蹤。應該將方法中的定參和局部參數的數量保持在 64 個以下,以確保實現最大數目的 JIT 優化操作。請記住在以后的 CLR 版本中此數目可能會有所變化。

  提示:使方法保持簡短。要這樣做的原因有很多,包括方法內聯、寄存操作和 JIT 持續時間的需要。

  其他 JIT 優化操作

  JIT 編譯器還可以執行大量其他的優化操作:常量和副本傳播、循環不變式提升以及若干其他操作。需要用來實現優化的編程模式都是免費的,無需花錢購買。

  為什么我在 Visual Studio 中沒有看到這些優化功能?

  當您在 Visual Studio 中使用 Debug(調試)菜單或按下 F5 鍵啟動應用程序時,無論您生成的是發行版還是調試版,所有的 JIT 優化功能都將被禁用。當托管應用程序通過調試器啟動時,即使它不是該應用程序的調試版本,JIT 也會發出非優化的 x86 指令。如果您希望 JIT 發出優化代碼,那么請從 Windows 資源管理器中啟動該應用程序,或者在 Visual Studio 中使用 CTRL+F5 組合鍵。如果希望查看優化的反匯編程序,并將其與非優化代碼進行對比,則可以使用 cordbg.exe。

 提示:使用 cordbg.exe 可以查看 JIT 發出的優化和非優化代碼的反匯編程序。使用 cordbg.exe 啟動該應用程序后,可以通過鍵入以下代碼來設置 JIT 模式:

(cordbg) mode JitOptimizations 1
// JIT 將生成優化的代碼

(cordbg) mode JitOptimizations 0

  JIT 將生成可調試(非優化)代碼。


  值類型

  CLR 可提供兩組不同的類型:引用類型和值類型。引用類型總是分配到托管堆中,并按引用傳遞(正如它的名稱所暗示的)。值類型分配到棧中或在堆中內聯為對象的一部分,默認情況下按值傳遞,不過您也可以按引用來傳遞它們。分配值類型時,所需的開銷非常小,假設它們總是又小又簡單,當它們作為參數進行傳遞時開銷也會很小。正確使用值類型的一個很好的示例就是包含 x 和 y 坐標的 Point 值類型。

  Point 值類型

struct Point
{
public int x;
public int y;

//
}

  值類型也可以視為對象,例如,可以在值類型上調用對象方法,它們可以轉換為對象,或傳遞到需要使用對象的位置。無論使用什么方法,只要將值類型轉換為引用類型,都需要經過裝箱 (Boxing) 處理。對值類型進行裝箱處理時,會在托管堆中分配一個新對象,而值則復制到此新對象中。這項操作將占用很大的系統開銷,還可能會降低或完全消除通過使用值類型而獲得的性能。將裝箱的類型隱式或顯式轉換回值類型的過程稱為取消裝箱 (Unboxed)。

  裝箱/取消裝箱值類型

  C#:

int BoxUnboxValueType()
{
int i = 10;
object o = (object)i; //i 被裝箱
return (int)o + 3; //i 被取消裝箱
}

  MSIL:

.method private hidebysig instance int32
BoxUnboxValueType() cil managed
{
// 代碼大小 20 (0x14)
.maxstack 2
.locals init (int32 V_0,
object V_1)
IL_0000: ldc.i4.s 10
IL_0002: stloc.0
IL_0003: ldloc.0
IL_0004: box [mscorlib]System.Int32
IL_0009: stloc.1
IL_000a: ldloc.1
IL_000b: unbox [mscorlib]System.Int32
IL_0010: ldind.i4
IL_0011: ldc.i4.3
IL_0012: add
IL_0013: ret
} // 方法 Class1::BoxUnboxValueType 結束

  如果實現自定義值類型(C# 中的結構),則應該考慮覆蓋 ToString 方法。如果不覆蓋此方法,那么對值類型上的 ToString 的調用將導致該類型被裝箱。對于從 System.Object 繼承的其他方法也是如此。在這種情況下,請使用 Equals 來進行覆蓋,盡管 ToString 很可能是最常用的調用方法。如果您希望了解值類型是否被裝箱以及何時裝箱,可以使用 ildasm.exe 實用程序在 MSIL 中查找 box 指令(如上所述)。

  覆蓋 C# 中的 ToString() 方法以防止裝箱

struct Point
{
public int x;
public int y;

//此操作將防止在調用 ToString 時對類型進行裝箱
public override string ToString()
{
return x.ToString() + "," + y.ToString();
}
}

  請注意,在創建集合(例如,浮點數組列表)時,添加到集合中的每一項都將進行裝箱。您應該考慮使用數組或為值類型創建自定義集合類。

  使用 C# 中的集合類時進行隱式裝箱

ArrayList al = new ArrayList();
al.Add(42.0F); //由于 Add() 接受對象因此進行隱式裝箱
float f = (float)al[0]; //取消裝箱

  異常處理

  通常,錯誤條件都將作為常規流程控制使用。在此情況下,如果試圖通過編程將用戶添加到 Active Directory 實例中,則只能試著添加該用戶,如果系統返回 E_ADS_OBJECT_EXISTS HRESULT,則說明它們已經存在于該目錄中。此外,您也可以通過搜索目錄查找該用戶,如果搜索失敗則只需添加該用戶。

  按照常規流程控制使用錯誤,在 CLR 環境中會降低性能。CLR 中的錯誤處理是借助結構化異常處理實現的。引發異常之前,托管異常的開銷非常小。在 CLR 中,引發異常時,需要使用堆棧遍歷為已引發的異常找到相應的異常處理程序。堆棧遍歷是一種開銷較大的操作。正如它的名稱所表示的,異常應該用于異?;蛞馔獾那闆r。

  提示:對于以性能為中心的方法,請返回預期結果的枚舉結果,而不是引發異常。

  提示:有多種 .NET CLR 異常性能計數器都可以通知您在應用程序中引發了多少異常。

  提示:如果您使用 VB.NET 來使用除 On Error Goto 以外的異常,錯誤對象就是不必要的開銷。


  線程和同步

  CLR 提供豐富的線程和同步功能,包括創建自己的線程、線程池和各種同步原語的能力。在充分利用 CLR 中支持的線程之前,應該仔細考慮一下線程的用法。請記住,添加線程實際上會降低吞吐量,而不會增加吞吐量,但它肯定會增加內存的利用率。在將要在多處理器計算機上運行的服務器應用程序中,采用并行操作(盡管這取決于將要執行多少鎖爭用,例如,序列化執行方式)來添加線程可以顯著地提高吞吐量;在客戶端應用程序中,添加顯示活動和/或進度的線程可以提高反應性能(低吞吐量開銷)。

  如果應用程序中的線程不是專門用于特定任務的線程,或者關聯有特殊的狀態,則應該考慮使用線程池。如果您過去使用過 Win32 線程池,那對 CLR 線程池也一定會比較熟悉。每個托管進程僅存在一個線程池實例。線程池可以智能地識別出它所創建的線程數量,并且會根據計算機上的負載對自身進行調節。

  要討論線程處理,就必須討論同步。所有由多線程為應用程序帶來的吞吐量收益都可能會因為同步邏輯編寫不正確而全部喪失。鎖定的粒度會大大影響應用程序的總體吞吐量,這是因為創建和管理鎖會帶來系統開銷,并且鎖定很可能會序列化執行步驟所致。我將使用在樹中添加節點的示例來說明這一觀點。例如,如果樹將成為共享的數據結構,則多線程需要在執行應用程序期間對其進行訪問,而且您需要對該樹進行同步訪問。您可以選擇在添加節點的同時鎖定整個樹,這意味著您只會在單一鎖定時帶來開銷,但其他試圖訪問該樹的線程都可能會因此而阻塞。這將是一個粗粒度的鎖定示例?;蛘?,您可以在遍歷該樹時鎖定每個節點,這意味著您會在每個節點上創建鎖時帶來開銷,但是其他線程不會因此而阻塞,除非它們試圖訪問您已經鎖定的特定節點。這是細粒度的鎖定示例。僅為要對其進行操作的子樹添加鎖也許是更合適的鎖定粒度。請注意,在本示例中,您可能會使用共享鎖 (RWLock),因為只有這樣才能讓多個讀者同時進行訪問。

  執行同步操作時最簡單有效的方法就是使用 System.Threading.Interlocked 類。Interlocked 類提供大量低級別的原子操作:Increment、Decrement、Exchange 和 CompareExchange。

  在 C# 中使用 System.Threading.Interlocked 類

using System.Threading;
//...
public class MyClass
{
void MyClass() //構造函數
{
//以原子方式遞增全局實例計數器的值
Interlocked.Increment(ref MyClassInstanceCounter);
}

~MyClass() //終結器
{
//以原子方式遞減全局實例計數器的值
Interlocked.Decrement(ref MyClassInstanceCounter);
//...
}
//...
}

  最常用的同步機制可能是監測器 (Monitor) 或臨界區 (Critical Section)。監測器鎖可直接使用,也可以借助 C# 中的 lock 關鍵字來使用。對于給定的對象來說,lock 關鍵字會對特定的代碼塊進行同步訪問。從性能的角度來說,如果監測器鎖的爭用率較低,則系統開銷相對較??;但是如果其爭用率較高,系統開銷也會相對較大。

  C# lock 關鍵字

//線程試圖獲取

//和代碼塊
lock(mySharedObject)
{
//如果塊中包含鎖,
//線程只能執行此塊中的代碼
}//線程釋放鎖

  RWLock 提供的是共享鎖定機制:例如,該鎖可以在“讀者”之間共享,但是不能在“作者”之間共享。在這種鎖也適用的情況下,使用 RWLock 可以比使用監測器帶來更好的吞吐量,它每次只允許一位讀者或作者獲得該鎖。System.Threading 命名空間也包括 Mutex 類。Mutex 是一種同步原語,可用來進行跨進程的同步操作。請注意,它比臨界區的開銷要大很多,僅當需要進行跨進程的同步操作時才應使用它。

  反射

  反射是由 CLR 提供的一種機制,用于在運行時通過編程方式獲得類型信息。反射在很大程度上取決于嵌入在托管程序集中的元數據。許多反射 API 都要求搜索并分析元數據,這些操作的開銷都很大。

  這些反射 API 可以分為三個性能區間:類型比較、成員枚舉和成員調用。這些區間的系統開銷一直在變大。類型比較操作,例如 C# 中的 typeof、GetType、is、IsInstanceOfType 等,都是開銷最小的反射 API,盡管它們的實際開銷一點也不小。成員枚舉操作可以通過編程方式對類的方法、屬性、字段、事件、構造函數等進行檢查。例如,可能會在設計時的方案中使用這一類的成員枚舉操作,在這種情況下,此操作將枚舉 Visual Studio 中的 Property Browser(屬性瀏覽器)的 Customs Web Controls(自定義 Web 控件)的屬性。那些用于動態調用類成員或動態發出 JIT 并執行某個方法的的反射 API 是開銷最大的反射 API。當然,如果需要動態加載程序集、類型實例化以及方法調用,還存在一種后期綁定方案,但是這種松散的耦合關系需要進行明確的性能權衡。一般情況下,應該在對性能影響很大的代碼路徑中避免使用反射 API。請注意,盡管您沒有直接使用反射,但是您使用的 API 可能會使用它。因此,也要注意是否間接使用了反射 API。

  后期綁定

  后期綁定調用是一種在內部使用反射的功能。Visual Basic.NET 和 JScript.NET 都支持后期綁定調用。例如,使用變量之前您不必進行聲明。后期綁定對象實際上是類型對象,可以在運行時使用反射將該對象轉換為正確的類型。后期綁定調用比直接調用要慢幾個數量級。除非您確實需要后期綁定行為,否則應該避免在性能關鍵代碼路徑中使用它。

  提示:如果您正在使用 VB.NET,且并不一定需要后期綁定,您可以在源文件的頂部包含 Option Explicit On 和 Option Strict On 以便通知編譯器拒絕后期綁定。這些選項將強制您進行聲明,并要求您設置變量類型并關閉隱式轉換。

  安全性

  安全性是必要的而且也是主要的 CLR 的組成部分,使用它時會降低性能。當代碼為 Fully Trusted(完全信任)且安全策略為默認設置時,安全性對應用程序的吞吐量和啟動時間的影響會很小。對代碼持不完全信任態度(例如,來自 Internet 或 Intranet 區域的代碼)或縮小 MyComputer Grant Set 都將增加安全性的性能開銷。

  COM 互操作和平臺調用

  COM 互操作和平臺調用會以幾乎透明的方式為托管代碼提供本機 API,通常調用大多數本機 API 時都不需要任何特殊代碼,但是可能需要使用鼠標進行多次單擊。正如您所預計的,從托管代碼中調用本機代碼會帶來開銷,反之亦然。這筆開銷由兩部分組成:一部分是固定開銷,此開銷與在本機代碼和托管代碼之間進行的轉換有關;另一部分是可變開銷,此開銷與那些可能要用到的參數封送和返回值有關。COM 互操作和平臺調用的固定開銷在開銷中占的比例較?。和ǔ2怀^ 50 條指令。在各托管類型之間進行封送處理的開銷取決于它們在邊界兩側的表示形式的相似程度。需要進行大量轉換的類型開銷相對較大。例如,CLR 中的所有字符串都為 Unicode 字符串。如果要通過平臺調用需要 ANSI 字符數組的 Win32 API,則必須縮小該字符串中的每個字符。但是,如果是將托管的整數數組傳遞到需要本機整數數組的類型中時,就不需要進行封送處理。

  由于存在與調用本機代碼相關的性能開銷,因此您應該確保該開銷是合理的開銷。如果您打算進行本機調用,請確保本機調用所做的工作使得因執行此調用而產生的性能開銷劃算,即盡量使方法“小而精”而非“大而全”。測量本機調用開銷的一種好方法是測量不接受任何參數也不具備任何返回值的本機方法的性能,然后再測量您希望調用的本機方法的性能。它們之間的差異即封送處理的開銷。

  提示:應創建“小而精”的 COM 互操作和平臺調用,而不是“大而全”的調用,并確保調用的開銷對于調用的工作量是劃算的。

  請注意,不存在與托管線程相關的線程模式。當您打算進行 COM 互操作調用時,需要確保已將執行調用的線程初始化為正確的 COM 線程模式。此操作通常是使用 MTAThreadAttribute 和 STAThreadAttribute 來實現的(盡管也可以通過編程來實現)。

  性能計數器

  有大量的 Windows 性能計數器可供 .NET CLR 使用。當開發人員首次診斷性能問題,或試圖識別托管應用程序的性能特點時,這些性能計數器就是他們可以選擇的武器。我已經簡要介紹了幾個與內存管理和異常有關的性能計數器。在 CLR 和 .NET Framework 中,性能計數器幾乎無處不在。通常,這些性能計數器都可以使用且對系統無害,它們的開銷較低,而且不會改變應用程序的性能特征。

  其他工具

  除了性能計數器和 CLR 分析器以外,您還需要使用常規的分析器來確定應用程序中的哪些方法花費的時間最多,且最常被調用。這些方法將是您需要最先進行優化的方法。有多種支持托管代碼的商用分析器可供使用,包括 Compuware 的 DevPartner Studio Professional Edition 7.0 和 Intel? 的 VTune? Performance Analyzer 7.0。Compuware 還生產一種免費的托管代碼分析器,名為 DevPartner Profiler Community Edition。

  小結

  本文只是從性能的角度初步介紹了 CLR 和 .NET Framework。在 CLR 和 .NET Framework 中,還有許多別的方面也會對應用程序的性能產生影響。我最希望告訴各位開發人員的是:請不要對您的應用程序的目標平臺以及您正在使用的 API 的性能做任何的假設。請測量它們!


原文轉自:http://www.anti-gravitydesign.com

評論列表(網友評論僅供網友表達個人看法,并不表明本站同意其觀點或證實其描述)
国产97人人超碰caoprom_尤物国产在线一区手机播放_精品国产一区二区三_色天使久久综合给合久久97