VS程序性能分析器 -- 使用說明

Visual Studio 2005提供了一個方便易用的程序性能分析器,從“工具”菜單中選擇“性能工具”子菜單,即可啓動一個“性能嚮導”,通過此嚮導可完成對程序分析器的設置工作。

本節通過一個實例介紹如何使用Visual Studio 2005提供的程序性能分析器。

10.2.1  讀懂示例程序代碼

首先請讀者運行一下配套光盤中本章的示例項目PerformanceTest,程序運行界面如圖10-1所示。

圖10-1  示例項目PerformanceTest

示例項目完成的功能是給文本加上行號。請準備一個擁有數百行的長文本作爲測試文本,並事先複製到剪貼板上。

如圖10-1所示,單擊“粘貼”按鈕將準備好的長文本粘貼到文本框中,用鼠標選擇文本,或單擊“全選”按鈕選中全部文本,再單擊“插入行號”按鈕即可給選中的文本插入行號。

示例項目PerformanceText有兩個版本,請先運行一下原始版本,再運行一下性能優化後的版本,就可以直觀地感受到程序性能的提高。

程序中使用RichTextBox控件,插入行號時保持原文本的格式。

下面介紹一下RichTextBox控件的基礎知識,瞭解這些知識有助於讀者讀懂示例代碼。

RichTextBox控件接收一種RTF格式的文本,其文字可以擁有字體、字號、顏色等特性,類似於Word。RichTextBox控件的Text屬性代表其內部的所有文本,RTF屬性代表Text屬性對應的RTF格式的數據。

RichTextBox內部將文本看成是行的集合,其Lines屬性是一個字串數組,通過在Lines數組中指定一個索引,即可訪問或修改特定行。

RichTextBox另一個重要的屬性是CharIndex,它代表從文檔開頭到某個字符的索引,即文檔中的某個字符“到文檔開頭有多遠”。

如圖10-2所示爲CharIndex和Lines屬性的示意圖。

圖10-2  RichTextBox的兩個屬性示意圖

請注意區分Lines數組中的索引與CharIndex屬性。在示例代碼中將使用這兩個屬性來完成插入行號的工作。

現在分析一下原始版示例程序的代碼。

//獲取每行第一個字符的索引

//lineIndex爲整個文檔中每行的行號,從0開始

private int GetFirstCharIndexFromLine(int lineIndex)

{

    //第一行第一個字符肯定是0

    if (lineIndex == 0)

        return 0;

    //以下計算行號大於0的行的首字符索引

    int charIndex = 0;

    //累加前面的整行

    for (int i = 0; i < lineIndex; i++)

        //每行最後要多加一個字符的長度

        charIndex += this.richTextBox1.Lines[i].Length + 1;

    return charIndex;

}

以下代碼完成插入行號的功能。

//插入行號

private void InsertNumber()

{

    richTextBox1.WordWrap = false;

    // 開始行,結束行

    int beginLine =

        richTextBox1.GetLineFromCharIndex(richTextBox1.SelectionStart);

    int endLine =

        richTextBox1.GetLineFromCharIndex(richTextBox1.SelectionStart +

        richTextBox1.SelectionLength - 1);

    //獲取選中的總行數

    int totalLines = endLine - beginLine + 1;

    for (int i = 0; i < totalLines; i++)

    {

        //將插入點光標移到每行開頭

        richTextBox1.SelectionStart = GetFirstCharIndexFromLine(

                                        i + beginLine);

        richTextBox1.SelectionLength = 0;

        richTextBox1.SelectionColor = Color.Black;

        //插入行號

        richTextBox1.SelectedText = (i + 1).ToString() + " ";

    }

}

由於必須在插入行號後保持原有文字的格式,所以,不能直接修改Lines數組,因爲裏面只包含了文本信息。而是採用插入文本的方法,將光標先移到每行開頭再插入。

10.2.2  對示例程序進行性能分析

從“工具”菜單中選擇“性能工具”à“性能嚮導”命令,打開“性能嚮導”的第1頁(見圖10-3)。(如果是Vista系統, VS需要用管理員權限啓動)

圖10-3  性能嚮導(1)

如圖10-3所示,在下拉列表框中選擇分析對象。Visual Studio 2005性能分析器可以分析.EXE、.DLL和ASP.NET頁面,當前解決方案中的項目會在下拉列表中出現。

在本章示例中由於只有一個Windows應用程序項目,所以只可選擇PerformanceTest。

選擇好分析的目標之後,單擊“下一步”按鈕,進入“性能嚮導”的第2頁(見圖10-4)。

圖10-4  性能嚮導(2)

性能嚮導的第2頁要求指定分析方法,Visual Studio 2005支持兩種分析方法,即採樣與檢測。

採樣是定期中斷程序的執行,記錄並分析其函數調用情況。比較適合於找出整個程序中的性能瓶頸。

檢測可提供更多信息,主要用於比對特定函數代碼優化前後的程序性能。

一般來說,第一次運行性能分析器時建議選取“採樣”分析方法,以確定好要分析的函數。以後則採用“檢測”分析方法以開始優化代碼。

選擇好分析方法後,單擊“下一步”按鈕顯示“性能嚮導”第3頁(見圖10-5)。

圖10-5  性能嚮導(3)

運行性能嚮導結束後,一個性能資源管理器面板出現在屏幕上(見圖10‑6)。

在“性能資源管理器”中單擊“啓動” 按鈕即可啓動程序,性能分析器自動在後臺監控程序運行。

這時,可以按照一般的習慣操作程序,當程序退出時,性能分析器會自動整理收集到的信息並生成一個報告。

現在,請讀者通過“性能資源管理器”啓動程序,並向其中粘貼一個較長的文本(我的示例有140行),全選,然後單擊“插入行號”按鈕,耐心等待插入工作完成後,結束整個程序的運行。則性能分析器會自動生成一個報告文件,如圖10-7所示。

             

圖10-6  性能資源管理器                           圖10-7  自動生成性能分析報告

雙擊圖10-7中所示的報告節點PerformanceTest061217.vsp,Visual Studio 2005會在主工作區中顯示此報告的內容(見圖10-8)。

圖10-8  性能分析報告

如圖10-8所示爲採用“採樣”方法分析的結果,可以看到其中列出了執行時間最長的採樣函數列表。

大多數情況下我們不關心CLR的運行情況(如圖10-8中所示的mscorwks.dll與mscoree.dll),也不關心繫統提供給我們的“官方控件”的運行情況(如圖10-8中所示的RichEd20.DLL),因爲這些組件都不是我們所能控制的。我們只關心自己代碼的運行情況。

爲此,在圖10-8中單擊“函數”按鈕,轉入“函數”視圖(見圖10-9)。

圖10-9  性能分析報告的“函數”視圖

在圖10-9中找到“PerformanceTest.exe”節點並展開,點擊“Inclusive Percent”列進行降序排列。

“Inclusive Percent”數值反映了某函數及其調用的子函數佔用取樣樣本的比例,數值越大表明其佔用CPU時間越多,因而對程序性能影響越大。

在圖10-9中可以清晰地看到,Main()函數高居榜首,這是肯定的,因爲它是程序入口點,自然執行時間最長。

緊隨其後的有3個函數:btnInsertLine_Click()、InsertNumber()和GetFirstCharIndex FromLine(),都佔用了大量的CPU時間。

現在我們來看看這些函數的調用情況。

在圖10-9中的“InsertNumber”節點上單擊鼠標右鍵,從彈出菜單中選擇“顯示調用InsertNumber的函數”命令,將會自動轉到“調用方/被調用方”視圖(見圖10-10)。

圖10-10 “調用方/被調用方”視圖

從圖10-10中可以很清禁地看到,btnInsertLine_Click()函數調用InsertNumber()函數,佔用的樣本數均爲89.648,這說明這兩個函數“速度幾乎是一樣快的”。所以,InsertNumber()是影響btnInsertLine_Click()函數的主要因素。

再看下方顯示的InsertNumber函數調用的子函數列表,GetFirstCharIndexFromLine()以86.766的數值高居“榜首”,這說明GetFirstCharIndexFromLine()函數是影響InsertNumber()函數運行速度的“罪魁禍首”。

根據上述分析,可以很有把握地推斷,程序性能問題就出在InsertNumber()函數調用的GetFirstCharIndexFromLine()函數上。

在圖10-10中的GetFirstCharIndexFromLine()節點上單擊鼠標右鍵,從彈出菜單中選擇“查看源”命令,則自動打開GetFirstCharIndexFromLine()函數的代碼,如下所示。

01          private int GetFirstCharIndexFromLine(int lineIndex)

02          {

03              //……

04              //以下計算行號大於0的行的首字符索引

05              int charIndex = 0;

06              //累加前面的整行

07              for (int i = 0; i < lineIndex; i++)

08                  //每行最後要多加一個字符的長度

09                  charIndex += this.richTextBox1.Lines[i].Length + 1;

10              return charIndex;

11          }

在上面代碼中可以看到第7句有一個循環。

再打開InsertNumber函數的代碼,發現第8句也有一個循環語句。

01  //插入行號

02  private void InsertNumber()

03  {

04      //……

05      for (int i = 0; i < totalLines; i++)

06     {

07         //……

08         richTextBox1.SelectionStart = GetFirstCharIndexFromLine(i +

                beginLine);

09         //……

10  }

11  }

這是一個典型的“大循環套小循環”,初步估計是由於循環的次數過多影響了程序運行性能。

爲了獲取更多的信息,將“性能資源管理器”中的分析方法由“採樣”改爲“檢測”(見圖10-11)。

圖10-11  轉換分析方法

之後,再次運行性能分析會話,查看GetFirstCharIndexFromLine()函數的執行時間(見圖10-12)。

圖10-12  獲取函數執行時間(1)

爲了能方便地查看特定函數的調用次數與執行時間,我們可以隱藏圖10-12中的部分列。請在“性能分析報告”的表頭上單擊鼠標右鍵,在彈出的窗口中選擇希望顯示的列(見圖10-13)。

如圖10-13所示,“Number of Calls”列顯示函數的調用次數,“Elapsed Inclusive Time”列顯示函數(包括它所調用的子函數)所執行的時間。

圖10-13  選擇要查看的列

經過調整的性能分析報告如圖10-14所示。

圖10-14  獲取函數執行時間(2)

可以看到,GetFirstCharIndexFromLine()被調用140次,執行了3866.757339毫秒。InsertNumber()函數調用1次,執行了4301.149911毫秒。“插入行號”按鈕單擊事件響應代碼btnInsertLine_Click()函數執行4303.952901毫秒。很明顯,時間都耗在執行GetFirstCharIndexFromLine()函數上了。

因此,我們找到了程序運行緩慢的原因,主要就是被多次調用的GetFirstCharIndex FromLine()函數性能太低,此函數就是整個程序的“性能瓶頸”。

10.2.3  優化代碼

在找到程序的性能瓶頸之後,即可動手優化代碼。

1.初次優化——使用Win32 API消除循環

首先想到的就是GetFirstCharIndexFromLine()函數能否運行得更快?查看源代碼,發現其中有循環語句,能否消除?

經過上網查詢各種資料,發現.NET Framework提供的RichTextBox控件其實是由以前的ActiveX控件RichEdit包裝而來,於是想到了使用Win32 API函數SendMessage()向其發送特定的消息來直接獲取每行的第一個字符索引。

這時,需要使用平臺調用技術,寫出一個新的函數GetFirstCharIndexFromLine2()取代老的GetFirstCharIndexFromLine()函數。

//通過調用Win32 API來獲取每行第一個字符的CharIndex值

[System.Runtime.InteropServices.DllImport("user32.DLL", EntryPoint =   

    "SendMessageA", SetLastError = true)]

private static extern int SendMessage  (System.IntPtr hwnd , UInt32  wMsg ,

    int wParam , int lParam );

private const uint EM_LINEINDEX = 0xBB;

//第二種方法,速度快,直接調用Win32 API獲取每行的首字符索引

private int GetFirstCharIndexFromLine2(int lineIndex)

{

    return SendMessage(this.richTextBox1.Handle,EM_LINEINDEX,lineIndex,0);

}

修改InsertNumber()函數如下:

//插入行號

private void InsertNumber()

{

      for (int i = 0; i < totalLines; i++)

      {

      richTextBox1.SelectionStart = GetFirstCharIndexFromLine2(i+beginLine);

      }

}

現在開始檢測代碼優化後的程序性能。

從“性能資源管理器”中啓動程序,執行與上一次相同的工作任務,向一個140行的文檔中插入行號,會明顯感到速度快了很多,其性能分析報告如圖10-15所示。

圖10-15  使用Win32 API後的程序性能分析報告

從圖10-15中可以看到,GetFirstCharIndexFromLine2()函數現在執行了1.799031毫秒,而原先的GetFirstCharIndexFromLine()函數執行了3866.757339毫秒;相應地,InsertNumber()函數現在執行390.064911毫秒(原來爲4301.149911毫秒),btnInsertLine_Click()函數現在執行392.692781毫秒(原來爲4303.952901毫秒),性能的提升幅度是驚人的。

對應地,由於示例文檔有140行,所以,GetFirstCharIndexFromLine2()函數與SendMessage()函數均執行了140次。調用次數還是多了些,能否進一步優化?

2.再次優化——引入局部變量以減少函數調用次數

由於可以訪問RichTextBox的Lines數組,所以,可以只獲取選中區域的第一行第一個字符的CharIndex值,以後只需加上本行字串的長度即可得到下一行第一個字符的CharIndex值。

修改後的代碼如下:

//插入行號

private void InsertNumber()

{

    richTextBox1.WordWrap = false;

    // 開始行,結束行

    //……

    int begin =GetFirstCharIndexFromLine2(beginLine);

    for (int i = 0; i < totalLines; i++)

    {

        //將插入點光標移到每行開頭

        richTextBox1.SelectionStart = begin;

        richTextBox1.SelectionLength = 0;

        richTextBox1.SelectionColor = Color.Black;

        //插入行號

        richTextBox1.SelectedText = (i + 1).ToString() + " ";

        begin += richTextBox1.Lines[i+beginLine ].Length + 1;

    }

}

再次運行,性能分析報告如圖10-16所示。

圖10-16  使用Lines數組減少函數調用次數後的性能分析報告

從圖10-16中可以看到,GetFirstCharIndexFromLine2()函數與SendMessage()函數均執行了1次,調用次數大幅度減少。前者的執行時間爲0.352875毫秒(原來爲1.799031毫秒),快了5倍左右。

然而,很奇怪的是,btnInsertLine_Click()函數現在執行的時間是479.148527毫秒,而在引入局部變量begin之前,其執行時間爲392.692781毫秒,以前的版本速度更快。而我們的程序速度主要就體現在btnInsertLine_Click()函數上,因爲它是調用者,顯然我們畫蛇添足了,新的代碼不如舊的好。

思索一下:

爲何被調用的函數執行快了,調用次數少了,調用它的函數反而執行慢了?

對比前後所修改的地方,發現新的InsertNumber()函數就比原來的老函數多增加了一個局部變量begin,並多加了一句代碼:

begin += richTextBox1.Lines[i+beginLine].Length + 1;

問題一定就在這裏。因爲修改前後兩個代碼只有這些不同。

提示:讀者現在一定體會到“步步爲營”的重要性了,在修改代碼時,一次只改動一點,然後馬上檢測這些改動的效果如何,這樣即使出錯,也能很快地定位錯誤。

Visual Studio 2005性能分析工具還提供了更多的手段來分析程序性能問題。

在“性能資源管理器”的“性能對話”節點PerformanceTest上單擊鼠標右鍵,從彈出菜單中選擇“屬性”命令(見圖10-17)。

圖10-17  設定性能會話的屬性

在圖10-17中,選中“收集.NET對象分配信息”複選框,然後再次對比InsertNumber()函數的兩種代碼(一種是直接調用140次GetFirstCharIndexFromLine2()函數的版本,另一種是引入一個局部變量begin後的版本),其結果如圖10-18所示。

圖10-18  未引入局部變量的內存分配情況

請注意未引入局部變量begin時,btnInsertLine_Click()函數佔1604274字節的空間。再看看引入了局部變量後的內存佔用情況(見圖10-19)。

圖10-19  引入局部變量後的內存分配情況

引入局部變量begin後,btnInsertLine_Click()函數佔用13328198字節的空間,是原來的8倍多。

內存佔用量的增大和函數執行速度的減慢,估計是受以下這句代碼的影響。

begin += richTextBox1.Lines[i + beginLine].Length + 1;

對此現象的分析涉及到RichTextBox控件內部的實現代碼,C#編譯器生成的IL指令以及CLR深層技術內幕,由於資料的匱乏筆者不能對此現象進行解釋,但讀者至少可以知道,筆者“自作聰明”地引入一個局部變量begin以減少對GetFirstCharIndexFromLine2()函數的調用次數,這種做法並不能達到讓程序運行得更快的預期目的。

3.再次嘗試——使用RichTextBox自身方法取代Win32API

經過前面的試驗,我們已經得到了比較理想的方案,這就是使用Win32 API來編寫一個GetFirstCharIndexFromLine2()函數,並且不引入新的局部變量。

但仔細查看Visual Studio 2005文檔,發現.NET Framework 2.0中的RichTextBox控件比1.1版增加了一個方法,名字就叫GetFirstCharIndexFromLine(),如果用RichTextBox控件自身的方法取代自己編寫的GetFirstCharIndexFromLine2()函數,效果如何呢?

請讀者自行進行此試驗。

我的結果是:

調用RichTextBox.GetFirstCharIndexFromLine()方法的程序運行速度與調用函數GetFirstCharIndexFromLine2()(未引入局部變量begin)相比慢一些(但僅有10多毫秒的差異),內存佔用則相同。

由此我推測,RichTextBox.GetFirstCharIndexFromLine()方法的內部實現代碼也是調用Win32 API函數SendMessage()實現的。

推薦使用RichTextBox自身方法來取代Win32 API,一方面代碼更少而運行速度並未慢多少,而且更重要的是,這將使我們編寫的代碼不再依賴於特定的Windows平臺API函數,而是由.NET Framework標準控件RichTextBox幫我們封裝好的“純的”.NET代碼,從而提高了我們代碼的可移植性。

4.小結

根據這個示例項目,可以得出以下結論。

(1)需要提高程序運行速度時,應該首先想辦法確定程序的性能瓶頸所在,這時可以通過性能分析工具的“採樣”方法來找出佔用CPU時鐘週期最多的函數,從而爲找到最影響程序性能的瓶頸提供線索,這樣才能“打蛇打在七寸上,一擊致命”。

(2)找到了影響程序性能的主要函數之後,即可通過閱讀源碼對代碼進行優化,利用性能分析工具的“檢測”方法,比對代碼修改前後的程序運行性能,從而最終確定優化方案。

進行性能分析需注意的問題:

當程序中有許多項功能時,每次只分析一個功能。而且保證每次分析時使用的數據與所進行的操作都是一樣的。

本章介紹的實例是一個單獨的EXE程序。Visual Studio 2005還支持直接對DLL的性能分析,這時,需要指定一個外部的EXE文件,其方法是給DLL項目創建一個性能會話,然後點擊DLL性能會話節點,從彈出菜單中選擇“屬性”命令(見圖10-20)。

圖10-20  分析獨立的DLL文件

圖10-20所示的PublicUIComponent爲一個類庫項目,通過指定一個外部的調用此DLL的EXE文件,Visual Studio 2005就可以對DLL進行性能分析。

如果需要進行操作系統內核級別的分析,則可以在進行性能分析時加入操作系統提供的計數器(見圖10-21)。

圖10-21  選擇計數器

如圖10-21所示爲性能會話節點的屬性頁,由於筆者電腦的CPU爲Pentium IV,所以可以使用專爲Pentium IV提供的計數器。

總之,Visual Studio 2005新引入的程序性能分析器是一個很強大的工具,掌握這一工具,有助於開發出性能優異的應用程序。但前提還是得要求程序員自身對於技術要有很好的掌握,不然,使用再強大的工具也寫不出好程序來。

調用DLL中非託管函數的一般方法

首先,應該在C#語言源程序中聲明外部方法,其基本形式是:

[DLLImport(“DLL文件”)]

修飾符 extern 返回變量類型 方法名稱 (參數列表)

其中:

  ● DLL文件:指被調用的非託管函數所在的DLL文件名。

q 修飾符:訪問修飾符,除了abstract以外,在聲明方法時可以使用的修飾符,如public、private。

q 返回變量類型:在DLL文件中需調用方法的返回變量類型。

q 方法名稱:在DLL文件中需調用方法的名字。

q 參數列表:列出在DLL文件中需調用方法的參數,可查詢表10-1將C/C++的數據類型轉換爲.NET的數據類型。

表10-1  非託管函數中的數據類型與.NET的數據類型對照表

 

Wtypes.h中的非託管類型                     非託管C語言類型                .NET Framework中對應的託管類名
 
HANDLE                                             void*                              System.IntPtr
 
BYTE                                                 unsigned char                  System.Byte
 
SHORT                                              short                                System.Int16 

WORD                                              unsigned short                 System.UInt16
 
INT                                                    int                                    System.Int32
 
UINT                                                  unsigned int                    System.UInt32
 
LONG                                                 long                                 System.Int32
 
BOOL                                                 long                                 System.Int32
 
DWORD                                              unsigned long                 System.UInt32
 
ULONG                                               unsigned long                  System.UInt32
 
CHAR                                                  char                                 System.Char
 
LPSTR                                                 char*                               System.String或System.Text.StringBuilder
 
LPCSTR                                               Const char*                     System.String或System.Text.StringBuilder
 
LPWSTR                                               wchar_t*                         System.String或System.Text.StringBuilder
 
LPCWSTR                                             Const wchar_t*               System.String或System.Text.StringBuilder
 
FLOAT                                                   Float                               System.Single
 
DOUBLE                                                Double                             System.Double
 

注意:

(1)需要在程序聲明中使用System.Runtime.InteropServices命名空間。

(2)DllImport只能放置在方法聲明上。

(3)DLL文件必須位於程序當前目錄或系統定義的查詢路徑中(即:系統環境變量中Path所設置的路徑)。

(4)返回的變量類型、方法名稱、參數列表一定要與DLL文件中的原始定義相一致。

(5)若要使用其他函數名,則可以使用EntryPoint屬性設置。例如:

[DllImport("user32.dll", EntryPoint="MessageBoxA")]

static extern int MsgBox(int hWnd, string msg, string caption, int type);

事實上,在.NET中調用非託管函數還有許多複雜的問題,更詳細的技術細節請在Visual Studio 2005文檔中查詢“平臺調用”。

 

 

轉載自:http://blog.csdn.net/agan4014/archive/2008/03/20/2199522.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章