【转】使用 Visual Studio Profiler 找出應用程式的瓶頸
Hari Pulapaka and Boris Vidolov
本文探討:
|
本文使用技術: Visual Studio 2008 |
過去十年來出現了許多新的軟體技術與平台。這些新技術全都需要專門的知識,才能用來建立高效能的應用程式。由於部落格等網際網路技術的出現,讓以往不滿的使用者很容易就能對您的應用程式散播不好的印象,因此您確實需要把效能列為第一優先。在規劃初期,您就應該考慮到回應速度,並建立可識別技術限制的原型。在整個開發過程中,您還要評量應用程式的不同效能因素,以找出可能的回復,並且確定測試人員記錄及追蹤執行速度較慢之案例的問題。
即使有詳盡的規劃,您可能還是要在產品開發過程中調查效能的問題。本文將介紹如何使用 Visual Studio®Team System Development Edition 或 Visual Studio Team Suite 找出應用程式的效能瓶頸。我們會逐步解說一個效能調查範例,來為您介紹 Visual Studio Profiler。請注意,雖然本文中的程式碼是使用 C# 撰寫,不過文中大部分的範例一樣也適用於原生 C/C++ 和 Visual Basic® 程式碼。
應用程式程式碼的剖析
為了合乎本文宗旨,我們將使用前述兩種 Visual Studio 版本所附的程式碼剖析工具。首先要解說的是一個小的範例專案,這個專案會繪製一個 Mandelbrot 碎形,如 [圖 1] 所示。這個應用程式的效率不是很高,繪製碎形大概要花 10 秒鐘。
Figure 1 效能測試的目標程式 (按影像可放大)
我們從 Visual Studio 2008 中新的 [分析] 功能表啟動 [效能精靈],來開始進行調查。在 Visual Studio 2005 中,這個功能可以從 [工具] | [效能工具] 功能表存取。這會啟動有三個步驟的精靈,它的第一步是要我們指定目標專案或網站。第二步則會提供兩種不同的程式碼剖析方法:取樣與檢測 (有關這些程式碼剖析方法的詳細資料,請參閱「效能剖析的解說」資訊看板)。我們選擇預設值即可。
精靈完成之後會顯示一個 [效能總管] 對話方塊,並建立一個新的效能工作階段。工作階段會包含目標應用程式 (在我們的例子裡是 Mandel),沒有報告。若要開始剖析,請按一下工具視窗工具列中的 [啟動並啟用程式碼剖析] 按鈕。
在應用程式繪製碎形後,我們立即關閉表單以停止程式碼剖析。Visual Studio 會自動在我們的效能工作階段加入新建立的報告,並開始分析報告。分析完成之後,Visual Studio Profiler 會顯示 [效能報告摘要],並列出最耗費資源的函式 (請參閱 [圖 2])。報告會以兩種方式顯示這些函式。第一種會測量列出之函式直接或間接執行的工作。每一個函式的數字代表在函式主體和它所有的子呼叫中收集到的累積樣本。第二個清單不會計算在子呼叫中收集到的樣本。摘要頁顯示 Visual Studio Profiler 在 DrawMandel 方法執行期間,收集了 30.71% 的樣本。其餘的 69% 樣本散佈在不同的函式中,未在此顯示。若要進一步了解報告選項,請參閱「報告視覺化選項」資訊看板。
Figure 2 效能測試顯示耗用較多資源的函式呼叫 (按影像可放大)
在報告的 [呼叫樹狀圖] 檢視中 (如 [圖 3] 所示),[內含樣本 %] 資料行代表在函式及其子系中收集到的樣本。[專有樣本 %] 資料行則代表只在函式主體中收集到的樣本。您可以看到 DrawMandel 方法直接呼叫 Bitmap.SetPixel。雖然 DrawMandel 本身只造成樣本總數的 30.71%,不過 Visual Studio Profiler 從 Bitmap.SetPixel 及其子系中收集到 64.54%。Bitmap.SetPixel 的主體只造成 0.68%,這也是它為何沒有在摘要頁面上顯示的原因。然而,其實 Bitmap.SetPixel 經由它的子系進行了大部分的處理。這才是應用程式的真正瓶頸。
Figure 3 受測試應用程式的呼叫樹狀圖樣本 (按影像可放大)
Bitmap.SetPixel 顯然並未在 Mandel 專案中最佳化。我們的應用程式需要更快的方法,來存取表單上所有的像素。幸好,Bitmap 類別還提供另一個實用的 API:Bitmap.LockBits。這個函式可以讓程式直接寫入點陣圖記憶體,因此可減少設定個別像素的額外負荷。此外,為了將繪製過程最佳化,我們要建立一個純整數陣列,並在陣列中填入每個像素的色彩值。然後我們要在單一作業中,將該陣列的值複製到點陣圖中。
最佳化應用程式
現在我們來修改 DrawMandel 方法,讓它使用 LockBits,而不要使用 SetPixel,再看看此一變更能提升多少效能。建立點陣圖之後,加入下列幾行以鎖定點陣圖位元,並取得到點陣圖記憶體的指標。
BitmapData bmpData =
bitmap.LockBits(
new Rectangle(0, 0, Width, Height),
ImageLockMode.ReadWrite,
bitmap.PixelFormat);
IntPtr ptr = bmpData.Scan0;
int pixels = bitmap.Width * bitmap.Height;
Int32[] rgbValues = new Int32[pixels];
然後在我們設定像素的內部迴圈中,為 Bitmap.SetPixel 的呼叫加上註解,並將它換成如下的新陳述式:
//bitmap.SetPixel(column, row, colors[color]);
rgbValues[row * Width + column] =
colors[color].ToArgb();
另外再加入下列幾行,將陣列複製到點陣圖記憶體中:
Marshal.Copy(rgbValues, 0, ptr, pixels); bitmap.UnlockBits(bmpData);
如此一來,如果我們在程式碼剖析工具下重新執行應用程式,就可以看到繪製碎形的速度幾乎快了三倍 (請參閱 [圖 4])。請注意新效能報告的摘要頁顯示,總樣本數的 83.66% 是 DrawMandel 的主體直接造成的。由於我們已將繪圖作業最佳化,因此瓶頸現在變成是碎形的計算。
Figure 4 修改過之程式碼的效能剖析 (按影像可放大)
現在我們將要更進一步,將計算作業也最佳化。傷腦筋的是,這次我們必須在單一函式中找出瓶頸。DrawMandel 是一個複雜的方法,因此很難知道要將焦點放在哪些計算作業上。幸好,Visual Studio 2008 取樣程式碼剖析工具預設也會收集行層級的資料,這有助於找出函式中哪幾行最耗費資源。
若要檢視行層級資料,我們要從另一個觀點來檢查效能報告。從 [目前的檢視] 功能表切換至 [模組] 檢視。[模組] 檢視與 [呼叫樹狀圖] 檢視不同,[模組] 檢視不會顯示函式如何互相呼叫,以及這些呼叫在父函式的內容中耗用多少資源的資訊。[模組] 檢視中包含的是每一個可執行檔 (組件或 DLL) 和該可執行檔中每一函式的累積樣本總數。Visual Studio Profiler 會從所有的呼叫堆疊累積該資料。
[模組] 檢視很適合觀察較宏觀的情況。例如,我們若是依 [專有樣本 %] 資料行排序,可以看到 Mandel.exe 本身就執行了 87.57% 的處理。經過我們最佳化之後,GDI+ 佔有的比例不到 3%。展開這些模組,可以看到個別方法的相同資訊。除此之外,Visual Studio 2008 中還可以將樹狀圖展開到函式層級以外,以顯示個別的行,甚至這些行裡個別指令的相同資料 (請參閱 [圖 5])。
Figure 5 跳到程式碼中經過程式碼剖析的行 (按影像可放大)
跳到原始碼會顯示 [圖 6] 所示的程式碼。此程式碼會計算最內部迴圈條件中的平方根。此作業很耗用資源,佔了應用程式處理總數的 18%。[圖 6] 中以強調顯示的幾行,是可以最佳化的程式碼。第一行使用了不必要的平方根,第二行則是 while 迴圈的不變量。
Figure 6 Code-Level Optimizations原始程式碼
for (int column = 1; column < Width; column++)
{
y = yStart;
for (int row = 1; row < Height; row++)
{
double x1 = 0;
double y1 = 0;
int color = 0;
int dept = 0;
while (dept < 100 && Math.Sqrt((x1 * x1) + (y1 * y1)) < 2)
{
dept++;
double temp = (x1 * x1) - (y1 * y1) + x;
y1 = 2 * x1 * y1 + y;
x1 = temp;
double percentFactor = dept / (100.0);
color = ((int)(percentFactor * 255));
}
//Comment this line to avoid calling Bitmap.SetPixel:
//bitmap.SetPixel(column, row, colors[color]);
//Uncomment the block below to avoid Bitmap.SetPixel:
rgbValues[row * Width + column] = colors[color].ToArgb();
y += deltaY;
}
x += deltaX;
}
最佳化的程式碼
for (int column = 1; column < this.Width; ++column)
{
y = yStart;
int index = column;
for (int row = 1; row < Height; row++)
{
double x1 = 0;
double y1 = 0;
int dept = 0;
double x1Sqr, y1Sqr;
while (dept < 100 && ((x1Sqr = x1 * x1) + (y1Sqr = y1 * y1)) < 4)
{
dept++;
double temp = x1Sqr - y1Sqr + x;
y1 = 2 * x1 * y1 + y;
x1 = temp;
}
rgbValues[index] = colors[((int)(dept * 2.55))].ToArgb();
index += Width;
y += deltaY;
}
x += deltaX;
}
修正之後,我們再重新剖析應用程式,檢查經過最佳化之程式碼的效能。建置並執行應用程式之後,碎形現在可在 1-2 秒內重新繪製。我們明顯地縮短了應用程式的啟動時間。
Visual Studio 2008 包含一項新功能,可以讓您比較兩份效能報告。為了要了解此功能的作用,我們在程式碼剖析工具下重新執行應用程式,並擷取最新的效能報告。若要查看應用程式兩個版本之間的差異,請在 [效能總管] 中選取原始和最新的報告。在報告上按一下滑鼠右鍵,然後按一下內容功能表中的 [比較效能報告] 選項。此命令會顯示新的報告,並在報告中顯示所有函式的一般檢視,以及兩份報告中該函式之個別 [專有樣本] 值之間的差異。 因為我們縮短了總執行時間,所以 DrawMandel 的相對百分比從 31.76 升高到 70.46。
為了要更清楚檢視實際的最佳化結果,我們要在 [比較選項] 窗格中,將 [資料行] 變更成為 [內含樣本] (請參閱 [圖 7])。我們也將 [臨界值] 增加到 1500 個樣本,因此不會看到任何的小波動。此外,您可能也注意到,報告預設會先顯示負面資料,亦即最佳化程度最低的函式,因為通常會使用它來尋找回復。但是,就我們的最佳化案例而言,我們要反向排序 [差異] 資料行,以便先看到最佳化程度最高的函式。請注意 DrawMandel 及其子系從 2,064 個樣本變更到 175 個樣本。最佳化超過 10 倍!您可以複製和貼上該報告的任何部分,來炫燿達成的效能改進。
Figure 7 比較 DrawMandel 的最佳化結果 (按影像可放大)
目標程式碼剖析
到目前為止,我們已示範了如何使用 Visual Studio Profiler 改進應用程式的效能。但是許多實際的應用程式都需要經歷多個使用者動作,才會出現效能問題。通常,您應該要忽視自己的案例開始執行之前所收集的任何資料。另外,也可能會在單一執行中從多個案例收集資料。
為了示範如何在這些情況下使用程式碼剖析工具,我們要改變作法,剖析一個範例電子商務網站 (事實上,我們使用的是 asp.net/downloads/starter-kits/the-beer-house 中 TheBeerHouse 範例的修改版本)。這個網站要花很長的時間載入,不過因為這是一次性成本,所以我們對於啟動時間,不如對於產品型錄為何要花很久的時間載入,以及在購物車中加入項目為何會這麼慢來得關心。
基於本文的目的,我們只會示範第一種案例的調查。但是,我們會針對這兩種案例收集資料,並示範如何篩選該資料,以著重在特定案例的效能問題。
首先,我們建立一個新的程式碼剖析工作階段。像前面一樣從 [分析] 功能表啟動 [效能精靈],然後在精靈的各頁面選擇預設值。請注意,網站的預設選項是 [檢測程式碼剖析 (Instrumentation Profiling)]。網站往往不會受到 CPU 限制;它們通常會依賴資料庫伺服器應用程式來執行較繁重的工作。因此,檢測方法是較好的選擇。
建立效能工作階段之後,我們在程式碼剖析工具下啟動網站,但是將避開啟動時間,以專注於研究單一案例。若要在 Visual Studio 2008 中執行此工作,可以在 [效能總管] 底下使用 [啟動並暫停程式碼剖析] 選項,以便在附加 Visual Studio Profiler 的情況下啟動應用程式,但是程式碼剖析工具要等到使用者繼續進行程式碼剖析時,才會開始收集資料 (請參閱 [圖 8])。
Figure 8 在啟動時暫停程式碼剖析工具
在網站載入期間,我們切換回到 Visual Studio。請注意 Visual Studio Profiler 所顯示的新工具視窗,名為 [資料收集控制]。這個視窗可以讓您多次暫停和繼續收集資料。該控制項的一個重要部分,是預先定義標記的清單。這些標記是書籤或標籤,您可以將之插入程式碼剖析資料中,以代表要研究的時間點。我們使用這些標記劃定每一個使用者案例的開始和結束。
首先,我們要使用內容功能表中的 [重新命名標記] 命令,將其中四個標記重新命名。另外也會移除未使用的標記 (請參閱 [圖 9])。我們到目前為止一直保持暫停程式碼剖析,以避免在啟動時間收集資料並準備我們的案例。等到網站載入之後,我們會再繼續程式碼剖析。
Figure 9 為測試案例命名程式碼剖析標記
我們已準備好開始第一個案例。先選取 [產品型錄要求 (Product Catalog Request)] 標記,再按一下 [插入標記] 按鈕,以標記該案例的開始。然後切換回到 Internet Explorer® 並顯示產品型錄,以完成第一個案例。等到網站顯示型錄之後,我們再插入 [已呈現產品型錄 (Product Catalog Rendered)] 標記,代表此案例的結束。我們選取啤酒蓋 (Beer Cap) 產品以轉換到下一個案例。如先前一樣,在加入之前和之後插入個別的標記。我們所有的案例到此完成,因此我們結束應用程式。
資料的分析完成之後,Visual Studio Profiler 就會顯示 [效能報告摘要]。這份報告與取樣報告稍有不同,它顯示的是最常被呼叫的函式,以及所花時間最長之函式的持續期間。請務必注意,此資料會在應用程式的存留期彙總,並包含我們的兩個案例和任何先前的活動。
很顯然,我們希望效能報告只顯示指定之案例的資料,濾除掉其餘資料。Visual Studio 2008 中的程式碼剖析工具有一個新的 [標記] 檢視,其中會列出所有插入的標記 (請注意,Visual Studio Profiler 會在程式的開始和結束時,自動插入額外的標記)。為了建立第一個案例的篩選條件,我們選取代表該案例開始和結束的標記,再選取內容功能表中的 [在標記上加入篩選條件]。這會自動建立需要的篩選條件 (請參閱 [圖 10])。除了標記之外,我們還可以依執行緒、處理序或時間間隔篩選。我們已設定好篩選條件,所以可以繼續並執行。
Figure 10 效能測試的目標程式 (按影像可放大)
請注意此篩選條件會套用至效能報告中所有的檢視。這就是為什麼 Visual Studio Profiler 會自動顯示篩選之後的新摘要頁面。此摘要頁面是產品型錄呈現案例的特定摘要。例如,我們可以發現 System.IDisposable.Dispose 花了 3.4 秒,或佔了案例執行時間的 61%,而先前所佔比率為 41%。經由篩選,我們可以真正了解此函式對我們的特定問題有多重要。
現在我們要修正此一效能問題。我們必須在程式碼中找出造成這些物件處置的函式。最容易的方式是使用「呼叫樹狀圖」配合「最忙碌路徑」功能,如 [圖 11] 所示。這會立即顯示出,原來造成大部分 IDisposable.Dispose 呼叫的是 SetInputControlsHighlight 函式。
Figure 11 使用最忙碌路徑尋找問題 (按影像可放大)
結果原來是函式包含了效能很差的記錄機制:
foreach (Control ctl in container.Controls) {
log += “Setting up control: “ + ctl.ClientID;
string tempDir =
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
using (StreamWriter sw = new StreamWriter(
Path.Combine(tempDir, “WebSite.log"), true)) {
sw.WriteLine(log);
}
...
這是偵錯期間誤留下來的記錄功能,而且已經不再提供任何特定的診斷用途,因此能夠安全地移除。Visual Studio 2008 中的「最忙碌路徑」功能,再次讓我們能夠快速地找出應用程式中的瓶頸。
不論您是使用原生 C/C++、C# 或 Visual Basic 撰寫應用程式,Visual Studio Profiler 都可大幅簡化效能的調查作業,以及協助您撰寫更快、更有效率的應用程式。Visual Studio 2008 為 Visual Studio Profiler 提供更多增強功能,使其比以往更容易發現程式中的效能瓶頸。
Hari Pulapaka 是 Microsoft Visual Studio Profiler 團隊中,負責測試計劃的軟體設計工程師。您可以透過 haripul@microsoft.com 與 Hari 連絡。
Boris Vidolov 是 Microsoft 的專案經理。他有 10 年以上的軟體業經歷,經手過許多效能關鍵應用程式。若要進一步了解 Boris,請造訪他的個人部落格 perfcop.spaces.live.com

浙公网安备 33010602011771号