前幾天,幫同事追查 .NET 程式 CPU 衝高問題,才發現 Visual Studio 2013 效能分析工具真是威力強大,特筆記備忘順便分享。原本想拿實務案例說明,但考量太多無關細節會失焦,所以我弄了一個簡單程式當靶機練習射擊:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace CPUSharMon
{
publicclass CPUHeater
{
publicenum JobTypes {
Light, Medium, High, Crazy
}
staticvoid RunSHA()
{
Guid g = Guid.NewGuid();
SHA512 sha512 = new SHA512CryptoServiceProvider();
sha512.ComputeHash(g.ToByteArray());
}
static Tuple<string, string> GenDESKey()
{
string rndStr = Guid.NewGuid().ToString();
returnnew Tuple<string, string>(
rndStr.Substring(0, 8), rndStr.Substring(8, 8));
}
staticbyte[] Encrypt(DESCryptoServiceProvider des, byte[] data)
{
var desencrypt = des.CreateEncryptor();
return desencrypt.TransformFinalBlock(data, 0, data.Length);
}
staticvoid RunDES()
{
var keyIv = GenDESKey();
DESCryptoServiceProvider des = new DESCryptoServiceProvider();
des.Key = Encoding.ASCII.GetBytes(keyIv.Item1);
des.IV = Encoding.ASCII.GetBytes(keyIv.Item2);
var data = newbyte[4096];
var enc = Encrypt(des, data);
}
staticvoid RunCrazyLoop()
{
for (var i = 0; i < 10000; i++)
{
Guid.NewGuid();
}
}
publicstaticvoid RunJob(JobTypes type, int times)
{
int j = 0;
for (int i = 0; i < times; i++)
{
switch (type)
{
case JobTypes.Light:
Random rnd = new Random();
j = i + rnd.Next(i);
break;
case JobTypes.Medium:
RunSHA();
break;
case JobTypes.High:
RunDES();
break;
case JobTypes.Crazy:
RunCrazyLoop();
break;
}
}
}
publicstaticvoid RunComplexJob()
{
RunJob(JobTypes.High, 80000);
RunJob(JobTypes.Light, 120000);
RunJob(JobTypes.Crazy, 6000);
RunJob(JobTypes.Medium, 30000);
}
}
}
我寫了一個 Console Application 程式,核心邏輯放在 CPUHeater 類別,RunJob() 方法提供四種粗重程度不一(但毫無營養)的工作:亂數加計算、SHA512 雜湊計算、DES 加密,以及狂跑迴圈生 GUID。(所以程式名稱叫 CPUSharMon ,「瞎忙」無誤)最後,另外宣告 RunComplexJob() 循序執行四種工作。
在八核 i7 執行,耗時約 20 秒,CPU 最高衝到 13%,等於把單核吃到 100%。接著我們就來用 VS2013 找出誰是把 CPU 衝高的兇手。
從選單列 ANALYZE/Performance and Diagnostics 開啟精靈:
選取分析對象:
有四個分析選項:CPU 取樣、Instrumentation(指令執行次數及耗時)、記憶體配置及執行緒等待狀況,這次的案例鎖定 CPU,故選第一個:
選取專案:(亦支援EXE程式及ASP.NET/JavaScript程式)
按下完成,VS2013 便啟動 CPUSharMon 程式並開始蒐集數據:
程式執行完會自動產出報告,但也可在觀察到特定 CPU 暴衝後手動停止。資料處理需要一點時間,接著檢測報告就出爐了:
先說明 Sample Profiling 偵測效能瓶頸原理:觀測程式每隔固定週期中斷 CPU,取回 Callstack 資料,藉此掌握程式當行執行的段並進行統計。由於取樣間隔固定,若每次取樣時常停在特定函式上,便可推斷大部分的 CPU 時間都耗在該函式,一般就是耗用 CPU 較多的運算來源。
我們的展示程式很單純,從彙總報告就能一眼看出效能瓶頸,Hot Path 列出了RunDES() 及 RunCrazyLoop() 出現在總取樣次數的比例最高,分別為 52.73% 及 41.09%,可想成整個執行期間有一半花在 RunDES,近一半花在 RunCrazyLoop。而下方則列出在取樣記錄出現次數最多的個別函式,由 RunCrzayLoop 中的 Guid.NewGuid 及 RunDES 中的加密程序分獲冠亞軍。
報告是互動式,點選函式名稱可以進一步剖析追查兇手,例如點選 HotPath 的第二項 RunComplexJob():
上方可看出 RunComplexJob 所有時間都耗在 RunJob 函式上(Call了四次,參數不同),而下方 Function Code View 會以數字及顏色標示各程序所佔比例,比例愈高愈紅,接著我們點選上方三個Called funtions 最右邊的 RunJob:
報表將再針對 RunJob 展開,上方的藍色區塊圖及下方程式碼都能看出耗用 CPU 較多的函式,再選點上方藍色區塊的 RunDES:
再點選藍色區塊的 Encrypt:
最後抓出 CreateEncryptor()、TransformFinalBlock() 分佔總取樣次數的 13.6% 及 36.4%,到這一步已觸及 .NET Framework 的範圍,不能也不需要再深入。(除非你要改寫 .NET Framework XD)
除了上述的 Function Detail 檢視,報表還提供其他檢視,其中 Call Tree 也蠻好用的。
在 Call Tree 表格中,程式函式的從屬關聯一目瞭然,可透過樹狀結構展開追兇,操作效率更高。而圖中紅字數字標出兩個好用功能:1)火焰圖示用來標示 Hot Path 2)齒輪符號可過濾雜訊,忽略 CPU 佔用不多的項目。另外還有自訂條件過濾等功能,函式太多時很好用!
找到疑犯,按右鍵可以快速切到其他檢視及相關函式,繼續深入調查:
另外,分析工具還提供多次觀測結果比較,像減肥廣告一樣做出「使用前」vs「使用後」的對照表,修改程式調校效能時很有用。
又到呼口號時間:Visual Studio 好威啊!
延伸閱讀:Analyzing Application Performance by Using Profiling Tools