本文章是由機器翻譯。

.NET 性能

使用 ETW 對 .NET 應用程式進行性能診斷

Subramanian Ramaswamy

下載代碼示例

您編寫一個託管應用程式來親身體驗一下 – 您會覺得其速度較慢。您的應用程式在功能上是正常的,但其性能有很多不足。您想診斷出性能問題並解決這些問題,但您的應用程式正在生產環境中運行,導致您無法安裝探查器或中斷它。或者,您應用程式的使用範圍可能不夠廣泛,無法證明購買 Visual Studio 探查器來進行 CPU 分析是合理的。

幸運的是,Windows 事件跟蹤 (ETW) 可以緩解這些問題。此強大的日誌記錄技術內置在 Windows 基礎結構的多個部分中,並且 Microsoft .NET Framework 4 CLR 中利用此技術使分析您的託管應用程式變得更加簡單。ETW 收集系統範圍的資料並分析所有資源(CPU、磁片、網路和記憶體),使其對獲取整體視圖很有用。此外,可對 ETW 生態系統進行調整以減少其開銷,使該系統適用于生產診斷。

本文旨在使您瞭解使用 ETW 分析託管應用程式的好處。我不會介紹所有內容 - 有幾個可用於診斷的 OS 事件和 CLR ETW 事件在本文中不會提到。但您將瞭解如何使用 ETW 生態系統大大提高託管應用程式的性能和功能。為了開始向您介紹針對託管代碼的基於 ETW 的診斷,我將使用可從 bcl.codeplex.com/releases/view/49601 下載的免費 ETW 工具 (PerfMonitor) 來演示示例調查。

PerfMonitor

利用 PerfMonitor,您可以方便快捷地收集 ETW 性能資料並生成有用報告。此工具不是為了替代深層分析工具(如 Visual Studio 探查器),而是為了向您提供應用程式性能特徵的概述,並讓您執行一些快速分析。

還有一種稱作 XPerf 的 ETW 診斷工具,可通過 Windows 性能工具包免費獲得此工具。雖然 XPerf 很適合用來在 Windows 上進行本機代碼分析,但它尚無法深入支援託管代碼分析。另一方面,PerfMonitor 公開了使用 ETW 分析託管代碼的範圍和能力。PerfMonitor 能夠收集與 .NET Framework 運行時代碼關聯的符號資訊,從而使它對 .NET Framework 性能調查很有價值,儘管它不支援 XPerf 可提供的深層分析。

PerfMonitor 是一個完全獨立的工具,您只需使用它即可開始對託管應用程式進行分析和診斷。唯一的要求是,您至少必須運行 Windows Vista 或 Windows Server 2008。PerfMonitor 是一個命令列工具,從其位置鍵入 PerfMonitor.exe usersGuide 將顯示一個概述。如果您想在操作環境下(如在生產伺服器上)診斷您客戶的程式,則您需要做的是將相應檔複製到該電腦上,並準備開始收集設定檔。如果需要,可離線分析設定檔。

在任何性能調查過程中,通常會檢查四個因素:CPU、磁片 I/O、記憶體和可伸縮性。大多數調查都將從 CPU 開始,CPU 將影回應用程式的啟動和執行時間。在診斷較長的啟動時間時,檢查磁片 I/O 是最有用的(磁片 I/O 是冷開機時間的主要因素,冷開機時間是指記憶體中沒有某個應用程式時(如重啟後)啟動此應用程式所花的時間),而過多的記憶體消耗(或洩露)可能會導致應用程式隨時間的流逝變得更慢。如果您希望您的應用程式的輸送量與處理器數成比例,則可伸縮性很關鍵。

PerfMonitor 可説明您獲取除可伸縮性以外的所有這些因素的快照,並且可為您提供足夠多的資訊以便您使用其他專業工具進行進一步的探究。例如,若要診斷與 CLR .NET 垃圾收集 (GC) 堆相關的問題,使用 CLRProfiler 將是更好的選擇。但是,PerfMonitor 很快會告知您是否存在問題,以及您是否需要使用其他工具進行進一步的探究。在某些情況下,PerfMonitor 自身會指出該問題,並包含您解決性能 Bug 所需的所有資訊,很快將向您介紹這一點。請查看“CLR 全面透析”專欄“.NET 應用程式的記憶體使用量審核”(msdn.microsoft.com/magazine/dd882521),其中討論了審核程式記憶體使用量並規劃性能的重要性。將這個原理擴展一下,您可利用 PerfMonitor 快速審核託管程式的多個方面的性能,而不僅僅是記憶體。

示例調查:CsvToXml

我使用 ETW 診斷的示常式序可將一個 CSV 檔轉換成一個 XML 檔。可從 code.msdn.microsoft.com/mag201012ETW 獲得原始程式碼和解決方案包(以及示例輸入 CSV 檔 data.csv)。若要執行程式,請運行命令 CsvToXml.exe data.csv output.xml。

與許多程式類似,CsvToXml 已迅速連在一起,開發人員從未期望將它用於大型 CSV 檔。當我開始在現實領域中使用該程式時,我發現它太慢了。它處理一個 750K 的檔所花的時間竟然超過了 15 秒!我知道出現了問題,但沒有分析工具,我也只能猜測此情況是因為運行速度慢導致的。(您能否只看一下原始程式碼就找出問題呢?)幸運地是,PerfMonitor 可以説明您找出該問題。

生成和查看程式跟蹤

第一步是,通過在管理員命令提示視窗中執行以下命令來快速審核應用程式(ETW 將收集電腦範圍內的資料,因此需要管理許可權):

PerfMonitor runAnalyze CsvToXml.exe data.csv out.xml

這將開始 ETW 日誌記錄、啟動 CsvToXml.exe、等待 CsvToXml 完成、停止日誌記錄,並最終呈現一個顯示對 CsvToXml 的分析資訊的網頁。通過一個簡單步驟,您便能夠擁有大量資料,這些資料可説明您揭示 CsvToXml 中的性能瓶頸。

圖 1 中捕獲了該命令的結果。此頁包含進程 ID、使用的命令列和高級性能資料的細目(其中包括 CPU 統計資訊、GC 統計資訊和即時 (JIT) 統計資訊),以及其他資料。PerfMonitor 還通過指向資訊性文章或其他工具的有用連結,來提供對開始診斷的位置的第一級分析。

圖 1 針對 CsvToXml 的性能分析

此報告說明,格式轉換所花費的時間將近 14 秒,在平均利用率為 99% 的 CPU 中花費的時間為 13.6 秒。因此,該方案與 CPU 性能聯繫緊密。

GC 中的時間總量和 GC 暫停時間量較少,這很好;但最大 GC 分配速率為 105.1MB/秒,這個速率過快了 - 這需要進一步的調查。

CPU 分析

詳細的 CPU 分析可提供 CPU 時間的細目,如圖 2 所示,還可以通過三種方式來讀取 CPU 設定檔資料。可以通過自下而上的視圖快速獲知,哪些方法佔用的 CPU 時間最多,應先對此進行診斷。自上而下的視圖可用於確定您的代碼是需要體系結構更改還是結構更改,並説明您瞭解程式的整體性能。調用方-被調用方視圖指示了各個方法之間的關係 - 例如,調用方法與被調用方法之間的對應關係。

圖 2 針對 CsvToXml.exe 的自下而上的分析

與其他 CPU 探查器類似,PerfMonitor 視圖為您提供了包含時間(特定方法使用的時間,包括其被調用方使用的時間)和排除時間(特定方法使用的時間,不包括其被調用方使用的時間)。當包含時間與排除時間相等時,將在特定方法內完成工作。PerfMonitor 還提供了 CPU 利用率圖,該圖對特定方法隨時間變化的 CPU 使用率進行了細分。通過將滑鼠指標懸停在報告中的列標題上方,可為您提供有關其含義的更多詳細資訊。

大多數性能調查都將從自下而上的視圖開始,該視圖是一個按排除時間劃分的方法清單(圖 2 中顯示了該視圖)。通過選擇自下而上的視圖,您會看到 mscorlib 方法 System.IO.File.OpenText 是佔用 CPU 最多的方法。按一下該連結將顯示 OpenText 方法的調用方/被調用方視圖,該視圖揭示了 CsvToXml.CsvFile.get_ColumnNames 方法正在從程式中調用 OpenText,而 get_ColumnNames 佔用的 CPU 時間約為 10 秒(圖 3)。此外,將在一個迴圈內從 CsvToXml.CsvFile.XmlElementForRow 調用該方法(XmlElementForRow 本身調用自 Main 方法)。

圖 3 get_ColumnNames 的調用方-被調用方視圖

因而,有些內容在這些方法中似乎是錯誤的。從這些方法中拉出代碼會導致出現問題,如圖 4 中突出顯示的部分所示:此檔在一個迴圈內反復打開和分析!

圖 4 方法 ColumnNames 由方法 XmlElementForRow 調用

public string[] ColumnNames{  get  {    using (var reader = File.OpenText(Filename))      return Parse(reader.ReadLine());  }}public string XmlElementForRow(string elementName, string[] row){  string ret = "<" + elementName;  for (int i = 0; i < row.Length; i++)    ret += " " + ToValidXmlName(ColumnNames[i]) + "=\"" + EscapeXml(row[i]) + "\"";  ret += "/>";  return ret;}

類似情況的發生頻率遠遠超出您的想像。 在最初編寫此方法時,開發人員可能認為此方法只會在極少數情況下被調用(與 ColumnNames 的情況相同),因此可能不會太多關注此方法的性能。 但是,以後經常會出現在迴圈中停止調用此方法的情況,導致應用程式的性能下降。

在 CSV 檔中,由於所有行的格式都相同,因此沒有必要每次都這樣做。 您可以將 ColumnNames 功能提升至構造函數中(如圖 5 所示),保留此屬性以提供緩存列名。 這可確保唯讀取此檔一次。

圖 5 緩存列名以獲取更好的性能

public CsvFile(string csvFileName){  Filename = csvFileName;    using (var reader = File.OpenText(Filename))      ColumnNames = Parse(reader.ReadLine());        }public string Filename { get; private set; }public string[] ColumnNames { get; private set;}

我們在重新生成後再次執行了上一個命令,發現應用程式的速度快多了;現在的持續時間僅為 2.5 秒。

但在使用修補程式審閱資料時,您會發現 CPU 時間仍占主導地位。 通過重新深入瞭解 CPU 時間並查看自下而上的分析,您會發現 Regex.Replace 現在是開銷最大的方法,並且此方法調用自 EscapeXml 和 ToValidXmlName。 由於 EscapeXml 是開銷最大的方法(排除時間為 330 毫秒),因此請檢查其原始程式碼:

private static string EscapeXml(string str){  str = Regex.Replace(str, "\"", "&quote;");  str = Regex.Replace(str, "<", "&lt;");  str = Regex.Replace(str, ">", "&gt;");  str = Regex.Replace(str, "&", "&amp;");  return str;}

由於還會在 XmlElementForRow 中的迴圈內調用 EscapeXml,因此此方法有可能會成為一個瓶頸。 規則運算式對這些替換有點多餘,而使用字串 Replace 方法應該會更高效。 將 EscapeXml 替換為以下內容:

private static string EscapeXml(string str){  str = str.Replace("\"", "&quote;");  str = str.Replace("<", "&lt;");  str = str.Replace(">", "&gt;");  str = str.Replace("&", "&amp;");  return str;}

利用此轉換後,時間總量已減少約 2 秒,且 CPU 時間仍占主導地位。 這是可接受的性能 - 您幾乎將執行速度加快了七倍。

為了便於讀者進行練習,我已在示常式序中保留了幾個性能 Bug,可使用 ETW 事件標識這些 Bug。

探究 GC 統計資訊

PerfMonitor GC 統計資訊提供了記憶體設定檔的簡要概述。 您可能記得,我強烈建議執行記憶體使用率審核,而通過 GC ETW 事件提供的資訊提供了有關 .NET GC 堆的任何問題的快照。 您可通過快速摘要視圖獲知 GC 聚合堆的大小、分配速率和 GC 暫停時間。 通過在 PerfMonitor 結果選項卡上選擇“GC 時間分析”連結,可顯示 GC 的詳細資訊、GC 發生的時間、GC 佔用的時間量等。

您可通過這些資訊來確定是否需要使用 CLRProfiler 或其他記憶體探查器進一步分析任何記憶體問題。 “.NET 垃圾回收堆透析”一文 (msdn.microsoft.com/magazine/ee309515) 對使用 CLRProfiler 調試 .NET GC 堆進行了深入的探討。

對於此特定程式而言,不存在任何令人不安的 GC 統計資訊。 分配速率較高;一個有效的經驗法則是讓分配速率低於 10MB/s。 但暫停時間非常短。 高的分配速率出現在 CPU 時間下麵,多數情況下這表示將獲得 CPU 增益 – 這與您發現的情況一樣。 但修復後的分配速率仍比較高,這表示進行了大量分配(您能糾正此問題嗎?)。 幾毫秒的 GC 暫停時間是對 .NET Framework 運行時提供的自調節和高效 GC 有力證明。 因此,.NET Framework GC 將自動負責記憶體管理。

探究 JIT 統計資訊

若要縮短啟動時間,要探究的第一批專案中的一個專案是對方法進行 JIT 編譯所需的時間。 如果花費的時間很長(例如,啟動應用程式所需的大部分時間都由 JIT 編譯所佔用),則應用程式可以從本機映射生成 (NGen) 中受益,它可通過對程式集進行預編譯並將其保存到磁片上來消除 JIT 編譯時間。 也就是說,對程式集進行 JIT 編譯並將其保存到磁片上,這樣便無需對後續執行進行 JIT 編譯。 在選擇採用 NGen 之前,您可能還需要考慮將要進行 JIT 編譯的一些方法推遲到程式中的某個時點執行,以便 JIT 編譯時間不會影響啟動。 有關詳細資訊,請參閱“NGen 的性能優勢”一文 (msdn.microsoft.com/magazine/cc163610)。

示例應用程式 CsvToXml.exe 的啟動成本並不高,因此允許它每次對所有方法進行 JIT 編譯是可行的。 JIT 編譯統計資訊還指明,已進行 JIT 編譯的方法的數目為 17(建議調用的所有方法都已進行 JIT 編譯),JIT 編譯時間的總量為 23 毫秒。 這些都不是與此應用程式相關的性能問題,但對於受 JIT 編譯時間影響的大型應用程式,使用 NGen 應會消除任何問題。 通常,當應用程式開始對數以百計或數以千計的方法進行 JIT 編譯時,JIT 編譯時間就會成為影響因素。 在此類情況下,NGen 是消除 JIT 編譯成本的解決方案。

MSDN 雜誌 中的其他文章中包含了有關改進啟動的更多指導資訊,而 ETW 事件可説明標識和解決瓶頸。 還提供了其他幾個 JIT 事件(包括 JIT 內聯事件),它們能提供對方法無法內聯的原因的深入分析。

.NET Framework 4 中的 CLR ETW 事件

CLR 團隊撰寫了一篇有關跟蹤 DLL 載入並確定啟動期間是否需要載入特定 DLL 的博客文章。 通過使用 ETW 事件,可使確定是否需要在啟動期間進行 DLL 載入的過程變得更加簡單。 通過使用 .NET Framework 4 中提供的 ETW 模組載入事件,我們可瞭解載入了哪些模組以及載入原因。 還有一些針對模組卸載等情況的事件。

.NET Framework 4 中還提供了幾個事件,利用這些事件可更輕鬆地診斷託管應用程式。 圖 6 總結了這些事件。 可使用 PerfMonitor runPrint 命令對執行期間觸發的所有事件進行轉儲。 CLR 團隊還會運行允許您連接和分離 ETW 分析的事件,並且該團隊打算繼續添加更多的 ETW 事件,以使調試託管應用程式的過程在將來版本中變得更加簡單。

圖 6 .NET Framework 4 中的 ETW 事件

事件類別名稱 說明
運行時資訊 ETW 事件 捕獲有關運行時的資訊,包括 SKU、版本號、啟動運行時的方式、啟動運行時所使用的命令列參數、GUID(如果適用)以及其他相關資訊。
異常引發 ETW 事件 捕獲有關引發的異常的資訊。
爭用 ETW 事件 捕獲有關對運行時使用的監控視器鎖或本機鎖的爭用情況的資訊。
執行緒池 ETW 事件 捕獲有關工作執行緒池和 I/O 執行緒池的資訊。
載入程式 ETW 事件 捕獲有關載入和卸載應用程式定義域、程式集和模組的資訊。
方法 ETW 事件 捕獲有關用於符號解析的 CLR 方法的資訊。
GC ETW 事件 捕獲有關 GC 的資訊。
JIT 跟蹤 ETW 事件 捕獲有關 JIT 內聯和尾調用的資訊。
交互操作 ETW 事件 捕獲有關 Microsoft 中間語言 (MSIL) 存根生成和緩存的資訊。
應用程式定義域資源監控 (ARM) ETW 事件 捕獲有關應用程式定義域的狀態的詳細診斷資訊。
安全性 ETW 事件 捕獲有關強式名稱和 Authenticode 驗證的資訊。
堆疊 ETW 事件 捕獲可用於其他事件以在引發事件後生成堆疊跟蹤的資訊。

您會發現執行目錄中有兩個尾碼為 PerfMonitorOutput 的檔;這兩個檔是 ETW 日誌檔。 您還會發現尾碼為 kernel 的檔,這表示它們包含 OS 事件。 PerfMonitor 收集的資料與 XPerf 使用的資料相同,因此您可以使用 PerfMonitor 來簡化資料收集,並簡化報告和 XPerf 以便對相同資料進行更高級的分析。 PerfMonitor 合併命令會將 ETW 檔轉換為 XPerf 的可讀格式。

總結

使用 ETW 進行性能調查不僅簡單而且很有效。 提供了多種免費的、低開銷的基於 ETW 的工具,這些工具允許有效調試託管代碼。 我剛剛介紹的只是 .NET Framework 運行時中提供的 ETW 事件的皮毛。 我的目標是,讓您開始使用 ETW 事件和工具調試託管應用程式。 通過下載 PerfMonitor、使用 CLR 中的 ETW 事件的 MSDN 文檔並閱讀 CLR Perf 博客,您可以快速開始對託管應用程式進行性能調查。

特別感謝 CLR Performance 的合作夥伴架構師 Vance Morrison 提供的指導以及為寫作本文提供的説明。

Subramanian Ramaswamy 是 Microsoft 的 CLR Performance 程式經理。 他擁有 佐治亞理工學院的電子與電腦工程博士學位。