共用方式為


撰寫High-Performance受控應用程式:入門

 

Gregor Noriskin
Microsoft CLR 效能小組

2003 年 6 月

適用於:
   Microsoft® .NET Framework

總結:從效能觀點瞭解.NET Framework的 Common Language Runtime。 瞭解如何識別 Managed 程式碼效能最佳做法,以及如何測量受控應用程式的效能。 (19 個列印頁面)

下載 CLR Profiler。 (330KB)

目錄

將 作為軟體發展的隱喻進行區分
The .NET Common Language Runtime
Managed 資料和垃圾收集行程
配置設定檔
分析 API 和 CLR 分析工具
裝載伺服器 GC
完成
處置模式
弱式參考的附注
Managed 程式碼和 CLR JIT
實值類型
例外狀況處理
執行緒和同步處理
反映
晚期系結
安全性
COM Interop 和 Platform Invoke
效能計數器
其他工具
結論
資源

將 作為軟體發展的隱喻進行區分

區分是描述軟體發展程式的絕佳隱喻。 交錯通常需要至少三個專案,但您可以嘗試交錯的專案數目沒有上限。 當您開始瞭解如何交錯時,您會發現當您攔截並擲回每個球時,會個別watch每個球。 當您進行時,您會開始專注于球的流程,而不是每個個別球。 當您主控雜亂時,可以再次專注于單一球,平衡該球在您的臉上,同時繼續交錯其他人。 您以直覺方式知道球的所在位置,而且可以將手放在正確的位置來捕捉並擲回。 因此,這如何像軟體發展?

軟體發展程式中的不同角色會區分不同的「三元」;專案與專案經理會交錯功能、資源和時間,以及軟體發展人員會處理正確性、效能和安全性。 一律可以嘗試交錯更多專案,但隨著任何交錯的學生可以證明,新增單一球會讓球在空中保持指數更困難。 在技術上,如果您混用少於三個球,則完全不相依。 如果身為軟體發展人員,您並未考慮您撰寫之程式碼的正確性、效能和安全性,則可以將案例設為您未執行您的工作。 當您一開始考慮正確性、效能和安全性時,您會發現自己一次必須專注于一個層面。 當您成為日常練習的一部分時,您會發現您不需要專注于特定層面,它們只會是您工作方式的一部分。 當您主控它們時,您將能夠直覺地做出取捨,並適當地專注您的工作。 如同雜亂一樣,練習是關鍵。

撰寫高效能程式碼具有本身的一部分;設定目標、測量和瞭解目標平臺。 如果您不知道程式碼必須有多快,您如何知道何時完成? 如果您未測量並分析程式碼,您如何知道何時符合您的目標,或為何不符合您的目標? 如果您不了解目標平臺,您如何知道在不符合目標的情況下要優化的內容。 這些原則適用于一般高效能程式碼的開發,無論您的目標平臺。 沒有關于撰寫高效能程式碼的文章將會完成,而不需要提及此小項。 雖然這三個都同樣重要,但本文將著重于後者兩個層面,因為它們適用于撰寫以 Microsoft® .NET Framework為目標的高效能應用程式。

在任何平臺上撰寫高效能程式碼的基本準則如下:

  1. 設定效能目標
  2. 量值、量值,然後測量更多
  3. 瞭解應用程式的目標硬體和軟體平臺

The .NET Common Language Runtime

.NET Framework的核心是 Common Language Runtime (CLR) 。 CLR 會為您的程式碼提供所有執行時間服務;Just-In-Time 編譯、記憶體管理、安全性和許多其他服務。 CLR 的設計目的是要有高效能。 也就是說,您可以利用該效能的方式,以及阻礙它的方式。

本文的目標是要從效能觀點提供 Common Language Runtime 的概觀、識別 Managed 程式碼效能最佳做法,以及示範如何測量受控應用程式的效能。 本文並非完整討論.NET Framework效能特性。 為了達到本文的目的,我將定義效能以包含輸送量、延展性、啟動時間和記憶體使用量。

Managed 資料和垃圾收集行程

開發人員在效能關鍵性應用程式中使用 Managed 程式碼的主要考慮之一是 CLR 的記憶體管理成本,這是垃圾收集行程 (GC) 所執行的成本。 記憶體管理的成本是與類型實例相關聯的記憶體配置成本、在實例存留期間管理該記憶體的成本,以及不再需要記憶體時釋放該記憶體的成本。

受控配置通常非常便宜;在大部分情況下,花費的時間少於 C/C++ mallocnew 。 這是因為 CLR 不需要掃描可用清單,以尋找下一個可用的連續記憶體區塊,足以保存新物件;它會在記憶體中保留下一個可用位置的指標。 您可以將 Managed 堆積配置視為「堆疊類似」。 如果 GC 需要釋放記憶體來配置新的物件,則配置可能會造成集合,在此情況下,配置比 mallocnew 更昂貴。 釘選的物件也會影響配置成本。 固定的物件是 GC 已指示不要在集合期間移動的物件,通常是因為物件的位址已傳遞至原生 API。

malloc不同于 或 new ,在 物件的存留期內,有一個與管理記憶體相關聯的成本。 CLR GC 是世代,這表示不會一律收集整個堆積。 不過,GC 仍必須知道所收集堆積部分的其餘堆積根物件中是否有任何即時物件。 記憶體,其中包含在較新世代中保存物件參考的物件,在物件的存留期內管理成本很高。

GC 是世代標記和掃掠垃圾收集行程。 Managed 堆積包含三個世代;第 0 代包含所有新的物件,第 1 代包含稍微較長的存留物件,而第 2 代則包含長期物件。 GC 會收集堆積的最社區段,以釋放足夠的記憶體,讓應用程式繼續。 Generation 的集合包含所有較新世代的集合,在此情況下,第 1 代集合也會收集第 0 代。 第 0 代會根據處理器快取的大小和應用程式的配置速率動態調整大小,而且通常需要不到 10 毫秒才能收集。 第 1 代會根據應用程式的配置速率動態調整大小,通常需要 10 到 30 毫秒才能收集。 第 2 代大小將取決於應用程式的配置設定檔,以及收集所需的時間。 這是這些第 2 代集合,最會影響管理應用程式記憶體的效能成本。

提示 GC 是自我調整,會根據應用程式記憶體需求自行調整。 在大部分情況下,以程式設計方式叫用 GC 會阻礙該調整。 藉由呼叫 GC 來「協助」 GC。收集 可能不改善您的應用程式效能。

GC 可能會在集合期間重新放置即時物件。 如果這些物件很大,重新配置的成本很高,則這些物件會配置在稱為「大型物件堆積」之堆積的特殊區域中。 會收集大型物件堆積,但不會壓縮,例如,不會重新放置大型物件。 大型物件是大於 80kb 的物件。 請注意,這可能會在未來的 CLR 版本中變更。 需要收集大型物件堆積時,它會強制完整集合,並在第 2 代集合期間收集大型物件堆積。 大型物件堆積中的物件配置和死率可能會對管理應用程式記憶體的效能成本產生顯著的影響。

配置設定檔

受控應用程式的整體配置設定檔會定義垃圾收集行程必須如何處理與應用程式相關聯的記憶體。 GC 必須更難管理記憶體、GC 所花費的 CPU 週期數目愈大,而 CPU 執行應用程式程式碼所花費的時間越少。 配置設定檔是配置的物件數目、這些物件的大小及其存留期的函式。 減輕 GC 壓力的最明顯方式就是配置較少的物件。 針對擴充性、模組化和重複使用使用物件導向設計技術所設計的應用程式,幾乎一律會產生增加的配置數目。 抽象和「精簡」會有效能負面影響。

GC 易記的配置設定檔會在應用程式開頭配置一些物件,然後在應用程式的存留期間存留,然後讓所有其他物件短期存留。 長期物件將包含少數或沒有短期物件的參考。 由於配置設定檔從此開始,GC 必須更難管理應用程式記憶體。

GC-unfriendly 配置設定檔會有許多物件存留到第 2 代,然後死去,或將許多短期物件配置於大型物件堆積中。 存留時間夠長而足以進入第 2 代的物件,然後死去是管理成本最高的物件。 如前所述,在舊代的物件中包含 GC 期間較新世代中物件的參考,也會增加集合的成本。

典型的真實世界配置設定檔會位於上述兩個配置設定檔之間的某處。 配置設定檔的重要計量是花費在 GC 中的 CPU 總時間百分比。 您可以從 .NET CLR 記憶體中取得此號碼:GC 效能計數器中的 %Time 。 如果此計數器的平均值高於 30%,您應該考慮進一步查看您的配置設定檔。 這不一定表示您的配置設定檔「不正確」;有一些需要大量記憶體的應用程式,其中此層級的 GC 是必要的且適當。 如果您遇到效能問題,此計數器應該是您查看的第一件事;如果您的配置設定檔是問題的一部分,它應該會立即顯示。

提示 如果 .NET CLR 記憶體: GC 效能計數器中的 % 時間指出您的應用程式在 GC 中花費的平均時間超過 30%,您應該仔細查看配置設定檔。

提示 GC 易記的應用程式會比第 2 代集合的層代更多。 您可以藉由比較 NET CLR 記憶體:#Gen 0 集合和 NET CLR 記憶體:#Gen 2 集合效能計數器來建立此比率。

分析 API 和 CLR 分析工具

CLR 包含功能強大的分析 API,可讓協力廠商撰寫受控應用程式的自訂分析工具。 CLR Profiler 是不受支援的配置分析範例工具,由 CLR 產品小組撰寫,使用此分析 API。 CLR Profiler 可讓開發人員查看其管理應用程式的配置設定檔。

圖 1 CLR 分析工具主視窗

CLR Profiler 包含一些非常實用的配置設定檔檢視,包括配置類型的長條圖、配置和呼叫圖表、一條時間表,顯示各種世代的 GC,以及這些集合之後 Managed 堆積的結果狀態,以及顯示個別方法配置和元件載入的呼叫樹狀結構。

圖 2 CLR 分析工具配置圖表

提示 如需如何使用 CLR Profiler 的詳細資訊,請參閱 zip 中包含的讀我檔案。

請注意,CLR Profiler 具有高效能的額外負荷,並大幅變更應用程式的效能特性。 當您使用 CLR 分析工具執行應用程式時,新興壓力 Bug 可能會消失。

裝載伺服器 GC

CLR 提供兩個不同的垃圾收集行程:工作站 GC 和伺服器 GC。 主控台和Windows Forms應用程式裝載工作站 GC,ASP.NET 裝載伺服器 GC。 伺服器 GC 已針對輸送量和多處理器延展性進行優化。 伺服器 GC 會在集合的整個期間暫停所有執行 Managed 程式碼的執行緒,包括標記和掃掠階段,而 GC 會在專用高優先順序 CPU 親和化執行緒上的所有 CPU 上平行執行。 如果在 GC 期間執行執行緒,則只有在原生呼叫傳回時,才會暫停這些執行緒。 如果您要建置要在多處理器機器上執行的伺服器應用程式,強烈建議您使用伺服器 GC。 如果您的應用程式不是由 ASP.NET 裝載,則您必須撰寫明確裝載 CLR 的原生應用程式。

提示 如果您要建置可調整的伺服器應用程式,請裝載伺服器 GC。 請參閱 為受控應用程式實作自訂 Common Language Runtime 主機

工作站 GC 已針對低延遲進行優化,這通常是用戶端應用程式所需的。 一個不想在 GC 期間用戶端應用程式中明顯暫停,因為通常用戶端效能不會以原始輸送量來測量,而是透過察覺的效能來測量。 工作站 GC 會執行並行 GC,這表示它在 Managed 程式碼仍在執行時執行 Mark 階段。 GC 只會在需要執行掃掠階段時暫停執行 Managed 程式碼的執行緒。 在工作站 GC 中,GC 只會在一個執行緒上完成,因此只能在一個 CPU 上完成。

完成

CLR 提供一種機制,可在釋放與類型實例相關聯的記憶體之前自動清除。 此機制稱為「最終化」。 一般而言,Finalization 是用來釋放原生資源,在此案例中為物件所使用的資料庫連結或作業系統控制碼。

最終處理是昂貴的功能,會增加 GC 上的壓力。 GC 會追蹤需要完成佇列中最終處理的物件。 如果在集合期間,GC 發現物件已不再存在,但需要最終化,則完成佇列中的該物件專案會移至 FReachable 佇列。 最終處理發生在稱為完成項執行緒的個別執行緒上。 因為物件在執行 Finalizer 期間可能需要整個狀態,所以物件及其指向的所有物件都會升級為下一代。 與物件或物件圖形相關聯的記憶體只會在下列 GC 期間釋放。

需要釋放的資源應該盡可能包裝在可完成的物件中;例如,如果您的類別需要 Managed 和 Unmanaged 資源的參考,則您應該將 Unmanaged 資源包裝在新的 Finalizable 類別中,並將該類別設為類別的成員。 父類別不應該是 Finalizable。 這表示只有包含 Unmanaged 資源的類別將會升級 (假設您未在包含 Unmanaged 資源之類別中保存父類別的參考) 。 另一件事是只有一個最終處理執行緒。 如果完成項造成此執行緒封鎖,將不會呼叫後續完成項、不會釋放資源,而且您的應用程式將會流失。

提示 完成項應盡可能保持簡單,且絕對不應該封鎖。

提示 請只完成需要清除之 Unmanaged 物件的包裝函式類別。

最終處理可視為參考計數的替代方案。 實作參考計數的物件會追蹤其他物件有多少參考 (,這可能會導致一些非常已知的問題) ,以便在其參考計數為零時釋放其資源。 CLR 不會實作參考計數,因此它必須提供一種機制,以便在沒有保留對物件的其他參考時自動釋放資源。 最終處理是該機制。 只有在需要清除的物件存留期未明確知道的情況下,才需要最終完成。

處置模式

如果明確知道物件的存留期,則應該積極釋放與物件相關聯的 Unmanaged 資源。 這稱為 「處置」物件。 Dispose 模式是透過 IDisposable 介面實作 (,但自行實作會是簡單) 。 如果您想要讓類別使用積極式最終處理,例如,讓類別的實例成為可處置的,您必須讓物件實作 IDisposable 介面,並提供 Dispose 方法的實作。 在 Dispose 方法中,您將呼叫位於 Finalizer 中的相同清除程式碼,並通知 GC 不再需要呼叫 GC 來完成物件 。SuppressFinalization 方法。 請讓 Dispose 方法和 Finalizer 同時呼叫通用最終處理函式,以便只維護一個版本的清除程式碼。 此外,如果物件的語意讓 Close 方法比 Dispose 方法更邏輯,則也應該實作 Close ;在此情況下,資料庫連接或通訊端會以邏輯方式「關閉」。 Close可以直接呼叫Dispose方法。

最好為具有完成項的類別提供 Dispose 方法;一個絕對無法確定該類別的使用方式,例如,其存留期是否明確已知。 如果您使用的類別會實作 Dispose 模式,而且您明確知道何時使用 物件完成,最絕對會呼叫 Dispose

提示 為所有可完成的類別提供 Dispose 方法。

提示 隱藏 Dispose 方法中的最終處理。

提示 呼叫一般清除函式。

提示 如果您使用的物件會實作 IDisposable,而且您知道不再需要物件,請呼叫 Dispose。

C# 提供非常方便的方式來自動處置物件。 關鍵字 using 可讓您識別程式碼區塊,之後會呼叫 Dispose 的一些可處置物件。

C# 的 using 關鍵字

using(DisposableType T)
{
   //Do some work with T
}
//T.Dispose() is called automatically

弱式參考的附注

在堆疊、暫存器、另一個物件或另一個 GC 根目錄中的物件的任何參考,都會在 GC 期間讓物件保持運作。 這通常是很好的事,考慮它通常表示您的應用程式不是使用該物件來完成。 不過,在某些情況下,您想要有物件的參考,但不想影響其存留期。 在這些情況下,CLR 提供稱為弱式參考的機制,只執行此動作。 例如,任何強式參考,根目錄物件的參考都可以轉換成弱式參考。 當您想要使用弱式參考的範例是,當您想要建立可周遊資料結構但不會影響物件的存留期的外部資料指標物件時。 另一個範例是,如果您想要建立記憶體壓力時排清的快取;例如,當 GC 發生時。

在 C 中建立弱式參考#

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

//Create weak reference
WeakReference wr = new WeakReference(mrt); 
mrt = null; //object is no longer rooted
//...

//Has object been collected?
if(wr.IsAlive)
{
   //Get a strong reference to the object
   mrt = wr.Target;
   //object is rooted and can be used again
}
else
{
   //recreate the object
   mrt = new MyRefType();
}

Managed 程式碼和 CLR JIT

Managed 元件是 Managed 程式碼的散發單位,包含稱為 Microsoft Intermediate Language (MSIL 或 IL) 的處理器獨立語言。 CLR Just-In-Time (JIT) 會將 IL 編譯為優化的原生 X86 指令。 JIT 是優化編譯器,但因為編譯會在執行時間發生,而且只有第一次呼叫方法時,它所執行的優化數目必須與執行編譯所需的時間進行平衡。 這通常對伺服器應用程式而言並不重要,因為啟動時間和回應性通常不是問題,但對於用戶端應用程式而言非常重要。 請注意,使用 NGEN.exe 在安裝時間執行編譯,即可改善啟動時間。

JIT 所完成的許多優化沒有與其相關聯的程式設計模式,例如,您無法明確撰寫它們的程式碼,但有一些可以執行的程式碼。 下一節將討論其中一些優化。

提示 使用 NGEN.exe 公用程式在安裝時間編譯應用程式,以改善用戶端應用程式的啟動時間。

方法內嵌

有與方法呼叫相關聯的成本;引數必須在堆疊上推送,或儲存在暫存器中,必須執行方法 Prolog 和 epilog,依此類推。 只要將呼叫之方法的方法主體移至呼叫端的主體,即可避免某些方法的成本。 這稱為方法內建。 JIT 會使用一些啟發學習法來決定方法是否應該內嵌。 以下是這些 (更重要的清單,請注意,這不是詳盡) :

  • 大於 32 個位元組 IL 的方法將不會內嵌。
  • 虛擬函式不會內嵌。
  • 具有複雜流程式控制制的方法將不會內嵌。 複雜流程式控制制是在此案例中以外的 if/then/else; 任何流程式控制制, switchwhile
  • 包含例外狀況處理區塊的方法不會內嵌,不過擲回例外狀況的方法仍適合內嵌。
  • 如果方法的任何正式引數都是結構,則不會內嵌方法。

我會仔細考慮針對這些啟發學習法明確撰寫程式碼,因為它們可能會在未來的 JIT 版本中變更。 請勿危害方法的正確性,以確保其會內嵌。 請注意, inline C++ 中的 和 __inline 關鍵字並不保證編譯器會將方法內嵌 (不過 __forceinline) 。

屬性取得和設定方法通常是適合內嵌的候選項目,因為它們通常是初始化私用資料成員。

**HINT **在嘗試保證內嵌時,不要危害方法的正確性。

範圍檢查刪除

Managed 程式碼的許多優點之一是自動範圍檢查;每次您使用 array[index] 語意存取陣列時,JIT 會發出檢查以確定索引位於陣列的界限。 在迴圈的內容中,具有大量反復專案,以及每個反復專案執行的少量指令,這些範圍檢查可能會很昂貴。 在某些情況下,JIT 會偵測到這些範圍檢查是不必要的,而且會消除迴圈主體中的檢查,只在迴圈執行開始之前檢查一次。 在 C# 中,有一個程式設計模式可確保排除這些範圍檢查:明確測試 「for」 語句中陣列的長度。 請注意,此模式的細微偏差會導致檢查不會遭到排除,在此情況下,將值新增至索引。

C 中的範圍檢查刪除#

//Range check will be eliminated
for(int i = 0; i < myArray.Length; i++) 
{
   Console.WriteLine(myArray[i].ToString());
}

//Range check will NOT be eliminated
for(int i = 0; i < myArray.Length + y; i++) 
{ 
   Console.WriteLine(myArray[i+x].ToString());
}

例如,搜尋大型不規則陣列時,優化特別明顯,因為會消除內部迴圈和外部迴圈的範圍檢查。

需要變數使用量追蹤的優化

一些 JIT 編譯程式優化需要 JIT 追蹤正式引數和區域變數的使用方式;例如,當第一次使用時,以及最後一次在 方法主體中使用時。 在 CLR 1.0 和 1.1 版中,JIT 將追蹤使用量的變數總數有 64 項限制。 需要使用量追蹤的優化範例是 Enregistration。 列舉是當變數儲存在處理器暫存器中,而不是堆疊框架上的變數時,例如在 RAM 中。 即使框架上的變數發生于處理器快取中,Enregistered 變數的存取速度會明顯比在堆疊框架上還要快。 只有 64 個變數會考慮進行註冊;所有其他變數都會推送在堆疊上。 除了 Enregistration 以外,還有其他優化取決於使用量追蹤。 方法的正式引數和區域變數數目應保留低於 64,以確保 JIT 優化的最大數目。 請記住,此數位可能會變更為未來的 CLR 版本。

提示 讓方法保持簡短。 有一些原因,包括方法內嵌、登錄和 JIT 持續時間。

其他 JIT 優化

JIT 編譯程式會執行一些其他優化:常數和複製傳播、迴圈不變異擷取,以及其他數個優化。 您不需要使用明確的程式設計模式來取得這些優化;免費。

為什麼我在 Visual Studio 中看不到這些優化?

當您從 [偵錯] 功能表使用 [開始] 或按 F5 在 Visual Studio 中啟動應用程式時,不論您已建置發行或偵錯版本,都會停用所有 JIT 優化。 當受管理的應用程式由偵錯工具啟動時,即使它不是應用程式的偵錯組建,JIT 也會發出非優化的 x86 指令。 如果您想要讓 JIT 發出優化的程式碼,請從 Windows 檔案總管啟動應用程式,或使用 Visual Studio 中的 CTRL+F5。 如果您想要檢視優化的反組解碼,並將其與非優化程式碼對比,您可以使用 cordbg.exe。

提示 使用cordbg.exe來查看 JIT 所發出之優化和非優化程式碼的反組解碼。 使用 cordbg.exe 啟動應用程式之後,您可以輸入下列命令來設定 JIT 模式:

(cordbg) mode JitOptimizations 1
JIT's will produce optimized code

(cordbg) mode JitOptimizations 0

JIT 會產生可偵錯 (非優化) 程式碼。

實值類型

CLR 會公開兩組不同的類型:參考型別和實值型別。 參考類型一律會配置在 Managed 堆積上,並以參考 (傳遞,因為名稱表示) 。 實數值型別會配置在堆疊上或內嵌為堆積上物件的一部分,而且預設會依值傳遞,不過您也可以依參考方式傳遞它們。 實值型別配置非常便宜,而且假設它們保持小且簡單,所以傳遞為引數很便宜。 適當使用實值型別的良好範例是包含 xy 座標的 Point 實值型別。

點數值型別

struct Point
{
   public int x;
   public int y;
   
   //
}

實值型別也可以視為物件;例如,可以在物件上呼叫物件方法、它們可以轉換成物件,或傳遞物件預期的位置。 不過,發生這種情況時,實值型別會透過稱為 Boxing 的程式轉換成參考型別。 當實數值型別為 Boxed 時,會在 Managed 堆積上配置新的 物件,並將值複製到新的 物件中。 這是成本高昂的作業,而且可以使用實值型別來減少或完全否定所得到的效能。 當 Boxed 類型是隱含或明確轉換回實數值型別時,它會是 Unboxed。

Box/Unbox 實數值型別

C#:

int BoxUnboxValueType()
{
   int i = 10;
   object o = (object)i; //i is Boxed
   return (int)o + 3; //i is Unboxed
}

Msil:

.method private hidebysig instance int32
        BoxUnboxValueType() cil managed
{
  // Code size       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
} // end of method Class1::BoxUnboxValueType

如果您在 C#) 中實作自訂實數值型別 (結構,您應該考慮覆寫 ToString 方法。 如果您未覆寫此方法,請在實數值型別上呼叫 ToString 會導致類型成為 Boxed。 這也適用于繼承自 System.Object的其他方法,在此情況下為 Equals,不過 ToString 可能是最常見的方法。 如果您想要知道實數值型別是否為 Boxed,您可以使用 ildasm.exe 公用程式 (來尋找 box MSIL 中的指示,如上述程式碼片段所示) 。

覆寫 C# 中的 ToString () 方法,以防止 Boxing

struct Point
{
   public int x;
   public int y;

   //This will prevent type being boxed when ToString is called
   public override string ToString()
   {
      return x.ToString() + "," + y.ToString();
   }
}

請注意,建立集合時,例如 float 的 ArrayList,每個專案都會在新增至集合時 Boxed。 您應該考慮使用陣列,或為您的實值型別建立自訂集合類別。

在 C 中使用集合類別時,隱含 Boxing#

ArrayList al = new ArrayList();
al.Add(42.0F); //Implicitly Boxed becuase Add() takes object
float f = (float)al[0]; //Unboxed

例外狀況處理

常見的做法是使用錯誤狀況作為一般流程式控制制。 在此情況下,嘗試以程式設計方式將使用者新增至 Active Directory 實例時,您可以直接嘗試新增使用者,而且如果傳回E_ADS_OBJECT_EXISTS HRESULT,您就會知道他們已存在於目錄中。 或者,您可以搜尋使用者的目錄,然後在搜尋失敗時只新增使用者。

這種對一般流程式控制制的錯誤用法是 CLR 內容中的效能反模式。 CLR 中的錯誤處理是透過結構化例外狀況處理來完成。 在擲回受控例外狀況之前,受控例外狀況非常便宜。 在 CLR 中,擲回例外狀況時,需要堆疊逐步解說,才能尋找所擲回例外狀況的適當例外狀況處理常式。 堆疊逐步執行是昂貴的作業。 例外狀況應該作為其名稱使用;在例外狀況或非預期的情況下。

**HINT **請考慮傳回預期結果的列舉結果,而不是針對效能關鍵方法擲回例外狀況。

**HINT **有許多 .NET CLR 例外狀況效能計數器會告訴您應用程式中擲回多少個例外狀況。

**HINT **如果您使用 VB.NET 使用例外狀況,而不是 On Error Goto ;錯誤物件是不必要的成本。

執行緒和同步處理

CLR 會公開豐富的執行緒和同步處理功能,包括能夠建立您自己的執行緒、執行緒集區和各種同步處理基本類型。 在利用 CLR 中的執行緒支援之前,您應該仔細考慮使用執行緒。 請記住,新增執行緒實際上可以減少輸送量,而不是增加輸送量,而且您可以確定它會增加記憶體使用率。 在即將在多處理器機器上執行的伺服器應用程式中,新增執行緒可以藉由平行化執行 (來大幅改善輸送量,不過它確實取決於鎖定爭用的程度,例如,序列化執行) ,以及在用戶端應用程式中,新增執行緒來顯示活動和/或進度,可改善小型輸送量成本 (的效能) 。

如果您的應用程式中的執行緒不是針對特定工作特製化,或有與其相關聯的特殊狀態,您應該考慮使用執行緒集區。 如果您在過去使用 Win32 執行緒集區,CLR 的執行緒集區會非常熟悉。 每個受控進程都有線程集區的單一實例。 執行緒集區對於所建立的執行緒數目很聰明,而且會根據電腦上的負載自行調整。

在討論同步處理的情況下,無法討論執行緒;多執行緒可提供應用程式的所有輸送量提升,都可以由寫入錯誤的同步處理邏輯來否定。 鎖定的資料細微性可能會大幅影響應用程式的整體輸送量,因為建立和管理鎖定的成本,以及鎖定可能會序列化執行的事實。 我將使用嘗試將節點新增至樹狀結構的範例,以說明這一點。 例如,如果樹狀結構是共用資料結構,則多個執行緒在執行應用程式期間需要存取它,而且您必須同步處理樹狀結構的存取權。 您可以選擇在新增節點時鎖定整個樹狀結構,這表示您只會產生建立單一鎖定的成本,但嘗試存取樹狀結構的其他執行緒可能會封鎖。 這是粗略鎖定的範例。 或者,當您周遊樹狀結構時,您可以鎖定每個節點,這表示您在建立每個節點的鎖定時會產生成本,但除非其他執行緒嘗試存取您已鎖定的特定節點,否則不會封鎖這些節點。 這是細部鎖定的範例。 鎖定的更適當細微性可能是只鎖定您正在操作的子樹狀結構。 請注意,在此範例中,您可能會使用共用鎖定 (RWLock) ,因為多個讀取器應該能夠同時取得存取權。

執行同步處理作業的最簡單和最高效能方式是使用 System.Threading.Interlocked 類別。 Interlocked 類別會公開一些低階不可部分完成作業: IncrementDecrementExchangeCompareExchange

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

using System.Threading;
//...
public class MyClass
{
   void MyClass() //Constructor
   {
      //Increment a global instance counter atomically
      Interlocked.Increment(ref MyClassInstanceCounter);
   }

   ~MyClass() //Finalizer
   {
      //Decrement a global instance counter atomically
      Interlocked.Decrement(ref MyClassInstanceCounter);
      //... 
   }
   //...
}

最常見的同步處理機制可能是 [監視] 或 [重大] 區段。 您可以直接使用監視器鎖定,或在 C# 中使用 lock 關鍵字。 關鍵字 lock 會將指定物件的存取同步處理至特定程式碼區塊。 從效能觀點來看,相當容易競爭的監視器鎖定相對便宜,但如果高度競爭,就會變得更昂貴。

C# lock 關鍵字

//Thread will attempt to obtain the lock
//and block until it does
lock(mySharedObject)
{
   //A thread will only be able to execute the code
   //within this block if it holds the lock
}//Thread releases the lock

RWLock 提供共用鎖定機制:例如,「讀取器」可以與其他「讀取器」共用鎖定,但「寫入器」無法共用。 在適用的情況下,RWLock 會產生比使用監視器更好的輸送量,這只允許單一讀取器或寫入器一次取得鎖定。 System.Threading 命名空間也包含 Mutex 類別。 Mutex 是允許跨進程同步處理的同步處理基本類型。 請注意,這比重要區段高很多,而且應該只在需要跨進程同步處理的情況下使用。

反映

反映是由 CLR 提供的機制,可讓您在執行時間以程式設計方式取得類型資訊。 反映高度相依于內嵌于 Managed 元件的中繼資料。 許多反映 API 都需要搜尋和剖析中繼資料,這是昂貴的作業。

反映 API 可以分組為三個效能貯體;類型比較、成員列舉和成員調用。 這其中每一個貯體的成本會逐漸增加。 類型比較作業—在此案例中,C# 中的 typeofGetTypeisIsInstanceOfType 等等,是反映 API 的最便宜,雖然它們不算便宜。 成員列舉可讓您以程式設計方式檢查類別的方法、屬性、欄位、事件、建構函式等等。 這些可能用於設計階段案例的範例,在此案例中,列舉 Visual Studio 中屬性瀏覽器之「屬性瀏覽器」的屬性。 反映 API 成本最高的是可讓您動態叫用類別的成員,或動態發出 JIT 並執行方法的 API。 在某些情況下,需要動態載入元件、類型具現化和方法調用,但這種鬆散結合需要明確的效能取捨。 一般而言,應該避免效能敏感程式碼路徑中的反映 API。 請注意,雖然您未直接使用反映,但您使用的 API 可能會使用它。 因此,也請注意反映 API 的可轉移用法。

晚期繫結

晚期繫結呼叫是使用反映的功能範例。 Visual Basic.NET 和 JScript.NET 都支援晚期繫結呼叫。 例如,您不需要在變數使用之前宣告變數。 晚期繫結物件實際上是類型物件,而 Reflection 是用來在執行時間將物件轉換成正確的類型。 晚期繫結呼叫比直接呼叫慢一些。 除非您特別需要晚期繫結行為,否則您應該避免其在效能關鍵性程式碼路徑中使用。

提示 如果您使用 VB.NET,而且不需要晚期繫結,您可以指示編譯器藉由在來源檔案頂端包含 Option Explicit OnOption Strict On 來禁止它。 這些選項會強制宣告變數並強型別,並關閉隱含轉型。

安全性

安全性是 CLR 的必要和不可或缺的一部分,而且具有與其相關聯的效能成本。 如果程式碼為完全信任,且安全性原則是預設值,安全性應該會對應用程式的輸送量和啟動時間產生次要影響。 部分信任的程式碼—例如,來自網際網路或內部網路區域的程式碼,或縮小 MyComputer 授與集,會增加安全性的效能成本。

COM Interop 和平台叫用

COM Interop 和 Platform Invoke 以幾乎透明的方式向 Managed 程式碼公開原生 API;呼叫大部分原生 API 通常不需要任何特殊程式碼,但可能需要按幾下滑鼠。 如您所預期,從 Managed 程式碼呼叫機器碼會產生相關成本,反之亦然。 此成本有兩個元件:與在原生和 Managed 程式碼之間執行轉換相關聯的固定成本,以及與任何引數封送處理相關聯的變數成本,以及可能需要傳回值。 COM Interop 和 P/Invoke 成本的固定貢獻很小:通常少於 50 個指示。 封送處理到 Managed 型別和從 Managed 型別傳出的成本將取決於界限任一端的標記法有何不同。 需要大量轉換的類型會比較昂貴。 例如,CLR 中的所有字串都是 Unicode 字串。 如果您要透過預期 ANSI 字元陣列的 P/Invoke 呼叫 WIN32 API,則必須縮小字串中的每個字元。 不過,如果要在預期原生整數陣列的情況下傳遞 Managed 整數陣列,則不需要封送處理。

因為有與呼叫機器碼相關聯的效能成本,所以您應該確定成本合理。 如果您要進行原生呼叫,請確定原生呼叫確實符合與進行呼叫相關聯的效能成本的工作,讓方法保持「區塊化」而非「聊天」。測量原生呼叫成本的好方法是測量原生方法的效能,該原生方法不採用任何引數且沒有傳回值,然後測量您想要呼叫之原生方法的效能。 差異可讓您指出封送處理成本。

提示 進行「Chunky」 COM Interop 和 P/Invoke 呼叫,而不是「聊天」呼叫,並確定進行呼叫的成本與呼叫的工作量對齊。

請注意,沒有任何與 Managed 執行緒相關聯的執行緒模型。 當您要進行 COM Interop 呼叫時,您必須確定將呼叫所在的執行緒初始化為正確的 COM 執行緒模型。 這通常是使用 MTAThreadAttribute 和 STAThreadAttribute (完成,但也可以透過程式設計方式) 完成。

效能計數器

.NET CLR 會公開一些 Windows 效能計數器。 當第一次診斷效能問題或嘗試識別受控應用程式的效能特性時,這些效能計數器應該是開發人員的選擇。 我已經提到一些與記憶體管理和例外狀況相關的計數器。 CLR 和.NET Framework幾乎每個層面都有效能計數器。 這些效能計數器一律可供使用,而且不具入侵性;它們具有低額外負荷,而且不會變更應用程式的效能特性。

其他工具

除了效能計數器和 CLR Profiler 以外,您會想要使用傳統分析工具來確定應用程式中哪些方法花費最多時間且最常呼叫。 這些會是您先優化的方法。 有一些商業分析工具可支援 Managed 程式碼,包括 Compuware 的 DevPartner Studio Professional Edition 7.0 和 Intel® 效能分析器 7.0 的 VTune™。 Compuware 也會針對稱為 DevPartner Profiler Community Edition 的 Managed 程式碼產生免費的分析工具。

結論

本文只是從效能觀點開始檢查 CLR 和.NET Framework。 CLR 架構的許多其他層面,以及會影響應用程式效能的.NET Framework。 我可以提供給任何開發人員的最佳指引,就是不要對應用程式的目標平臺效能進行任何假設,以及您使用的 API。 測量所有專案!

快樂的雜亂。

資源