如何讓程序跑得更快些?——試試Visual Studio中的性能分析工具 (By Jun Guo)


      咦,性能?我們又回到這個永恆的話題上了。Yep,大部分程序猿都對性能有着不懈追求。某國最喜歡“多快好省”,“多”和“省”我們是很難做到了,但讓自己的程序跑得又快又好,則是我們最樂意干的活。干同樣一件事情,別人的程序要跑1分鍾,而自己的程序只要幾秒鍾,這是多爽的一件事啊(您打敗了全國99%的程序猿……)!

      不過,話雖然這樣說,但實際操作起來,效率優化並不是件容易的事。時間復雜度是最容易拉開效率差距的地方,但卻也是最難拉開人與人之間差距的地方——畢竟很多問題的解決方案都比較成熟了,要能找到個時間復雜度更優的算法似乎不是一件容易的事情。然而,即便是復雜度相同的兩份程序,由於程序常數不同,運行效率也往往有很大差異。試一下stdlib.h里的qsort,以及STL里的sort,就可以清楚地看出同樣O(NlogN)的排序算法能有多大區別。不僅如此,實際程序往往還會涉及I/O、線程通信等操作,這些操作的快慢可不是靠復雜度分析就能得出結論的了。

      因此,如何在有限時間內盡可能地提高程序效率是個非常重要而復雜的問題。Visual Studio為我們提供了強大的性能分析工具,讓我們能很快找出程序的性能瓶頸,從而能有針對性地改進程序常數。

        

      我們先看一個簡單的例子。下述程序的功能非常簡單:讀入一個文本文件,統計各個單詞出現的頻率,並輸出詞頻最高的100個單詞。單詞被簡單定義為連續的大小寫字母所組成的字符串,即I’m會被視為I和m兩個單詞。

static void Main(string[] args)
{
    const int MAX_WORD_NUM = 1000000;
    const int BUFFER_SIZE = 100000;
    const int OUTPUT_NUM = 100;

     // 讀入文件
    StreamReader sr = new StreamReader(new BufferedStream(new FileStream(
                "different.txt", FileMode.Open), BUFFER_SIZE));
    string data = sr.ReadToEnd();

      // 切割出單詞
    string[] words = Regex.Split(data, "[^a-zA-Z]");

      // 統計單詞詞頻
    Dictionary<string, int> dict = new Dictionary<string, int>((int)(MAX_WORD_NUM * 1.5));
    foreach (var word in words)
    {
        if (word == "")
            continue;

        if (dict.ContainsKey(word))
            dict[word]++;
        else
            dict.Add(word, 1);
    }

    List<Tuple<int, string>> list = new List<Tuple<int, string>>(MAX_WORD_NUM);
    foreach (var item in dict)
    {
        list.Add(Tuple.Create(item.Value, item.Key));
    }

    // 輸出詞頻最高的前100個單詞
    list.Sort();
    int count = 0;
    for (int i = list.Count - 1; i >= 0; i--)
    {
        Console.WriteLine(list[i].Item2 + " " + list[i].Item1);
count
++; if (count > OUTPUT_NUM) break; } sr.Close(); }

      文本文件different.txt大約有100MB。好,現在我們來運行程序!大概過了10s,程序輸出結果了。結果倒是正確的,但效率也未免太低了點(我之前的一篇博文有提到類似的詞頻統計程序,那個程序對320M的文本文件做詞頻統計大概只要4s,也就是說速度是上述程序的8倍)。OK,那上述程序的問題到底出在哪里?大家眾說紛紜,有的人吐槽文件讀入,因為I/O非常緩慢;有的人說是Hash表的查詢與存儲操作比較耗時(雖說理想情況下是O(1),但有沖突時會惡化);也有的人認為是最后的排序消耗了大量時間,畢竟其復雜度最高(不計字符串長度,其他操作是O(N),排序是O(NlogN),確實高了點)。

     

      為了阻止大家繼續吐槽,我們還是來試試性能分析工具好了。

      首先要確保編譯程序時采用Release編譯,之后在Visual Studio 2012中選中“分析”-->“啟動性能向導”,可以看到下圖:     

     

      我們看到有兩種性能分析方法:

  • CPU采樣
  • 檢測

      簡單來說,CPU采樣就是程序運行時,Visual Studio會定時查看當前程序正在運行哪個函數,並記錄下來。當程序運行結束后,Visual Studio就會得出一個關於程序運行時間分布的大致印象。這種做法的優點是不需要改動程序,運行較快,可以很快得出性能瓶頸。但這種方法不能得出精確數據,有時可能會有誤差。

      而檢測則指Visual Studio會將檢測代碼注入到每一個函數中,這樣整個程序的一舉一動都將被記錄在案,程序的所有性能數據都可以被精准地測量。然而這種方法會極大增加程序的運行時間,對數據的分析時間也會變得很漫長。

 

      一般來說,我們會先用CPU采樣的方式找到性能瓶頸,然后對特定的模塊采用檢測的方法進行詳細分析。由於這兩者方式的操作很類似,所以下文僅展示CPU采樣的用法。對上述程序進行CPU采樣后,我們可以看到如下報告:

     

      點擊上圖中的Main函數,我們可以查看更具體的報告,如下圖:

      

      在上圖最右側的“已調用函數”中點擊相應函數還可以跳轉到函數內各行代碼的耗時統計。由於上面的函數耗時統計已經足夠我進行性能優化,對我而言暫時沒必要具體到代碼行,這里就不再贅述了。

       可以看到,排序確實占了很多運行時間。然而,卻有一個出乎我們意料的存在——Regex.Split函數居然占了將近30%的運行時間,與此對比Hash表的查詢與插入操作卻僅僅占了1%左右的時間,至於I/O操作的開銷更是不見蹤影。也許有些人在一開始也確實猜到Split函數會比較耗時,但占用30%的時間恐怕還是絕大多數人始料未及的。

 

      我們不妨着手自己實現這個Spilt函數(不要吐槽我為什么不優先改進Sort,我在這里僅僅是展示一下嘛)。代碼如下:

// 切割出單詞
List<string> words = new List<string>(MAX_DIFF_WORD_NUM * 10);
int curPos = data.Length - 1, lastPos = curPos;
while (curPos >= 0)
{
      while (curPos >= 0 && !char.IsLetter(data[curPos]))
            curPos--;
      lastPos = curPos;

      while (curPos >= 0 && char.IsLetter(data[curPos]))
            curPos--;
      words.Add(data.Substring(curPos + 1, lastPos - curPos));
}

      之后重新運行一次性能分析。

      

      嗯,這次就合理多了,整個程序的時間基本花費在Sort上(畢竟我們還沒有改進Sort),I/O操作的開銷開始體現出來,而Spit函數所帶來的巨大開銷已經減少了許多。(為什么手動實現的Split較快呢?因為Regex.Split里檢測[^a-zA-Z]的開銷要遠遠大於!char.isLetter()的開銷,52次運算 vs 4次運算哦。)

 

      OK,那么接下來的優化目標顯然是Sort了。如何優化相信各位算法大神肯定都很清楚,因為我們只要取詞頻前100的單詞,所以沒必要完整地做排序,用個可以容納100個元素的最小堆滾一遍即可。具體就不再贅述了。

 

      就這樣,我們可以沿着“性能分析-->改進-->再性能分析”的流程,逐步提高程序的性能和我們自己的編程水平。

      要注意一點的是,寫程序時最好不要沒做分析就過早地進行“性能優化”,正如上文所提到的,雖然有人提到Hash表和I/O操作會影響性能,但從性能分析的結果來看卻非如此。這兩者所帶來的時間開銷非常少。如果不經分析就盲目優化,也許只會事倍功半。

      另外還有一點要注意的是,雖然性能分析工具指明了程序各個部分的耗時,但這也不意味我們改進效率一定要優先改進耗時最多的部分。固然,改進耗時最多的部分往往能得到最明顯的效果,但這並不意味耗時最多的部分很容易改進。像上文所示的Split函數,雖然其耗時並非最多,但由於其改進非常簡單,有時反而會成為我們優先改進的對象。在實際項目中,我們要在改進所能得到的效果以及改進所要投入的精力之間妥協,優先完成有能力做而效果又比較明顯的性能優化。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM